SpringBoot3 构建Spring Authorization Server认证服务!

开发 前端
OAuth 2.0(Open Authorization 2.0)是一种授权框架,允许第三方应用程序访问用户在另一个服务提供者上托管的资源,而无需共享用户的凭据(例如用户名和密码)。

在之前的SpringCloud微服务专栏中,我介绍了基于 Spring Security OAuth2 构建的统一认证服务器。随着技术的不断发展,Spring Security OAuth2 已于2022年6月5日宣布停止维护。为了应对这一变化,Spring 官方推出了新产品——Spring Authorization Server。该组件实现了 OAuth 2.1协议 和 OpenID Connect 1.0 规范以及其他相关规范的实现,它构建在 Spring Security 之上,为构建 OpenID Connect 1.0 Identity Provider 和 OAuth2 Authorization Server 产品提供安全、轻量级和可定制的基础。

接下来我将使用 Spring Authorization Server (以下简称SAS) 来更新原专栏的认证服务,今天先让我们来搭建一个简单的认证服务认识一下Spring Authorization Server。

概念理解

Oauth2.0

OAuth 2.0(Open Authorization 2.0)是一种授权框架,允许第三方应用程序访问用户在另一个服务提供者上托管的资源,而无需共享用户的凭据(例如用户名和密码)。

在Oauth2.0中,定义了四种角色:

•资源所有者(Resource Owner),•客户端(Client),•资源服务器(Resource Server),•授权服务器(Authorization Server)

以及四种授权模式:

•授权码授权(Authorization Code Grant),•隐式授权(Implicit Grant),•密码授权(Resource Owner Password Credentials Grant),•客户端凭证授权(Client Credentials Grant)。

关于Oauth2.0的详细概念及认证流程网上已经有大量的文章说明,这里不再赘述。

Oauth2.1

OAuth 2.1 在 OAuth 2.0 的基础上进行了以下改进:

•推荐使用 Authorization Code+PKCE 模式授权

授权码 (Authorization Code) 模式大家都很熟悉了,也是最安全的授权流程, 那 PKCE 又是什么呢? PKCE 全称是 Proof Key for Code Exchange, 在 2015 年发布为 RFC 7636, 我们知道, 授权码模式虽好, 但是它不能给公开的客户端用, 因为公开的客户端没有能力保存好秘钥(client_secret), 所以在此之前, 对于公开的客户端, 只能使用隐式模式和密码模式, PKCE 就是为了解决这个问题而出现的, 另外它也可以防范授权码拦截攻击, 实际上它的原理是客户端提供一个自创建的证明给授权服务器, 授权服务器通过它来验证客户端,把访问令牌(access_token) 颁发给真实的客户端而不是伪造的,以下是其流程图

图片图片

•移除隐式授权模式

•移除密码模式

OpenID Connect(OIDC)

OIDC是OpenID Connect的简称,OIDC=(Identity, Authentication) + OAuth 2.0,它在原Oauth2.0的基础上构建了一个身份层,是一个基于OAuth2协议的身份认证标准协议。我们都知道OAuth2是一个授权协议,它无法提供完善的身份认证功能。OIDC使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息通过一个叫ID Token 的东西传递给客户端,ID Token使用JWT格式来包装,使得ID Token可以安全的传递给第三方客户端程序并且容易被验证。如果ID Token返回的内容不够,授权服务器还提供一个UserInfo接口,可以获取用户更完整的信息。在可以选择 OIDC 的情况下,应该选择 OIDC。

如下是一个ID_Token解析后的例子,包含不限于以下几个字段信息

{
  "sub": "dailymart",                # 用户ID
  "aud": "oidc-client",                # ID Token的受众,即Client_ID            
  "auth_time": 1722780563,          # 完成认证的时间
  "iss": "http://127.0.0.1:9090",   # 发行人,即认证服务器
  "exp": 1722782868,                # 到期时间
  "iat": 1722781068,                # 发布时间
    ...
}

SAS上手体验

SpringBoot集成SAS

1、引入spring-boot-starter-oauth2-authorization-server

在SpringBoot3.1中提供了对SAS的支持,只需要引入依赖即可完成授权服务器的搭建

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

DDD项目当前使用的SpringBoot版本是3.2.7,对应SAS版本为1.2.5。

如果需要尝试其他版本,也可以手动引入,如:

<groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>1.3.1</version>
</dependency>

2、认证服务器配置AuthorizationServerConfig

@Slf4j
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {


    /**
     * Security过滤器链,用于协议端点
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain (HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity (http);
        // 开启OIDC
        http.getConfigurer (OAuth2AuthorizationServerConfigurer.class)
                .oidc (Customizer.withDefaults ());


        http
            .exceptionHandling ((exceptions) -> exceptions
                    .defaultAuthenticationEntryPointFor (
                            new LoginUrlAuthenticationEntryPoint ("/login"),
                            new MediaTypeRequestMatcher (MediaType.TEXT_HTML)
                    )
            )
            //接受用户信息和/或客户端注册的访问令牌
            .oauth2ResourceServer ((resourceServer) -> resourceServer
                    .jwt (Customizer.withDefaults ()));


        return http.build ();
    }


    /**
     * 配置密码解析器,使用BCrypt的方式对密码进行加密和验证
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }






    /**
     * 管理客户端
     * @param passwordEncoder 密码管理器
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("oidc-client")
                .clientSecret(passwordEncoder.encode("123456"))
                //客户端认证基于请求头
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/oidc-client") // 页面地址需要跟这个保持一致
                .postLogoutRedirectUri("http://127.0.0.1:8080/")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope("user.info")
                .scope("all")
                // 客户端设置,设置用户需要确认授权,设置false后不需要确认
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                //设置accessToken有效期
                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(2)).build())
                .build();


        return new InMemoryRegisteredClientRepository(oidcClient);
    }




    /**
     * 用于签署访问令牌
     */
    @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);
    }


    /**
     * 创建RsaKey
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        }
        catch (Exception ex) {
            log.error ("generateRsaKey Exception", ex);
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }




    /**
     * 解码签名访问令牌
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }






    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }




}

在 这段代码中我们基于内存模式(InMemory)构建了一个oidc-client客户端,客户端通过请求头的形式进行认证,并支持授权码、刷新码、客户端三种认证方式,通过tokenSettings将access_token的有效期设置成2小时。

3、Spring Security 安全配置

@EnableWebSecurity 
@Configuration(proxyBeanMethods = false) 
public class DefaultSecurityConfig {


    /**
     * 用于认证的Spring Security过滤器链。
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) ->
                        authorize
                        .requestMatchers("/assets/**","/webjars/**","/actuator/**","/oauth2/**","/login").permitAll()
                        .anyRequest().authenticated()
                )
                .cors(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(Customizer.withDefaults());


        return http.build();
    }




    /**
     * 配置内存用户
     * @param passwordEncoder 密码管理器
     */
    @Bean
    public UserDetailsService users(PasswordEncoder passwordEncoder) {


        UserDetails userDetails = User.withUsername("dailymart")
                .password(passwordEncoder.encode("123456"))
                .roles("USER")
                .build();


        return new InMemoryUserDetailsManager(userDetails);
    }




}

在这里我们构建了一个InMemory的dailymart用户,这些代码使用过Spring Security OAuth2的同学来说肯定很熟悉。

通过上面的三步,我们就构建了一个最基础的认证服务器。

授权码模式演示

1、启动认证服务器后(9090)我们访问如下地址获取token

http://127.0.0.1:9090/oauth2/authorize?client_id=oidc-client&response_type=code&scope=user.info+openid&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/oidc-client

注意,SAS会校验redirect_url与客户端中配置的是否一致,此参数不能乱配置。

2、SpringSecurity检测到用户未登录,跳转至登录页面

图片图片

3、登录以后然后系统会跳转至确认授权页面(ClientSettings.builder().requireAuthorizationConsent(true)),确认授权以后再跳转到redirect_url上,并在参数中返回code

图片

4、通过postman调用oauth2接口获取access_token

图片图片

在第一步的scope参数中我们申请了openid权限,这个时候SAS会启用OIDC协议并返回ID_TOKEN,如果未申请openid则是默认的oauth2协议。

图片图片

此时我们将id_token解开即可获得用户信息。

5、 获取用户详细信息

SAS提供一个userInfo接口用于获取用户的详细信息,通过postman调用并在请求头中设置上一步拿到的access_token

图片图片

6、我们还可以通过浏览器访问http://127.0.0.1:9090/.well-known/openid-configuration以获取认证服务器的详细信息

{
    "issuer": "http://127.0.0.1:9090",
    "authorization_endpoint": "http://127.0.0.1:9090/oauth2/authorize",
    "device_authorization_endpoint": "http://127.0.0.1:9090/oauth2/device_authorization",
    "token_endpoint": "http://127.0.0.1:9090/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:9090/oauth2/jwks",
    "userinfo_endpoint": "http://127.0.0.1:9090/userinfo",
    "end_session_endpoint": "http://127.0.0.1:9090/connect/logout",
    "response_types_supported": ["code"],
    "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
    "revocation_endpoint": "http://127.0.0.1:9090/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:9090/oauth2/introspect",
    "introspection_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt"],
    "code_challenge_methods_supported": ["S256"],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "scopes_supported": ["openid"]
}

小结

本篇文章我们先熟悉一下如何基于spring-boot-starter-oauth2-authorization-server构建认证服务器,后面几篇文章我们将对其进行改造让其符合生产使用。

责任编辑:武晓燕 来源: JAVA日知录
相关推荐

2023-08-07 14:28:07

SpringBoot工具

2021-11-10 05:00:30

服务器Spring授权

2023-08-09 08:29:51

SpringWeb编程

2021-08-18 10:36:43

Sping社区实验项目服务器

2023-08-11 08:59:49

分库分表数据数据库

2024-10-08 08:26:43

2021-11-11 07:38:15

服务器过滤器框架

2024-07-31 14:03:00

Spring定时任务管理

2023-08-08 08:23:08

Spring日志​线程池

2023-06-19 08:05:17

RFCwebSpring

2024-03-04 08:19:11

SpringURLHeader

2024-05-10 08:10:05

Spring虚拟线程JDK

2023-02-01 10:40:01

2017-06-26 09:06:10

Spring Clou微服务架构

2017-09-04 16:15:44

服务网关架构

2022-10-10 08:00:00

微服务Spring Boo容器

2014-11-18 11:20:51

webserviceCXFSpring

2022-09-22 10:01:47

微服务授权认证

2017-07-03 09:50:07

Spring Clou微服务架构

2024-10-11 11:37:38

点赞
收藏

51CTO技术栈公众号