SpringBoot3 构建Spring Authorization Server认证服务!

科技   2024-09-11 08:48   北京  

在之前的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构建认证服务器,后面几篇文章我们将对其进行改造让其符合生产使用。

- End-

DailyMart是一个基于 DDD 和Spring Cloud Alibaba的微服务商城系统,采用SpringBoot3.x以及JDK17。旨在为开发者提供集成式的学习体验,并将其无缝地应用于实际项目中。该专栏包含领域驱动设计(DDD)、Spring Cloud Alibaba企业级开发实践、设计模式实际应用场景解析、分库分表战术及实用技巧等内容。如果你对这个系列感兴趣,可在本公众号回复关键词 DDD 获取完整文档以及相关源码。


JAVA日知录
写代码的架构师,做架构的程序员! 实战、源码、数据库、架构...只要你来,你想了解的这里都有!
 最新文章