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, # 发布时间
...
}
1.2.3.4.5.6.7.8.9.
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>
1.2.3.4.
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>
1.2.3.4.
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();
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.117.118.119.120.121.122.123.124.125.126.127.128.129.130.131.132.133.134.135.136.137.138.
在 这段代码中我们基于内存模式(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);
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.
在这里我们构建了一个InMemory的dailymart用户,这些代码使用过Spring Security OAuth2的同学来说肯定很熟悉。
通过上面的三步,我们就构建了一个最基础的认证服务器。
授权码模式演示
1、启动认证服务器后(9090)我们访问如下地址获取token
注意,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"]
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.
小结
本篇文章我们先熟悉一下如何基于spring-boot-starter-oauth2-authorization-server构建认证服务器。