在上一篇中,我们介绍了如何自定义 token,这一篇我们来介绍一下如何自定义 oidc。oidc 是OpenID Connect 的缩写,是 OAuth 2.0 协议之上的一个简单的身份层。我们在之前的篇章中,经常会看到用户信息被写入 id_token 返回给客户端,如果不想一下子往 id_token 写入太多用户信息或者客户端根本就没有 id_token 权限,那么此时,客户端想要获取更多的用户信息,则只能通过“/userinfo”接口来获取了。但是,通过默认的“/userinfo”接口,也往往获取不到我们想要的用户信息,这时候就需要我们通过自定义 oidc 来实现 userInfo了。
1. 默认 userInfo 演示
我们通过下面的请求获取 token 信息。
将获取到的 id_token 进行 jwt 解析如下。
因为在下面的代码中,我们已经将 sys_user 表中的 username、name、description 字段写入到id_token 中,因此这几个字段也被一起返回给客户端。
此时,我们请求 http://spring-oauth-server:9000/userinfo 地址,带上刚刚获取的 access_token,则返回如下结果。
我们看到,只返回 sub 和 name 两个字段,而 username、description 这些字段并未返回。
那么,“/userinfo”为什么只返回 sub 和 name 这两个字段字段呢?因为我们获取 token 时, scope 传入的范围是 profile openid,其中 openid 要求认证服务器要返回 id_token,而 profile 是要求认证服务器要返回 profile 权限对应的字段。profile 权限默认的字段有 "name", "family_name", "given_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "gender", "birthdate", "zoneinfo", "locale", "updated_at",再加上处理过程中默认加上“sub”字段,这些字段默认都从 id_token 中的 claims 取值,那么“/userinfo”从 id_token 中能取到有值的,也就 sub 和 name 了。
在 Spring Authorization Server 中,scope 定义的权限范围有 openid、profile、email、address、phone。其权限对照关系如下。
权限名称 | 字段范围 |
openid | id_token、sub |
profile | name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at |
email、email_verified | |
address | address,是一个 json对象、包含 formatted、street_address、locality、region、postal_code、country等信息。此处慎用json类型,容易出现json解析异常。 |
phone | phone_number、phone_number_verified |
2. 自定义 OIDC
我们之前在 AuthorizationServerConfig 类中开启 oidc 的时候,直接使用 Customizer.withDefaults() 启动默认配置,通过官网介绍,我们可以通过设置自定义的 userInfoRequestConverter() 和 authenticationProvider() 来实现自定义的 userInfo。
实现步骤如下。
首先在 MyOidcUserInfoAuthenticationConverter 中需要提供 OidcUserInfo 对象,而 OidcUserInfo 中的构造器提供的都是默认的字段,并无我们 sys_user 表中的 username、name、descriptio n字段,因此我们需要创建 OidcUserInfo 的子类 MyOidcUserInfo 来实现自己的构造器, MyOidcUserInfo 代码如下。
package org.oauth.server.authentication.oidc;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* @author Rommel
* @version 1.0
* @date 2023/7/28-10:20
* @description TODO
*/
public class MyOidcUserInfo extends OidcUserInfo {
private static final long serialVersionUID = 610L;
private final Map<String, Object> claims;
public MyOidcUserInfo(Map<String, Object> claims) {
super(claims);
Assert.notEmpty(claims, "claims cannot be empty");
this.claims = Collections.unmodifiableMap(new LinkedHashMap(claims));
}
public Map<String, Object> getClaims() {
return this.claims;
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj != null && this.getClass() == obj.getClass()) {
MyOidcUserInfo that = (MyOidcUserInfo)obj;
return this.getClaims().equals(that.getClaims());
} else {
return false;
}
}
public int hashCode() {
return this.getClaims().hashCode();
}
public static MyOidcUserInfo.Builder myBuilder() {
return new MyOidcUserInfo.Builder();
}
public static final class Builder {
private final Map<String, Object> claims = new LinkedHashMap();
private Builder() {
}
public MyOidcUserInfo.Builder claim(String name, Object value) {
this.claims.put(name, value);
return this;
}
public MyOidcUserInfo.Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
claimsConsumer.accept(this.claims);
return this;
}
public MyOidcUserInfo.Builder username(String username) {
return this.claim("username", username);
}
public MyOidcUserInfo.Builder name(String name) {
return this.claim("name", name);
}
public MyOidcUserInfo.Builder description(String description) {
return this.claim("description", description);
}
public MyOidcUserInfo.Builder status(Integer status) {
return this.claim("status", status);
}
public MyOidcUserInfo.Builder phoneNumber(String phoneNumber) {
return this.claim("phone_number", phoneNumber);
}
public MyOidcUserInfo.Builder email(String email) {
return this.claim("email", email);
}
public MyOidcUserInfo.Builder profile(String profile) {
return this.claim("profile", profile);
}
public MyOidcUserInfo.Builder address(String address) {
return this.claim("address", address);
}
public MyOidcUserInfo build() {
return new MyOidcUserInfo(this.claims);
}
}
}
MyOidcUserInfoAuthenticationConverter 代码如下。
package org.oauth.server.authentication.oidc;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.oauth.server.authentication.password.PasswordGrantAuthenticationToken;
import org.oauth.server.constant.OAuth2Constant;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* @author Rommel
* @version 1.0
* @date 2023/7/20-14:37
* @description TODO
*/
public class MyOidcUserInfoAuthenticationConverter implements AuthenticationConverter {
private MyOidcUserInfoService myOidcUserInfoService;
public MyOidcUserInfoAuthenticationConverter(MyOidcUserInfoService myOidcUserInfoService){
this.myOidcUserInfoService = myOidcUserInfoService;
}
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//查询用户信息
MyOidcUserInfo myOidcUserInfo = myOidcUserInfoService.loadUser(authentication.getName());
//返回自定义的OidcUserInfoAuthenticationToken
return new OidcUserInfoAuthenticationToken(authentication, myOidcUserInfo);
}
}
MyOidcUserInfoService 代码如下。
package org.oauth.server.authentication.oidc;
/**
* @author Rommel
* @version 1.0
* @date 2023/7/25-16:50
* @description TODO
*/
import jakarta.annotation.Resource;
import org.oauth.server.authentication.oidc.MyOidcUserInfo;
import org.oauth.server.model.SysUserEntity;
import org.oauth.server.service.SysUserService;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* Example service to perform lookup of user info for customizing an {@code id_token}.
*/
@Service
public class MyOidcUserInfoService {
@Resource
private SysUserService sysUserService;
public MyOidcUserInfo loadUser(String username) {
SysUserEntity sysUserEntity = sysUserService.selectByUsername(username);
return new MyOidcUserInfo(this.createUser(sysUserEntity));
}
private Map<String, Object> createUser(SysUserEntity sysUserEntity) {
return MyOidcUserInfo.myBuilder()
.name(sysUserEntity.getName())
.username(sysUserEntity.getUsername())
.description(sysUserEntity.getDescription())
.status(sysUserEntity.getStatus())
.phoneNumber(sysUserEntity.getUsername())
.email(sysUserEntity.getUsername() + "@example.com")
.profile("https://example.com/" + sysUserEntity.getName())
.address("XXX共和国XX省XX市XX区XXX街XXX号")
.build()
.getClaims();
}
}
MyOidcUserInfoAuthenticationProvider 代码如下(参考框架中的 OidcUserInfoAuthenticationProvider)。
package org.oauth.server.authentication.oidc;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
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.oidc.authentication.OidcUserInfoAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.util.Assert;
import java.util.*;
import java.util.function.Function;
/**
* @author Rommel
* @version 1.0
* @date 2023/7/28-18:35
* @description TODO
*/
public class MyOidcUserInfoAuthenticationProvider implements AuthenticationProvider {
private final Log logger = LogFactory.getLog(this.getClass());
private final OAuth2AuthorizationService authorizationService;
private Function<OidcUserInfoAuthenticationContext, MyOidcUserInfo> userInfoMapper = new MyOidcUserInfoAuthenticationProvider.DefaultOidcUserInfoMapper();
public MyOidcUserInfoAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OidcUserInfoAuthenticationToken userInfoAuthentication = (OidcUserInfoAuthenticationToken)authentication;
AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(userInfoAuthentication.getPrincipal().getClass())) {
accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken)userInfoAuthentication.getPrincipal();
}
if (accessTokenAuthentication != null && accessTokenAuthentication.isAuthenticated()) {
String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
OAuth2Authorization authorization = this.authorizationService.findByToken(accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
if (authorization == null) {
throw new OAuth2AuthenticationException("invalid_token");
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with access token");
}
OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
if (!authorizedAccessToken.isActive()) {
throw new OAuth2AuthenticationException("invalid_token");
} else {
//从认证结果中获取userInfo
MyOidcUserInfo myOidcUserInfo = (MyOidcUserInfo)userInfoAuthentication.getUserInfo();
//从authorizedAccessToken中获取授权范围
Set<String> scopeSet = (HashSet<String>)authorizedAccessToken.getClaims().get("scope") ;
//获取授权范围对应userInfo的字段信息
Map<String, Object> claims = DefaultOidcUserInfoMapper.getClaimsRequestedByScope(myOidcUserInfo.getClaims(),scopeSet);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated user info request");
}
//构造新的OidcUserInfoAuthenticationToken
return new OidcUserInfoAuthenticationToken(accessTokenAuthentication, new MyOidcUserInfo(claims));
}
}
} else {
throw new OAuth2AuthenticationException("invalid_token");
}
}
public boolean supports(Class<?> authentication) {
return OidcUserInfoAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setUserInfoMapper(Function<OidcUserInfoAuthenticationContext, MyOidcUserInfo> userInfoMapper) {
Assert.notNull(userInfoMapper, "userInfoMapper cannot be null");
this.userInfoMapper = userInfoMapper;
}
private static final class DefaultOidcUserInfoMapper implements Function<OidcUserInfoAuthenticationContext, MyOidcUserInfo> {
private static final List<String> EMAIL_CLAIMS = Arrays.asList("email", "email_verified");
private static final List<String> PHONE_CLAIMS = Arrays.asList("phone_number", "phone_number_verified");
private static final List<String> PROFILE_CLAIMS = Arrays.asList("name", "username", "description", "status", "profile");
private DefaultOidcUserInfoMapper() {
}
public MyOidcUserInfo apply(OidcUserInfoAuthenticationContext authenticationContext) {
OAuth2Authorization authorization = authenticationContext.getAuthorization();
OidcIdToken idToken = (OidcIdToken)authorization.getToken(OidcIdToken.class).getToken();
OAuth2AccessToken accessToken = authenticationContext.getAccessToken();
Map<String, Object> scopeRequestedClaims = getClaimsRequestedByScope(idToken.getClaims(), accessToken.getScopes());
return new MyOidcUserInfo(scopeRequestedClaims);
}
private static Map<String, Object> getClaimsRequestedByScope(Map<String, Object> claims, Set<String> requestedScopes) {
Set<String> scopeRequestedClaimNames = new HashSet(32);
scopeRequestedClaimNames.add("sub");
if (requestedScopes.contains("address")) {
scopeRequestedClaimNames.add("address");
}
if (requestedScopes.contains("email")) {
scopeRequestedClaimNames.addAll(EMAIL_CLAIMS);
}
if (requestedScopes.contains("phone")) {
scopeRequestedClaimNames.addAll(PHONE_CLAIMS);
}
if (requestedScopes.contains("profile")) {
scopeRequestedClaimNames.addAll(PROFILE_CLAIMS);
}
Map<String, Object> requestedClaims = new HashMap(claims);
requestedClaims.keySet().removeIf((claimName) -> {
return !scopeRequestedClaimNames.contains(claimName);
});
return requestedClaims;
}
}
}
将 MyOidcUserInfoAuthenticationConverter、MyOidcUserInfoAuthenticationProvider 设置到AuthorizationServerConfig中,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 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.oidc.MyOidcUserInfoAuthenticationConverter;
import org.oauth.server.authentication.oidc.MyOidcUserInfoAuthenticationProvider;
import org.oauth.server.authentication.oidc.MyOidcUserInfoService;
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.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.Collection;
import java.util.Date;
import java.util.Set;
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";
@Resource
private UserDetailsService userDetailsService;
@Resource
private MyOidcUserInfoService myOidcUserInfoService;
/**
* 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());
//自定义oidc
.oidc(oidcCustomizer->{
oidcCustomizer.userInfoEndpoint(userInfoEndpointCustomizer->{
userInfoEndpointCustomizer.userInfoRequestConverter(new MyOidcUserInfoAuthenticationConverter(myOidcUserInfoService));
userInfoEndpointCustomizer.authenticationProvider(new MyOidcUserInfoAuthenticationProvider(authorizationService));
});
});
//设置登录地址,需要进行认证的请求被重定向到该地址
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(myOidcUserInfoService));
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer(MyOidcUserInfoService myOidcUserInfoService) {
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
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());
}
};
}
}
3. 自定义 OIDC 测试
oauth2_registered_client 表增加一条手机号码的记录如下。
INSERT INTO `oauth-server`.`oauth2_registered_client` (`id`, `client_id`, `client_id_issued_at`, `client_secret`, `client_secret_expires_at`, `client_name`, `client_authentication_methods`, `authorization_grant_types`, `redirect_uris`, `post_logout_redirect_uris`, `scopes`, `client_settings`, `token_settings`) VALUES ('d84e9e7c-abb1-46f7-bb0f-4511af362ca8', 'custom-oidc-id', '2023-07-12 07:33:42', '$2a$10$.J0Rfg7y2Mu8AN8Dk2vL.eBFa9NGbOYCPOAFEw.QhgGLVXjO7eFDC', NULL, '自定义oidc测试', 'client_secret_basic', 'refresh_token,authorization_password,authorization_mobile', '', 'http://127.0.0.1:9000/', 'openid,profile,email,address,phone', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.client.require-proof-key\":false,\"settings.client.require-authorization-consent\":true}', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.token.reuse-refresh-tokens\":true,\"settings.token.id-token-signature-algorithm\":[\"org.springframework.security.oauth2.jose.jws.SignatureAlgorithm\",\"RS256\"],\"settings.token.access-token-time-to-live\":[\"java.time.Duration\",3600.000000000],\"settings.token.access-token-format\":{\"@class\":\"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat\",\"value\":\"self-contained\"},\"settings.token.refresh-token-time-to-live\":[\"java.time.Duration\",7200.000000000],\"settings.token.authorization-code-time-to-live\":[\"java.time.Duration\",3600.000000000],\"settings.token.device-code-time-to-live\":[\"java.time.Duration\",3600.000000000]}');
3.1. 测试所有权限
启动认证服务器,postman 请求 http://spring-oauth-server:9000/oauth2/token 地址,参数如下。
返回结果如下。
将 id_token 进行 jwt 解析如下。
此时 id_token 并未返回过多用户信息。
postman 请求 http://spring-oauth-server:9000/userinfo 地址,带上刚刚获取的 access_token,则返回如下结果。
从上面结果中可以看到,userinfo 已经返回我们自定义的 username、description、status 字段了。
3.2. 测试 profile 权限
postman 请求 http://spring-oauth-server:9000/oauth2/token 地址,scope 只传 profile 值,则结果如下。
可以看到,id_token 已经不返回了,因为当前 scope 只有 profile。
postman 请求 http://spring-oauth-server:9000/userinfo 地址,带上刚刚获取的 access_token,则返回如下结果。
可以看到,address、email、phone_number 等字段已经不返回了,而返回的字段正是我们在 DefaultOidcUserInfoMapper 定义的 PROFILE_CLAIMS 值。
3.3. 测试 address 权限
postman 请求 http://spring-oauth-server:9000/oauth2/token 地址,scope 只传 address 值,则结果如下。
postman 请求 http://spring-oauth-server:9000/userinfo 地址,带上刚刚获取的 access_token,则返回如下结果。
可以看到,只返回了 address 字段。
3.4. 测试 email 权限
postman 请求 http://spring-oauth-server:9000/oauth2/token 地址,scope 只传 email 值,则结果如下。
postman 请求 http://spring-oauth-server:9000/userinfo 地址,带上刚刚获取的 access_token,则返回如下结果。
可以看到,只返回了 email 字段。
3.5. 测试 phone 权限
postman 请求 http://spring-oauth-server:9000/oauth2/token 地址,scope 只传 phone 值,则结果如下。
postman 请求 http://spring-oauth-server:9000/userinfo 地址,带上刚刚获取的 access_token,则返回如下结果。
可以看到,只返回了 phone_number 字段。
3.6. 测试组合权限
postman 请求 http://spring-oauth-server:9000/oauth2/token 地址,scope 传 address email phone 值,则结果如下。
postman 请求 http://spring-oauth-server:9000/userinfo 地址,带上刚刚获取的 access_token,则返回如下结果。
可以看到,返回 address、email、phone 权限组合字段。
4. 总结
本篇先是演示了默认 userinfo 接口的返回结果,并介绍了默认 userinfo 返回哪些字段信息,然后讲解并实现如何自定义 oidc,最后对自定义 oidc 的各种权限场景进行了测试演示。
本篇代码在 spring-oauth-oidc-server 目录下:链接地址