Event address: CSDN 21-day learning challenge
related articles:
- Definition and operation process of OAuth2
- Spring Security OAuth implements Gitee quick login
- Spring Security OAuth implements GitHub quick login
- Spring Security's filter chain mechanism
- Spring Security OAuth Client configuration loading source code analysis
- Detailed Explanation of Spring Security Built-in Filters
- Why are two OAuth2AuthorizationRequestRedirectFilter analysis loaded
foreword
We have connected GitHub and Gitee clients before, and using OAuth2 Client can quickly and conveniently integrate third-party logins. On the one hand, integrating third-party logins reduces the cost of customer acquisition for enterprises, and at the same time provides users with a more convenient login experience. But with the development and growth of enterprises, it is more and more necessary to build their own OAuth2 server. OAuth2 includes not only the previous OAuth client, but also the authorization server. Here we need to build our own authorization server with minimal configuration. The authorization server mainly provides functions such as OAuth Client registration, user authentication, token distribution, token verification, and token refresh. In practical applications, the authorization server and the resource server can be implemented in the same application, or can be split into two independent applications. Here, for the convenience of understanding, we split them into two applications.
Authorization Server Changes
The authorization server (Authorization Server) is not currently integrated in the Spring Security project, but exists in the Spring ecosystem as an independent project. Figure 1 shows the position of the Spring Authorization Server in the Spring project list.

figure 1
Why is Spring Authorization Server not integrated in Spring Security?
The reason is that both Spring Security OAuth and Spring Cloud Security in Spring have their own implementation of OAuth. The Spring team initially wanted to separate OAuth into Spring Security, but later the Spring team realized that the OAuth authorization service is not suitable for inclusion in In the Spring Security framework, Spring announced in November 2019 that it will not support the authorization server in Spring Security. The original text is as follows:
original: Since the Spring Security OAuth project was created, the number of authorization server choices has grown significantly. Additionally, we did not feel like creating an authorization server was a common scenario. Nor did we feel like it was appropriate to provide authorization support within a framework with no library support. After careful consideration, the Spring Security team decided that we would not formally support creating authorization servers.
But the community reacted strongly to the fact that Spring Security no longer supports authorization servers. So in April 2020, Spring launched the Spring Authorization Server project. At present, the latest GA version of the project is 0.3 GA, and the preview version is 1.0.0-M1.
Minimal configuration
Install the authorization server
1. Create a new Spring Boot project named spring-security-authorization-server 2. Introduce pom dependencies
copy<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>0.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
Configure Authorization Server
copyimport com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ClientSettings; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.util.matcher.RequestMatcher; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.UUID; @Configuration(proxyBeanMethods = false) public class AuthorizationServerConfig { //Authorization endpoint filter chain @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); RequestMatcher endpointsMatcher = authorizationServerConfigurer .getEndpointsMatcher(); http //Without authentication, it will automatically jump to the /login page .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("/login")) ) .requestMatcher(endpointsMatcher) .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated() ) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .apply(authorizationServerConfigurer); return http.build(); } //Filter chain for authentication @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } //Configure principal user @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() .username("user") .password("user") .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } //register client @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) //client id .clientId("testClientId") //Client secret key, authorized server needs encrypted storage .clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("testClientSecret")) //authorization method .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) //Supported authorization types .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) //Callback address, supports multiple, local test cannot use localhost .redirectUri("http://127.0.0.1:8080/login/oauth2/code/customize") .scope(OidcScopes.OPENID) //authorization scope .scope("message.read") .scope("userinfo") .scope("message.write") //Whether an authorization page is required, open and jump to the authorization page, manual confirmation is required .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); return new InMemoryRegisteredClientRepository(registeredClient); } //token encryption @Bean public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } //Configure protocol endpoints, such as /oauth2/authorize, /oauth2/token, etc. @Bean public ProviderSettings providerSettings() { return ProviderSettings.builder().build(); } }
The above is the configuration of the minimal authorization server. Here we store the authorization subject and the client in memory, and of course they can also be persisted in the database, using JdbcUserDetailsManager and JdbcRegisteredClientRepository respectively. ProviderSettings.builder().build() uses the default configuration, and we will use these addresses later:
copypublic static Builder builder() { return new Builder() .authorizationEndpoint("/oauth2/authorize") .tokenEndpoint("/oauth2/token") .jwkSetEndpoint("/oauth2/jwks") .tokenRevocationEndpoint("/oauth2/revoke") .tokenIntrospectionEndpoint("/oauth2/introspect") .oidcClientRegistrationEndpoint("/connect/register") .oidcUserInfoEndpoint("/userinfo"); }
❗ The official pointed out that @Import(OAuth2AuthorizationServerConfiguration.class) can also be used to minimize configuration, but I personally tested that this method is not very useful, and there are still problems.
configure client
Here we want to use our own to build an authorization server, we need to customize a client, or use the previous example of integrating GitHub, as long as we expand it in the configuration file. The complete configuration is as follows:
copyspring: security: oauth2: client: registration: gitee: client-id: gitee_clientId client-secret: gitee_secret authorization-grant-type: authorization_code redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' client-name: Gitee github: client-id: github_clientId client-secret: github_secret # customize customize: client-id: testClientId client-secret: testClientSecret authorization-grant-type: authorization_code redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' client-name: Customize scope: - userinfo provider: gitee: authorization-uri: https://gitee.com/oauth/authorize token-uri: https://gitee.com/oauth/token user-info-uri: https://gitee.com/api/v5/user user-name-attribute: name # customize customize: authorization-uri: http://localhost:9000/oauth2/authorize token-uri: http://localhost:9000/oauth2/token user-info-uri: http://localhost:9000/userinfo user-name-attribute: username
❗ When configuring the authorization server uri, do not still use 127.0.0.1, because it is a local test, the session of the authorization server and the session of the client will overwrite each other, resulting in inexplicable problems. Please distinguish between the callback address and the address of the authorization server endpoint uri.

client session

Authorization server session
to experience
In addition, in order to enable better debugging, you can add @EnableWebSecurity(debug = true) and log logs in the two applications. The logs are as follows, and open the TRACE level log:
copylogging: level: root: INFO org.springframework.web: INFO org.springframework.security: TRACE org.springframework.security.oauth2: TRACE
Now start two applications, visit http://127.0.0.1:8080/hello, and automatically jump to the login page.

Click Customize, it will jump to the authorization server, pay attention to see that the address bar address is localhost:9000/login, enter the username/password to log in, user/user.

After logging in, it will jump to the authorization page. Since we have not customized it, we are using the default page. You can see that the address of the page is http://localhost:9000/oauth2/authorize?response_type=code&client_id=testClientId&scope=userinfo&state=yV1ElAN2855yq3bY5kgj_rmilnCclyvZHkxVB7a1d84%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/customize.

We check userinfo and jump back to the client after submission. Let's look at the log received by the client, and the authorization server called back the callback address we filled in with the code. Request received for GET '/login/oauth2/code/customize?code=DPAlx5uyrUpfrZIlBKrpIy_mmcgiyC2qCxPFtUeLA0fBrZd238XM2vN8M1jv9XAgl0KA-D54P_KzVH7RbUw7ApBUc2pbnuSVRZUyHazozmNM4YgQ06CZryfr20qLRhW4&state=_Sgak7GLILLKbwr9JVuwA2xVp95CWPgUMByQcvePkgM%3D'
copy************************************************************ Request received for GET '/login/oauth2/code/customize?code=DPAlx5uyrUpfrZIlBKrpIy_mmcgiyC2qCxPFtUeLA0fBrZd238XM2vN8M1jv9XAgl0KA-D54P_KzVH7RbUw7ApBUc2pbnuSVRZUyHazozmNM4YgQ06CZryfr20qLRhW4&state=_Sgak7GLILLKbwr9JVuwA2xVp95CWPgUMByQcvePkgM%3D': org.apache.catalina.connector.RequestFacade@1a8761d0 servletPath:/login/oauth2/code/customize pathInfo:null headers: host: 127.0.0.1:8080 connection: keep-alive upgrade-insecure-requests: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 sec-fetch-site: cross-site sec-fetch-mode: navigate sec-fetch-user: ?1 sec-fetch-dest: document sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" referer: http://127.0.0.1:8080/ accept-encoding: gzip, deflate, br accept-language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7 cookie: JSESSIONID=2527F412F53FA27A30BFBC39161ABB63 Security filter chain: [ DisableEncodeUrlFilter WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter CsrfFilter LogoutFilter OAuth2AuthorizationRequestRedirectFilter OAuth2AuthorizationRequestRedirectFilter OAuth2LoginAuthenticationFilter DefaultLoginPageGeneratingFilter DefaultLogoutPageGeneratingFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter OAuth2AuthorizationCodeGrantFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ] ************************************************************
Summarize
The configuration of Spring Security's minimal authorization server is over here. Although the amount of code in this demo is very small, it involves a lot of knowledge and has many pitfalls.
The code description in the Spring Security document is not updated in time. For example, the @Import(OAuth2AuthorizationServerConfiguration.class) document states that it is a minimal configuration, but the quick start of the document provides another minimal configuration method.
In addition, if an exception occurs on the authorization server, it will not print the stack, but put the error information into the response, which is intended to be displayed on the page. However, the default error page of the demo does not display the error details, only the error number 400 , as shown in the figure.

Spring Authorization Server still needs a lot of improvement, and Spring Security is no exception. Not long ago, I also submitted a PR to fix a bug that lasted for several versions Bugs in 😅), I have seen a lot of foreign products, but they are not much better than domestic open source projects, and there are many pitfalls. However, the open source projects of some of our big factories are actually very good, but they are sprayed by netizens.
