Spring Boot 最新版3.x 集成 OAuth 2.0实现认证授权服务、第三方应用客户端以及资源服务
作者:mmseoamin日期:2023-12-14

目录

  • 前言
  • 本文开发环境介绍
  • 开发环境端口说明
  • 认证授权服务
    • pom.xml依赖
    • 新建Oauth2ServerAutoConfiguration类
    • main函数
    • yml配置
    • 第三方应用OAuth客户端
      • pom.xml依赖
      • 新建Oauth2ClientAutoConfiguration类
      • 新建OauthClientDemoController类
      • main函数
      • yml配置
      • 资源服务
        • pom.xml依赖
        • 新建ResourceServerAutoConfiguration类
        • 新建UserController类
        • main函数
        • yml配置
        • 演示
        • OAuth客户端openid演示
        • 结束

          前言

          Spring Boot 3已经发布一段时间,网上关于Spring Boot 3的资料不是很多,本着对新技术的热情,学习和研究了大量Spring Boot 3新功能和新特性,感兴趣的同学可以参考Spring官方资料全面详细的新功能/新改进介绍

          • Spring版本升级到6.x
          • JDK版本至少17+
          • 新特性有很多,本文主要针对OAuth 2.0的集成,如果快速开发自己的认证授权服务、OAuth客户端以及资源服务

            本文开发环境介绍

            开发依赖版本
            Spring Boot3.0.2

            开发环境端口说明

            新建三个服务,分别对应认证授权服务、OAuth客户端以及资源服务

            服务端口
            认证授权服务8080
            OAuth客户端服务8081
            资源服务8082

            认证授权服务

            pom.xml依赖

            Spring发布了spring-security-oauth2-authorization-server项目,目前最新版是1.0版,pom.xml依赖如下

            
                
                    org.springframework.boot
                    spring-boot-starter-web
                
                
                    org.springframework.boot
                    spring-boot-starter-security
                
                
                    org.springframework.security
                    spring-security-oauth2-authorization-server
                    ${spring-security-oauth2-authorization-server.version}
                
            
            

            新建Oauth2ServerAutoConfiguration类

            package com.wen3.oauth.ss.authserver.authconfigure;
            import 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.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.resource.OAuth2ResourceServerConfigurer;
            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.oauth2.core.AuthorizationGrantType;
            import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
            import org.springframework.security.oauth2.core.oidc.OidcScopes;
            import org.springframework.security.oauth2.jwt.JwtDecoder;
            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.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
            import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
            import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
            import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
            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.AntPathRequestMatcher;
            import java.security.KeyPair;
            import java.security.KeyPairGenerator;
            import java.security.interfaces.RSAPrivateKey;
            import java.security.interfaces.RSAPublicKey;
            import java.util.UUID;
            @Configuration
            public class Oauth2ServerAutoConfiguration {
                @Bean
                @Order(1)
                public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
                    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
                    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                            .oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
                    http
                            // Redirect to the login page when not authenticated from the
                            // authorization endpoint
                            .exceptionHandling((exceptions) -> exceptions
                                    .authenticationEntryPoint(
                                            new LoginUrlAuthenticationEntryPoint("/login"))
                            )
                            // Accept access tokens for User Info and/or Client Registration
                            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
                    return http.build();
                }
                @Bean
                @Order(2)
                public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
                    http
                            .authorizeHttpRequests((authorize) -> authorize
                                    .requestMatchers(new AntPathRequestMatcher("/actuator/**"),
                                            new AntPathRequestMatcher("/oauth2/**"),
                                            new AntPathRequestMatcher("/**/*.json"),
                                            new AntPathRequestMatcher("/**/*.html")).permitAll()
                                    .anyRequest().authenticated()
                            )
                            // Form login handles the redirect to the login page from the
                            // authorization server filter chain
                            .formLogin(Customizer.withDefaults());
                    return http.build();
                }
                @Bean
                public UserDetailsService userDetailsService() {
                    UserDetails userDetails = User.withDefaultPasswordEncoder()
                            .username("test")
                            .password("test")
                            .roles("USER")
                            .build();
                    return new InMemoryUserDetailsManager(userDetails);
                }
                @Bean
                public RegisteredClientRepository registeredClientRepository() {
                    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                            .clientId("demo-client-id")
                            .clientSecret("{noop}demo-client-secret")
                            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            //                .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).build())
                            .redirectUri("http://127.0.0.1:8081/login/oauth2/code/client-id-1")
                            .redirectUri("http://127.0.0.1:8081/login/oauth2/code/client-id-2")
                            .scope(OidcScopes.OPENID)
                            .scope(OidcScopes.PROFILE)
                            .scope("message.read")
                            .scope("message.write")
                            .scope("user_info")
                            .scope("pull_requests")
                            // 登录成功后对scope进行确认授权
                            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                            .build();
                    return new InMemoryRegisteredClientRepository(registeredClient);
                }
                @Bean
                public JWKSource 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;
                }
                @Bean
                public JwtDecoder jwtDecoder(JWKSource jwkSource) {
                    return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
                }
                @Bean
                public AuthorizationServerSettings authorizationServerSettings() {
                    return AuthorizationServerSettings.builder().build();
                }
            }
            

            main函数

            @SpringBootApplication
            public class OauthServerApplication {
                public static void main(String[] args) {
                    SpringApplication.run(OauthServerApplication.class, args);
                }
            }
            

            yml配置

            server:
              port: 8080
            

            第三方应用OAuth客户端

            pom.xml依赖

            
                
                    org.springframework.boot
                    spring-boot-starter-web
                
                
                    org.springframework.boot
                    spring-boot-starter-security
                
                
                    org.springframework.boot
                    spring-boot-starter-oauth2-client
                
            
            

            新建Oauth2ClientAutoConfiguration类

            package com.wen3.oauth.ss.authclient.autoconfigure;
            import org.springframework.context.annotation.Bean;
            import org.springframework.context.annotation.Configuration;
            import org.springframework.security.config.annotation.web.builders.HttpSecurity;
            import org.springframework.security.config.http.SessionCreationPolicy;
            import org.springframework.security.web.SecurityFilterChain;
            @Configuration
            public class Oauth2ClientAutoConfiguration {
                @Bean
                public SecurityFilterChain authorizationClientSecurityFilterChain(HttpSecurity http) throws Exception {
                    http
                            .authorizeHttpRequests()
                            .anyRequest().authenticated()
                            .and().logout()
                            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
                            .and().oauth2Client()
                            .and().oauth2Login();
                    return http.build();
                }
            }
            

            新建OauthClientDemoController类

            package com.wen3.oauth.ss.authclient.controller;
            import lombok.extern.slf4j.Slf4j;
            import org.springframework.security.core.Authentication;
            import org.springframework.security.core.context.SecurityContextHolder;
            import org.springframework.web.bind.annotation.RequestMapping;
            import org.springframework.web.bind.annotation.RestController;
            @Slf4j
            @RestController
            public class OauthClientDemoController {
                @RequestMapping(path = "/hello")
                public String demo() {
                    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                    log.info("authentication: {}", authentication);
                    return "hello";
                }
            }
            

            main函数

            @SpringBootApplication
            public class OauthServerApplication {
                public static void main(String[] args) {
                    SpringApplication.run(OauthServerApplication.class, args);
                }
            }
            

            yml配置

            server:
              port: 8081
              servlet:
                session:
                  cookie:
                    # 需要更换存放sessionId的cookie名字,否则认证服务和客户端的sessionId会相互覆盖
                    name: JSESSIONID-2
            spring:
              security:
                oauth2:
                  client:
                    registration:
                      client-id-1:
                        provider: demo-client-id
                        client-id: demo-client-id
                        client-secret: demo-client-secret
                        authorization-grant-type: authorization_code
                        redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
                        #            client-authentication-method: POST
                        scope: user_info, pull_requests
                        client-name: demo-client-id
                      client-id-2:
                        provider: demo-client-id2
                        client-id: demo-client-id
                        client-secret: demo-client-secret
                        authorization-grant-type: authorization_code
                        redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
                        #            client-authentication-method: POST
                        scope: user_info, pull_requests
                        client-name: demo-client-id2
                    provider:
                      demo-client-id:
                        authorization-uri: http://127.0.0.1:8080/oauth2/authorize
                        token-uri: http://127.0.0.1:8080/oauth2/token
                        user-info-uri: http://127.0.0.1:8082/user/info
                        user-name-attribute: name
                        jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
                      demo-client-id2:
                        authorization-uri: http://127.0.0.1:8080/oauth2/authorize
                        token-uri: http://127.0.0.1:8080/oauth2/token
                        user-info-uri: http://127.0.0.1:8082/user/info
                        user-name-attribute: name
                        jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
            

            资源服务

            pom.xml依赖

            
                
                    org.springframework.boot
                    spring-boot-starter-web
                
                
                    org.springframework.boot
                    spring-boot-starter-security
                
                
                    org.springframework.boot
                    spring-boot-starter-oauth2-resource-server
                
            
            

            新建ResourceServerAutoConfiguration类

            package com.wen3.oauth.ss.resourceserver.autoconfigure;
            import org.springframework.context.annotation.Bean;
            import org.springframework.context.annotation.Configuration;
            import org.springframework.security.config.annotation.web.builders.HttpSecurity;
            import org.springframework.security.web.SecurityFilterChain;
            @Configuration
            public class ResourceServerAutoConfiguration {
                @Bean
                public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
                    http.authorizeHttpRequests().anyRequest().authenticated().and()
                            .oauth2ResourceServer().jwt();
                    return http.build();
                }
            }
            

            新建UserController类

            package com.wen3.oauth.ss.resourceserver.controller;
            import jakarta.servlet.http.HttpServletRequest;
            import jakarta.servlet.http.HttpServletResponse;
            import org.springframework.http.MediaType;
            import org.springframework.web.bind.annotation.RequestMapping;
            import org.springframework.web.bind.annotation.RequestMethod;
            import org.springframework.web.bind.annotation.ResponseBody;
            import org.springframework.web.bind.annotation.RestController;
            import java.util.HashMap;
            import java.util.Map;
            @RestController
            public class UserController {
                @RequestMapping(path = "/user/info", method = {RequestMethod.GET,RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE)
                @ResponseBody
                public Map getUser(HttpServletRequest request, HttpServletResponse response) {
                    Map map = new HashMap<>();
                    map.put("name", "xxx");
                    return map;
                }
            }
            

            main函数

            package com.wen3.oauth.ss.resourceserver;
            import org.springframework.boot.SpringApplication;
            import org.springframework.boot.autoconfigure.SpringBootApplication;
            @SpringBootApplication
            public class ResourceServerApplication {
                public static void main(String[] args) {
                    SpringApplication.run(ResourceServerApplication.class, args);
                }
            }
            

            yml配置

            server:
              port: 8082
            spring:
              security:
                oauth2:
                  resourceserver:
                    jwt:
                      jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
            

            演示

            • 在浏览器地址栏输入http://127.0.0.1:8081/hello

              Spring Boot 最新版3.x 集成 OAuth 2.0实现认证授权服务、第三方应用客户端以及资源服务,在这里插入图片描述,第1张

              因为配置了多个client,会让用户选择用哪个client进行OAuth登录

            • 选择第一个client-id-1
            • 跳转到认证授权服务进行登录
            • 输入用户名test,密码test

              Spring Boot 最新版3.x 集成 OAuth 2.0实现认证授权服务、第三方应用客户端以及资源服务,在这里插入图片描述,第2张

            • 登录成功后跳转授权页面

              Spring Boot 最新版3.x 集成 OAuth 2.0实现认证授权服务、第三方应用客户端以及资源服务,在这里插入图片描述,第3张

            • 勾选scope确认授权
            • 重定向请求

              Spring Boot 最新版3.x 集成 OAuth 2.0实现认证授权服务、第三方应用客户端以及资源服务,在这里插入图片描述,第4张

              以上所有页面都是Spring默认的,真实业务开发会自定义这些页面

              OAuth客户端openid演示

              • 在浏览器输入http://127.0.0.1:8080/.well-known/openid-configuration
                {"issuer":"http://127.0.0.1:8080","authorization_endpoint":"http://127.0.0.1:8080/oauth2/authorize","token_endpoint":"http://127.0.0.1:8080/oauth2/token","token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"jwks_uri":"http://127.0.0.1:8080/oauth2/jwks","userinfo_endpoint":"http://127.0.0.1:8080/userinfo","response_types_supported":["code"],"grant_types_supported":["authorization_code","client_credentials","refresh_token"],"revocation_endpoint":"http://127.0.0.1:8080/oauth2/revoke","revocation_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"introspection_endpoint":"http://127.0.0.1:8080/oauth2/introspect","introspection_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid"]}
                
              • 如果配置了issuer-uri,启动的时候会调用${issuer-uri}/.well-known/openid-configuration获取provider配置信息,如果issuer-uri配置了path也会替换成/.well-known/openid-configuration
              • 从/.well-known/openid-configuration这个接口获取到的user-info-uri地址是http://127.0.0.1:8080/userinfo 所以会从授权服务获取用户信息
              • 要想让授权服务的/userinfo接口正常返回,则需要在配置registration时,在scope增加openid,同时scope还需要在profile、email、address、phone中增加至少一个,修改后的yml配置如下
                server:
                  port: 8081
                  servlet:
                    session:
                      cookie:
                        # 需要更换存放sessionId的cookie名字,否则认证服务和客户端的sessionId会相互覆盖
                        name: JSESSIONID-2
                spring:
                  security:
                    oauth2:
                      client:
                        registration:
                          client-id-1:
                            provider: demo-client-id
                            client-id: demo-client-id
                            client-secret: demo-client-secret
                            authorization-grant-type: authorization_code
                            redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
                #            client-authentication-method: POST
                            scope: openid, profile, user_info, pull_requests
                            client-name: demo-client-id
                          client-id-2:
                            provider: demo-client-id2
                            client-id: demo-client-id
                            client-secret: demo-client-secret
                            authorization-grant-type: authorization_code
                            redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
                #            client-authentication-method: POST
                            scope: openid, profile, user_info, pull_requests
                            client-name: demo-client-id2
                        provider:
                          demo-client-id:
                            issuer-uri: http://127.0.0.1:8080
                          demo-client-id2:
                            issuer-uri: http://127.0.0.1:8080
                
                • 授权页面会有变化

                  Spring Boot 最新版3.x 集成 OAuth 2.0实现认证授权服务、第三方应用客户端以及资源服务,在这里插入图片描述,第5张

                  结束

                  本人经过研读SpringBoot3相关源码,基本上把所有功能都体验了一遍,这篇文章主要是针对最新版的SpringBoot集成OAuth功能进行演示,背后的原理,大家有疑问的可以留言交流。