Preface
This article combs and integrates SpringCloud and SpringSecurity OAuth2 building process! Long time no SpringSecurity OAuth2 series of codes, forget, close-up this article comb the thread! Get dry!!!
Maven Version
Microservice Version
<spring-boot.version>2.3.2.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR9</spring-cloud.version> <spring-cloud-alibaba.version>2.2.5.RELEASE</spring-cloud-alibaba.version> <!-- spring cloud alibaba rely on --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- spring cloud rely on --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency>
SpringSecurity OAuth2 Version
<!--Security module--> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> </dependency>
The final version of spring-security-oauth2-autoconfigure automatically adapts to 2.1.2 after being qualified by the microservice version
Authorization Number Mode
At first, I don't put a bunch of configurations on it all at once. Write whatever I need, or I won't know what that configuration is for or what it is useful for! That's why I wrote this article!
AuthorizationServerConfig - AuthorizationServerConfig
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { }
SpringSecurity Core Configuration
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { }
Start Server-Access Test
Visithttp://localhost:3000/oauth/authorize?response_type=code&client_id=tao&redirect_uri=http://baidu.com&scope=all
Try entering any account password
That's because we haven't configured anything!
Configure Password Encryption
In WebSecurityConfig
@Bean public PasswordEncoder passwordEncoder() {//Password Encryption return new BCryptPasswordEncoder(); }
Configure login user account password
In WebSecurityConfig
@Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder().encode("123456")).roles("USER","ADMIN").authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("p1,p2")); //Configure global user information here }
Authorization Service Configuration Endpoint Information
AuthorizationServerConfig
@Autowired PasswordEncoder passwordEncoder; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //Memory-based for easy testing clients.inMemory()// Use in-memory storage .withClient("tao")// client_id //.secret("secret")//unencrypted .secret(passwordEncoder.encode("secret"))//encryption //.resourceIds("res1")//Resource List .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// The authorization type authorization_code,password,refresh_token,implicit,client_credentials allowed by the client .scopes("all", "ROLE_ADMIN", "ROLE_USER")// Permitted scope of authorization //.autoApprove(false)//false Jump to Authorization Page //Add Verification Callback Address .redirectUris("http://baidu.com"); }
Restart Service Testing
http://localhost:3000/oauth/authorize?response_type=code&client_id=tao&redirect_uri=http://baidu.com&scope=all
Logon Success Authorization Number
Authorization number acquisition token
The authorization code is almost done here! Next, let's try the password mode.
Unsupported grant type: password, password mode is not supported by default, need to be configured outside!
Password mode
Configure Authentication Manager
In WebSecurityConfig
@Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
Licensing Service Configuration Password Mode
AuthorizationServerConfig
@Autowired private AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) {//Configure access endpoints and token services for tokens endpoints .authenticationManager(authenticationManager)//Authentication Manager ; }
Restart Access Test
Success!
Simplified mode
That's easy. Token is displayed directly in the address barhttp://localhost:3000/oauth/authorize?client_id=tao&response_type=token&scope=all&redirect_uri=http://baidu.com
Client mode won't be demonstrated here, it doesn't really use much! So here, the authorization is basically done, as for other configurations which will be in depth below, now that we have Token, we can test the certification!
Authentication Tests
Create Test Resources
@Slf4j @RestController @RequestMapping("/mbb") public class MbbController { @GetMapping("/init") public R init(){ return R.ok(); } } @Slf4j @RestController @RequestMapping("/oth") public class OthController { @GetMapping("/init") public R init(){ return R.ok(); } } @Slf4j @RestController @RequestMapping("/test") public class TestController { @GetMapping("/init") public R init(){ return R.ok(); } }
This is actually three requests, /oth/init, /test/init, /test/init
Authentication Tests
We get Token by authorizing it in password mode
Access Test/oth/init
The same is true for other visits! Think about what the problem is? Just checked the code below to see that there is no security policy configured in WebSecurityConfig, so let's configure it!
Configure Security Policy
In WebSecurityConfig
@Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers()//All requests in the system .antMatchers("/**")//Requests Taken Over by SpringSecurity/**All Requests in the System .and() .authorizeRequests()//Request taken over by SpringSecurity .antMatchers("/test/*")///test/* in request to take over (/**) .hasAnyAuthority("p3")//Configuration requires p1 permissions .antMatchers("/mbb/*")///mbb/* in request to take over (/**) .permitAll()//No privileges required .anyRequest()//Other Requests .authenticated()//Require authentication .and() .csrf().disable()//Close csrf ; }
Be careful
Simplification mode:http://localhost:3000/oauth/authorize?client_id=tao&response_type=token&scope=all&redirect_uri=http://baidu.com
This does not authorize code mode, simplified mode jump/login is 403, why, because there is no login page configured here!, let's configure SpringSecurity
formLogin
@Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .and() .requestMatchers()//All requests in the system .antMatchers("/**")//Requests Taken Over by SpringSecurity/**All Requests in the System .and() .authorizeRequests()//Request taken over by SpringSecurity .antMatchers("/test/*")///test/* in request to take over (/**) .hasAnyAuthority("p3")//Configuration requires p1 permissions .antMatchers("/mbb/*")///mbb/* in request to take over (/**) .permitAll()//No privileges required .anyRequest()//Other Requests .authenticated()//Require authentication .and() .csrf().disable()//Close csrf ; }
Test it!
Next to the topic!
Here we configure SpringSecurity to accept all requests i.e. /** and then configure / test/* with permissions requiring p1, and / mbb/ * with direct release without permissions! Access test!
There seems to be a problem here, according to the way SpringSecurity was written before, Token is here, and our users carry p1 privileges by default. Theoretically, /oth/init only needs token to be accessed, /test/init only needs p1 privileges to be accessed to carry Token (of course, there is p1 privilege here)So it should be accessible as well, but only the / mbb/init configuration is accessible. That's okay. Think about it first! Let's look at the returned exception information! code 403, which is unauthorized! The problem is in this authentication area, here's just a padding for future articles Introduction to Separate Authentication Framework for Front and Back Ends of SpringSecurity OAuth Development
There's a key message at the end of this article!
The reason is simple: we have a Token with us here, but now the code can't support parsing this Token! Parsing this Token here is through the OAuth2Authentication Processing Filter mentioned in the previous article, we go directly to the source code to see if there is this filter! For the whole search filter, you can see the previous articles! Login, Authorization, Authentication issues arising from SpringBoot's integration with SpringSecurityOAuth2
This step is included in this article!
Source Code Analysis
There are two sets of filter chains, the first one is SpringSecurity OAuth2 default, the other is SpringSecurity which we configure ourselves to see what's inside
SpringSecurity OAuth2 This default only takes over/oauth/token, /oauth/token_key, /oauth/check_token, that is, the three requests will be SpringSecurityOAuth2 processing, then only process /** for our own configured SpringSecurity Obviously our / test/init, /oth/init, /mbb/init will be taken over by SpringSecurity, but note that the filter on this filter chain does not resolve Token's OAuth2Authentication Processing Filter, which also leads us to the filter even if Token is added and the permissions are met, or 403 unauthorized! Normally in Spring SecurityIn OAuth2 system, SpringSecurity only cooperates with authorization operation, and certification is handed over to resource service. It is also mentioned here that normally, authorization service and resource service are not set up in the same server. Here you can understand that the authentication service and resource service code are not written together. Then we want resource to take over authorization service.What about resources! Of course there is a way, but it's better to go around! Follow me, old driver takes you out of the pit!
Resource taking over request
As mentioned above, resource clothing has two access methods, one is to write resource clothing together with authorized clothing, the other is to develop resource clothing and authorized clothing independently! I'll show both of them here!
Authorized clothing and resource clothing are written together
Create ResourceServerConfig
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)//Open Note Authentication public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //Resource service id identification public static final String RESOURCE_ID = "res1"; //Configure Resource Service Security Rules @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests()//Authorized Request .anyRequest()//Any Request .authenticated()//Require authentication .and().csrf().disable(); } }
The resource service we are currently configuring takes over all requests, that is, all requests need to be authenticated! SpringSecurity's security policy remains the same! Restart the server and access the test!
It looks like something is wrong. Let's debug the source code and check the filter chain group!
Filter chain group is available, resource service is added, compared to the filter OAuth2AuthenticationProcessing Filter which handles Token, but why does the response returned here still show an invalid Token because Token's storage policy is configured here because we don't tell the resource service's storage policy!We don't know where to check, so we need to configure Token's storage policy!
Configure Token Storage Policy
@Configuration public class TokenConfig { @Bean public TokenStore tokenStore() { //Store tokens in memory (plain tokens) return new InMemoryTokenStore(); } }
Define AuthorizationServerTokenServices
In the AuthorizationServerConfig class
@Autowired private TokenStore tokenStore; @Autowired private ClientDetailsService clientDetailsService; //Token Management Service @Bean public AuthorizationServerTokenServices tokenService() { DefaultTokenServices service = new DefaultTokenServices(); service.setClientDetailsService(clientDetailsService);//Client Detail Service service.setSupportRefreshToken(true);//Support refresh token service.setTokenStore(tokenStore);//Token Storage Policy service.setAccessTokenValiditySeconds(7200); // The default token expiration time is 2 hours service.setRefreshTokenValiditySeconds(259200); // Refresh token default validity period 3 days return service; }
Change AuthorizationServerEndpointsConfigurer configuration
In the AuthorizationServerConfig class
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) {//Configure access endpoints and token services for tokens endpoints .authenticationManager(authenticationManager)//Authentication Manager .tokenServices(tokenService()) ; }
Resource Server Configuration Token Storage Policy-ResourceServerSecurityConfigurer
In ResourceServerConfig class
@Autowired TokenStore tokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID)//Resource id .tokenStore(tokenStore) .stateless(true); }
Modify Resource Permissions
@Slf4j @RestController @RequestMapping("/mbb") public class MbbController { @PreAuthorize("hasAuthority('p8')") @GetMapping("/init") public R init(){ return R.ok(); } } @Slf4j @RestController @RequestMapping("/oth") public class OthController { @PreAuthorize("hasAuthority('p1')") @GetMapping("/init") public R init(){ return R.ok(); } } @Slf4j @RestController @RequestMapping("/test") public class TestController { @GetMapping("/init") public R init(){ return R.ok(); } }
Here we configure that/mbb/init does not require privileges, /oth/init requires p1 privileges, /test/init requires p8. At first we configure the user with p1 and p2 privileges by default. The security policy in SpringSecurity remains unchanged, or is it the following: SpringSecurity takes over the request, /test/* requires p1 privileges, /mbb/* is all released, which looks like the Controller interfaceThere are conflicting annotation labels on the page! I actually wrote this intentionally! Wait to demonstrate!
@Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .and() .requestMatchers()//All requests in the system .antMatchers("/**")//Requests Taken Over by SpringSecurity/**All Requests in the System .and() .authorizeRequests()//Request taken over by SpringSecurity .antMatchers("/test/*")///test/* in request to take over (/**) .hasAnyAuthority("p3")//Configuration requires p1 permissions .antMatchers("/mbb/*")///mbb/* in request to take over (/**) .permitAll()//No privileges required .anyRequest()//Other Requests .authenticated()//Require authentication .and() .csrf().disable()//Close csrf ; }
Restart Service-Test
This is interesting because / oth/init is accessible because the current user of hi has p1 and p2 privileges, and / test/init is accessible because no privileges are set here, but here SpringSecurity's security policy is to configure p3 privileges, where the user has only p1 and p2 privileges, so it should not be accessible in theory, but it is actually accessibleYes, so there's a conflict here. I'll mark this question first to mark question 1Explain later, /mbb/init is inaccessible! This is because P8 is required for annotation on the / mbb/init request and is inaccessible because the user does not have p8, but there is a conflict with the security policy in SpringSecurity. The security policy in SpringSecurity is configured to allow all requests. This problem is actually the same as Question 1 labeled above!The case where this resource clothing is written with authorized clothing here already limits resources, so here's how to interpret Markup Question 1
Markup Question 1
Here we look at the previous articles SpringSecurityOAuth2 can't skip to get authorization code address after login, skip to root path directly
Here we can explain that the server currently has three sets of filter chains, the first one is for us, the second one is for resource service, the third one is for SpringSecurity, and there is a sequential d-value. This is critical because we know that a path can only be handled by one filter, where resources take over all requests and SpringSecurity takes over all requests.Here, however, both the Resource Suit and the Spring Security take over all requests, but a request can only be processed by one filter chain, so this means that which filter chain is in front of the other, and the rules for that filter configuration will take effect. This detail can be seen in the article above, where the order of this filter chain group can also be adjusted by Order.!
Resource Suits Deployed Separately
Create Test Suits
pom is as good as above
Resource controller s are as good as before
@Slf4j @RestController @RequestMapping("/mbb") public class MbbController { @PreAuthorize("hasAuthority('p8')") @GetMapping("/init") public R init(){ return R.ok(); } } @Slf4j @RestController @RequestMapping("/oth") public class OthController { @PreAuthorize("hasAuthority('p1')") @GetMapping("/init") public R init(){ return R.ok(); } } @Slf4j @RestController @RequestMapping("/test") public class TestController { @GetMapping("/init") public R init(){ return R.ok(); } }
Resource Service Configuration
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { public static final String RESOURCE_ID = "res1"; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID)//Resource id .tokenServices(tokenService())//Service for validating tokens .stateless(true); } @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests()//Authorized Request .anyRequest()//Any Request .authenticated()//Require authentication .and().csrf().disable(); } @Bean @Primary public ResourceServerTokenServices tokenService() { //DefaultTokenServices can be used for local validation if resource and certification are combined //To verify token using a remote service request authorization server, you must specify the url, client_id, client_secret to verify token RemoteTokenServices service=new RemoteTokenServices(); service.setCheckTokenEndpointUrl("http://localhost:3000/oauth/check_token"); service.setClientId("tao"); service.setClientSecret("secret"); return service; } }
Exclude resource clothing configuration from authorized clothing
Delete ResourceServerConfig directly!
Open Licensing Service Remote Inspection token
Here we have written resource and authorized clothing separately, so Token can not be shared. You can check Token remotely. AuthorizationServerConfig is configured as follows
/** * @Description: Security constraints used to configure token endpoints * Token The token is signed by default, and the resource service needs to verify the signature. * So you need to use a symmetric Key value to participate in the signature calculation. * This Key value exists in both the Authorization Service and the Resource Service. * Or you can sign Token using an asymmetric encryption algorithm. * Public Key Published in the URL connection /oauth/token_key, * The default access security rule is "denyAll()". * That is, it is turned off by default, * You can inject a standard SpEL expression into the AuthorizationServerSecurityConfigurer configuration to turn it on * (For example, it may be appropriate to use "permitAll()" to open because it is a public key. */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security .tokenKeyAccess("permitAll()")//Open access tokenkey (this is open when doing JWT! That is, the resource service can get JWT-encrypted keys through this request. There is no use here at this time, and there is no need to configure it) .checkTokenAccess("permitAll()")//Open Remote Token Check/oauth/check_token body token .allowFormAuthenticationForClients();//Allow client to authenticate using form }
Other configurations remain intact! Restart server testing!
Get Token from Password Mode
Requests here are only taken over by resource servers, so requests here are expected. If we have hundreds of resource servers, then each request needs to be authenticated by a remote call to the authorization service, then our authorization service will be under a lot of pressure, so we can use the JWT mentioned in the code above, and then we will turn on the configuration JWT issuance Token!
Before you start JWT, I recommend that you first learn about TokenStore and then, by the way, AuthorizationCodeServices
AuthorizationCodeServices
SpringSecurity OAuth2 About AuthorizationCodeServices
TokenStore
SpringSecurity OAuth2 About TokenStore
JWT Replace Default Token
SpringSecurity OAuth replaces the default Token with JWT
Custom JWT data
SpringSecurityOAuth2 customizes JWT data in a JWT-generated Token pattern
Getting data in JWT
SpringSecurityOAuth2 Gets Data in JWT
So here, we have a better understanding of token generation and storage strategies, so now our authorization clothing is to use JWT to generate Token, customize some data, and then store one in Redis. Although JWT is self-contained and can set expiration time, this is to meet the actual business, for example, when we disable it for users, and thenThe previously issued JWT-format Token still parses the data, so it doesn't conform to the business logic here, so here's a copy stored in Redis to delete the Token in Redis when we disable the user, so that the old JWT-format Token that the user has disabled will fail to compare in Redis, so that the user status information can be updated in time!Let's test resource acceptance with the code we have now
Resource Accreditation JWT Format Token
Because we useJWT-formatted Token, then this Token carries permission information. As long as you configure the resource service, JWT's Token-formatted resource service has helped us understand it well! We configure the JWT-formatted Token in the authorization service according to the above article, then the authorization service does not need to be activated, since the Token carries the permission information, that means the resource service does not need remote access grant.Token check, we have transformed the core configuration of resource clothing! Below!
Improve core configuration of resource clothing
/** * @description: Resource Clothing Core Configuration * @author TAO * @date 2021/9/15 23:40 */ @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { public static final String RESOURCE_ID = "res1"; @Autowired TokenStore tokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID)//Resource id .tokenStore(tokenStore) .stateless(true); } @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests()//Authorized Request .anyRequest()//Any Request .authenticated()//Require authentication .and().csrf().disable(); } }
Notice that there is a tokenStore injection here, so it needs to be consistent with the authorization service, the authorization service is Redis, and the authorization service is Redis. Then the resource service knows where to read the Token and parse the comparison through the Token obtained from Redis, so this tokenStore is very important!,Usually we pull out the tokenStore so that the resource and authorized clothing share the same tokenStore, so that tokenStore is accessed in the same way!
TokenStore
/** * @description: Token Stores the core configuration, defaulting to program memory, based on the configuration when tokenStore is configured * @author TAO * @date 2021/9/15 22:22 */ @Slf4j @Configuration public class TokenStoreConfig { /** * Redis OAuth-related prefixes in */ String OAUTH_ACCESS = "yy:access:"; @Autowired private RedisConnectionFactory redisConnectionFactory; /** *Store tokens in memory (plain tokens) * @return */ @Bean @ConditionalOnProperty(prefix = "security.oauth2", name = "tokenStore", havingValue = "memory" ,matchIfMissing=true) public TokenStore tokenStoreInMemory() { log.info("===>"+"tokenStoreInMemory"); return new InMemoryTokenStore(); } /** * Token Persist Redis * Configuration when using redis, * prefix Check Configured Prefixes * name Properties Checked * havingValue The configuration class takes effect when the value is redis * Default effective (matchIfMissing = true) * @author tao */ @Bean @ConditionalOnProperty(prefix = "security.oauth2", name = "tokenStore", havingValue = "redis") public TokenStore tokenStoreInRedis() { log.info("===>"+"tokenStoreInRedis"); RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory); tokenStore.setPrefix(OAUTH_ACCESS);//Setting OAuth-related prefixes in Redis return tokenStore; } }
Here I'm dividing the authorization and resource services into modules so that the entire configuration shared by SpringSecurity OAuth2 is written into common, and then the authorization and resource services refer to the configuration in common. This makes it very convenient to use later, simply create a normal amount of SpringWeb projects can directly reference authorization service dependency if they need authorization service, or directly refer to resource service dependency if they need resource service, which ensures that we do not need to develop redundant code and makes the architecture more reusable.
Restart Server Test Resource Suit Verification JWT Format Token
Resource permissions or unchanged
/mbb/init still needs p8 privileges, /oth/init or p1, /test/init does not set privileges, but Token is required. Our users still need p1, p2 privileges by default, so the test starts!
Test Successful! So here is the Isolated Resource Authentication JWT Format Token. Now that the above mentioned JWT Format Token is self-contained, we will parse the data in JWT. Another problem here is that the authentication failed to return, where/mbb/init has insufficient privileges and the returned information is SpringSecurity.OAuth2 default information, not very accurate, not very friendly to the front-end, users, then this will be modified in the future! Boiled down to SpringSecurity OAuth2 exception handling!
Parsing JWT custom data
I've written this article in the past and it's very detailed. It's no longer futile here. SpringSecurityOAuth2 Gets Data in JWT
SpringSecurity OAuth2 exception handling
Because SpringSecurity OAuth2 exception handling is a long section, pick it up to write an article! SpringSecurity OAuth2 exception handling OAuth2Exception