在之前的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 获取完整文档以及相关源码。