Spring Authorization Server (七)自定义Token
标签: Spring Authorization Server (七)自定义Token 架构博客 51CTO博客
2023-07-28 18:24:29 212浏览
在上一篇中,我们介绍了自定义的密码模式,在使用密码模式获取token信息的时候,我们在PasswordGrantAuthenticationProvider类的authenticate方法中,只生成了access_token和refresh_token,并没有生成id_token,本篇我们就来生成id_token,并实现自定义的id_token和access_token。
授权码id_token生成演示
在上一篇中,我们从OAuth2AuthorizationCodeAuthenticationProvider类中拷贝生成refresh_token的代码时,也看到了该类中也有生成id_token的代码,但是如果将该代码拷贝过来直接生成id_token,那么将会导致使用refresh_token去获取token的时候报错。下面将OAuth2AuthorizationCodeAuthenticationProvider生成id_token的代码拷贝进PasswordGrantAuthenticationProvider演示一下。
PasswordGrantAuthenticationProvider类拷贝生成id_token的代码后整合如下。
package org.oauth.server.authentication.password;
import jakarta.annotation.Resource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.security.Principal;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Rommel
* @version 1.0
* @date 2023/7/20-14:41
* @description TODO
*/
public class PasswordGrantAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
@Resource
private UserDetailsService userDetailsService;
@Resource
private PasswordEncoder passwordEncoder;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE =
new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private final Log logger = LogFactory.getLog(getClass());
public PasswordGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PasswordGrantAuthenticationToken passwordGrantAuthenticationToken =
(PasswordGrantAuthenticationToken) authentication;
Map<String, Object> additionalParameters = passwordGrantAuthenticationToken.getAdditionalParameters();
//授权类型
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
//用户名
String username = (String)additionalParameters.get(OAuth2ParameterNames.USERNAME);
//密码
String password = (String)additionalParameters.get(OAuth2ParameterNames.PASSWORD);
//请求参数权限范围
String requestScopesStr = (String)additionalParameters.get(OAuth2ParameterNames.SCOPE);
//请求参数权限范围专场集合
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
// Ensure the client is authenticated
OAuth2ClientAuthenticationToken clientPrincipal =
getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// Ensure the client is configured to use this authorization grant type
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
//校验用户名信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(!passwordEncoder.matches(password,userDetails.getPassword())){
throw new OAuth2AuthenticationException("密码不正确!");
}
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(userDetails,clientPrincipal,userDetails.getAuthorities());
// Initialize the DefaultOAuth2TokenContext
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthenticationToken)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(authorizationGrantType)
.authorizedScopes(requestScopeSet)
.authorizationGrant(passwordGrantAuthenticationToken);
// Initialize the OAuth2Authorization
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizedScopes(requestScopeSet)
.attribute(Principal.class.getName(), usernamePasswordAuthenticationToken)
.authorizationGrantType(authorizationGrantType);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Generated access token");
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Generated refresh token");
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
//获取客户端权限范围和请求参数权限范围的交集
Set<String> scopes = this.getInterseSet(registeredClient.getScopes(),requestScopeSet);
// ----- ID token -----
OidcIdToken idToken;
if (scopes.contains(OidcScopes.OPENID)) {
// @formatter:off
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token
.build();
// @formatter:on
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the ID token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Generated id token");
}
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
authorizationBuilder.token(idToken, (metadata) ->{
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims());
}
);
} else {
idToken = null;
}
//保存认证信息
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved authorization");
}
if (idToken != null) {
additionalParameters = new HashMap<>();
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated token request");
}
return new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
@Override
public boolean supports(Class<?> authentication) {
return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication);
}
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
/**
* @param set1
* @param set2
* @return
* @author Rommel
* @date 2023/7/21-3:11
* @version 1.0
* @description 取两个集合的交集
*/
private Set<String> getInterseSet(Set<String> set1,Set<String> set2){
if(CollectionUtils.isEmpty(set1)|| CollectionUtils.isEmpty(set2)){
return Set.of();
}
Set<String> set = set1.stream().filter(set2::contains).collect(Collectors.toSet());
if(CollectionUtils.isEmpty(set)){
set = Set.of();
}
return set;
}
}
在oauth2_registered_client表中,给客户端id为password-client-id的scopes增加openid权限。
启动认证服务器,postman请求http://spring-oauth-server:9000/oauth2/token地址,参数如下。
返回结果如下。
我们看到,上面结果中有返回了id_token。
此时,使用获取到的refresh_token再去获取token,则出现下面的情况。
控制台,报错信息如下。
将id_token使用jwt解析如下。
使用授权码模式去获取token如下。
将id_token使用jwt解析如下。
通过对比密码模式和授权码模式的id_token解析,可以发现,密码模式的id_token比授权码模式的id_token少了两个字段,分别是:auth_time(认证时间)、sid(sessionId)。至于为什么少了这两个字段,也比较好理解,因为我们使用自定义密码模式获取token时,是没有浏览器交互的。由于少了这两个字段,当使用refresh_token去获取token时,会去取这两个字段的值,进入下面的代码,因此就报了“value cannot be null”的异常。
为了解决“value cannot be null”异常,我们需要在生成id_token的时候,补上auth_time、sid这两个字段。
自定义id_token
参照官网文档,我们可以对access_token和id_token添加自定义的字段。
首先,构造OAuth2TokenCustomizer<JwtEncodingContext> @Bean,然后将它设置到token生成器中。我们在上一篇生成access_token和refresh_token的时候,已在AuthorizationServerConfig配置类中经构造了一个token生成器。
/**
*配置token生成器
*/
@Bean
OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
现在只需添加OAuth2TokenCustomizer<JwtEncodingContext> @Bean,然后将它设置到token生成器中,添加后,AuthorizationServerConfig配置类代码如下。
package org.oauth.server.config;
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.apache.catalina.util.StandardSessionIdGenerator;
import org.oauth.server.authentication.device.DeviceClientAuthenticationConverter;
import org.oauth.server.authentication.device.DeviceClientAuthenticationProvider;
import org.oauth.server.authentication.mobile.MobileGrantAuthenticationConverter;
import org.oauth.server.authentication.mobile.MobileGrantAuthenticationProvider;
import org.oauth.server.authentication.password.PasswordGrantAuthenticationConverter;
import org.oauth.server.authentication.password.PasswordGrantAuthenticationProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.token.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Date;
import java.util.UUID;
/**
* @author Rommel
* @version 1.0
* @date 2023/7/10-16:34
* @description TODO
*/
@Configuration
public class AuthorizationServerConfig {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
/**
* Spring Authorization Server 相关配置
* 此处方法与下面defaultSecurityFilterChain都是SecurityFilterChain配置,配置的内容有点区别,
* 因为Spring Authorization Server是建立在Spring Security 基础上的,defaultSecurityFilterChain方法主要
* 配置Spring Security相关的东西,而此处authorizationServerSecurityFilterChain方法主要配置OAuth 2.1和OpenID Connect 1.0相关的东西
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,RegisteredClientRepository registeredClientRepository,
AuthorizationServerSettings authorizationServerSettings,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<?> tokenGenerator)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
//AuthenticationConverter(预处理器),尝试从HttpServletRequest提取客户端凭据,用以构建OAuth2ClientAuthenticationToken实例。
DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
new DeviceClientAuthenticationConverter(
authorizationServerSettings.getDeviceAuthorizationEndpoint());
//AuthenticationProvider(主处理器),用于验证OAuth2ClientAuthenticationToken。
DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
new DeviceClientAuthenticationProvider(registeredClientRepository);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint ->
//设置用户码校验地址
deviceAuthorizationEndpoint.verificationUri("/activate")
)
.deviceVerificationEndpoint(deviceVerificationEndpoint ->
//设置授权页地址
deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)
)
.clientAuthentication(clientAuthentication ->
//设置AuthenticationConverter(预处理器)和AuthenticationProvider(主处理器)
clientAuthentication
.authenticationConverter(deviceClientAuthenticationConverter)
.authenticationProvider(deviceClientAuthenticationProvider)
)
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
//设置自定义密码模式
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(
new PasswordGrantAuthenticationConverter())
.authenticationProvider(
new PasswordGrantAuthenticationProvider(
authorizationService, tokenGenerator)))
//设置自定义手机验证码模式
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(
new MobileGrantAuthenticationConverter())
.authenticationProvider(
new MobileGrantAuthenticationProvider(
authorizationService, tokenGenerator)))
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。
.oidc(Customizer.withDefaults());
//设置登录地址,需要进行认证的请求被重定向到该地址
http
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* 客户端信息
* 对应表:oauth2_registered_client
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
/**
* 授权信息
* 对应表:oauth2_authorization
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 授权确认
*对应表:oauth2_authorization_consent
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
*配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
*/
@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);
}
/**
*生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
*/
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;
}
/**
* 配置jwt解析器
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
*配置认证服务器请求地址
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
//什么都不配置,则使用默认地址
return AuthorizationServerSettings.builder().build();
}
/**
*配置token生成器
*/
@Bean
OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
jwtGenerator.setJwtCustomizer(jwtCustomizer());
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JwsHeader.Builder headers = context.getJwsHeader();
JwtClaimsSet.Builder claims = context.getClaims();
if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
// Customize headers/claims for access_token
} else if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
// Customize headers/claims for id_token
claims.claim(IdTokenClaimNames.AUTH_TIME, Date.from(Instant.now()));
StandardSessionIdGenerator standardSessionIdGenerator = new StandardSessionIdGenerator();
claims.claim("sid", standardSessionIdGenerator.generateSessionId());
}
};
}
}
测试id_token
启动认证服务器,postman请求http://spring-oauth-server:9000/oauth2/token地址,结果如下。
使用jwt解析id_token,可以看到已经包含我们自己添加的auth_time、sid这两个字段了。
再将获取到的refresh_token去获取token,结果如下。
此时可以看到,已经正常返回token信息了。
我们也可以将sys_user表中的一些字段加入到id_token,代码如下。
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JwsHeader.Builder headers = context.getJwsHeader();
JwtClaimsSet.Builder claims = context.getClaims();
SysUserEntity sysUserEntity = sysUserService.selectByUsername(context.getPrincipal().getName());
if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
// Customize headers/claims for access_token
} else if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
// Customize headers/claims for id_token
claims.claim(IdTokenClaimNames.AUTH_TIME, Date.from(Instant.now()));
StandardSessionIdGenerator standardSessionIdGenerator = new StandardSessionIdGenerator();
claims.claim("sid", standardSessionIdGenerator.generateSessionId());
claims.claim("username", sysUserEntity.getUsername());
claims.claim("name", sysUserEntity.getName());
claims.claim("description", sysUserEntity.getDescription());
}
};
重新获取的id_token解析如下。
自定义access_token
目前我们的access_token是只包含客户端权限范围的,并没有包含用户的权限范围。用户名为user获取到的access_token解析如下。
我们知道,用户user的权限为“USER”。
我们可以通过上面添加的OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer()方法来自定义access_token中的内容。AuthorizationServerConfig配置类改造 jwtCustomizer()如下。
package org.oauth.server.config;
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 jakarta.annotation.Resource;
import org.apache.catalina.util.StandardSessionIdGenerator;
import org.oauth.server.authentication.device.DeviceClientAuthenticationConverter;
import org.oauth.server.authentication.device.DeviceClientAuthenticationProvider;
import org.oauth.server.authentication.mobile.MobileGrantAuthenticationConverter;
import org.oauth.server.authentication.mobile.MobileGrantAuthenticationProvider;
import org.oauth.server.authentication.password.PasswordGrantAuthenticationConverter;
import org.oauth.server.authentication.password.PasswordGrantAuthenticationProvider;
import org.oauth.server.model.SysUserEntity;
import org.oauth.server.service.SysUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.token.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Rommel
* @version 1.0
* @date 2023/7/10-16:34
* @description TODO
*/
@Configuration
public class AuthorizationServerConfig {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
@Resource
private SysUserService sysUserService;
@Resource UserDetailsService userDetailsService;
/**
* Spring Authorization Server 相关配置
* 此处方法与下面defaultSecurityFilterChain都是SecurityFilterChain配置,配置的内容有点区别,
* 因为Spring Authorization Server是建立在Spring Security 基础上的,defaultSecurityFilterChain方法主要
* 配置Spring Security相关的东西,而此处authorizationServerSecurityFilterChain方法主要配置OAuth 2.1和OpenID Connect 1.0相关的东西
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,RegisteredClientRepository registeredClientRepository,
AuthorizationServerSettings authorizationServerSettings,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<?> tokenGenerator)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
//AuthenticationConverter(预处理器),尝试从HttpServletRequest提取客户端凭据,用以构建OAuth2ClientAuthenticationToken实例。
DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
new DeviceClientAuthenticationConverter(
authorizationServerSettings.getDeviceAuthorizationEndpoint());
//AuthenticationProvider(主处理器),用于验证OAuth2ClientAuthenticationToken。
DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
new DeviceClientAuthenticationProvider(registeredClientRepository);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint ->
//设置用户码校验地址
deviceAuthorizationEndpoint.verificationUri("/activate")
)
.deviceVerificationEndpoint(deviceVerificationEndpoint ->
//设置授权页地址
deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)
)
.clientAuthentication(clientAuthentication ->
//设置AuthenticationConverter(预处理器)和AuthenticationProvider(主处理器)
clientAuthentication
.authenticationConverter(deviceClientAuthenticationConverter)
.authenticationProvider(deviceClientAuthenticationProvider)
)
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
//设置自定义密码模式
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(
new PasswordGrantAuthenticationConverter())
.authenticationProvider(
new PasswordGrantAuthenticationProvider(
authorizationService, tokenGenerator)))
//设置自定义手机验证码模式
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenRequestConverter(
new MobileGrantAuthenticationConverter())
.authenticationProvider(
new MobileGrantAuthenticationProvider(
authorizationService, tokenGenerator)))
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。
.oidc(Customizer.withDefaults());
//设置登录地址,需要进行认证的请求被重定向到该地址
http
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* 客户端信息
* 对应表:oauth2_registered_client
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
/**
* 授权信息
* 对应表:oauth2_authorization
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 授权确认
*对应表:oauth2_authorization_consent
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
*配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
*/
@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);
}
/**
*生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
*/
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;
}
/**
* 配置jwt解析器
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
*配置认证服务器请求地址
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
//什么都不配置,则使用默认地址
return AuthorizationServerSettings.builder().build();
}
/**
*配置token生成器
*/
@Bean
OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
jwtGenerator.setJwtCustomizer(jwtCustomizer());
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JwsHeader.Builder headers = context.getJwsHeader();
JwtClaimsSet.Builder claims = context.getClaims();
SysUserEntity sysUserEntity = sysUserService.selectByUsername(context.getPrincipal().getName());
if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
// Customize headers/claims for access_token
claims.claims(claimsConsumer->{
UserDetails userDetails = userDetailsService.loadUserByUsername(context.getPrincipal().getName());
claimsConsumer.merge("scope",userDetails.getAuthorities(),(scope,authorities)->{
Set<String> scopeSet = (Set<String>)scope;
Collection<SimpleGrantedAuthority> simpleGrantedAuthorities = ( Collection<SimpleGrantedAuthority>)authorities;
simpleGrantedAuthorities.stream().forEach(simpleGrantedAuthority -> {
if(!scopeSet.contains(simpleGrantedAuthority.getAuthority())){
scopeSet.add(simpleGrantedAuthority.getAuthority());
}
});
return scopeSet;
});
});
} else if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
// Customize headers/claims for id_token
claims.claim(IdTokenClaimNames.AUTH_TIME, Date.from(Instant.now()));
StandardSessionIdGenerator standardSessionIdGenerator = new StandardSessionIdGenerator();
claims.claim("sid", standardSessionIdGenerator.generateSessionId());
claims.claim("username", sysUserEntity.getUsername());
claims.claim("name", sysUserEntity.getName());
claims.claim("description", sysUserEntity.getDescription());
}
};
}
}
测试access_token
启动认证服务器,postman请求http://spring-oauth-server:9000/oauth2/token地址,结果如下。
access_token解析如下。
可以看到,“USER”权限已经被加入到scope中了。
总结
本篇先是演示了将授权码模式生成id_token的代码在自定义密码模式中直接生成id_token的后果,接着解释了使用自定义密码模式时,默认生成的id_token为何缺少了auth_time、sid这两个字段的原因,最后介绍了如何自定义id_token和自定义access_token。
本篇代码在spring-oauth-token-server目录下:链接地址
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论