作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
学习必须往深处挖,挖的越深,基础越扎实!
阶段1、深入多线程
阶段2、深入多线程设计模式
阶段3、深入juc源码解析
码哥源码部分
码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】
码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】
码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】
码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】
打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】
今天内容主要是解决一位粉丝提的问题:如何在jwt中添加用户的额外信息并在资源服务器中获取这些数据。
涉及的知识点有以下三个:
- 如何在返回的jwt中添加自定义数据
- 如何在jwt中添加用户的额外数据,比如用户id、手机号码
- 如何在资源服务器中取出这些自定义数据
下面我们分别来看如何实现。
如何在返回的jwt中添加自定义数据
这个问题比较简单,只要按照如下两步即可:
- 编写自定义token增强器
package com.javadaily.auth.security;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* <code>JwtTokenEnhancer</code>
* </p>
* Description:
* 自定义Token增强
* @author mx
* @date 2023/7/4 15:56
*/
public class CustomJwtTokenConverter extends JwtAccessTokenConverter{
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication authentication) {
Object principal = authentication.getUserAuthentication().getPrincipal();
final Map<String,Object> additionalInformation = new HashMap<>(4);
additionalInformation.put("author","java日知录");
additionalInformation.put("weixin","javadaily");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInformation);
return super.enhance(oAuth2AccessToken,authentication);
}
}
- 在认证服务器
AuthorizationServerConfig
中配置自定义token增强器
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer(){
//自定义jwt 输出内容,若不需要就直接使用JwtAccessTokenConverter
JwtAccessTokenConverter converter = new CustomJwtTokenConverter();
// 设置对称签名
converter.setSigningKey("javadaily");
return converter;
}
通过上述两步配置,我们生成的jwt token中就可以带上 author
和 weixin
两个属性了,效果如下:
有的同学可能要问,为什么配置了这个增强器就会生成额外属性了呢?
❞
这是因为我们会使用 DefaultTokenServices#createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken)
方法时有如下一段代码:
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
如果系统配置了 accessTokenEnhancer
就会调用 accessTokenEnhancer
的 enhance()
方法进行token增强。我们是继承了 JwtAccessTokenConverter
,所以会在jwt token的基础上增加额外的信息。
如何在jwt中添加用户的额外数据
要添加额外数据我们还是要从 CustomJwtTokenConverter
想办法,要添加用户的额外数据比如用户id和手机号码那就必须要在用户中包含这些信息。
原来我们自定义的 UserDetailServiceImpl
中返回的是默认的 UserDetails
。里面只包含用户名属性,即username,代码调试效果如下:
所以我们这里需要自定一个 UserDetails
,包含用户的额外属性,然后在 UserDetailServiceImpl
中再返回我们这个自定义对象,最后在 enhance
方法中强转成自定义用户对象并添加额外属性。
实现顺序如下:
- 自定义UserDetails
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* <p>
* <code>CustomUser</code>
* </p>
* Description:
* 自定义用户信息
* @author mx
* @date 2023/11/17 15:05
*/
public class SecurityUser extends User {
@Getter
private Integer id;
@Getter
private String mobile;
public SecurityUser(Integer id, String mobile,
String username, String password,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.id = id;
this.mobile = mobile;
}
}
- 在UserDetailServiceImpl中返回自定义对象
private UserDetails buildUserDetails(SysUser sysUser) {
Set<String> authSet = new HashSet<>();
List<String> roles = sysUser.getRoles();
if(!CollectionUtils.isEmpty(roles)){
roles.forEach(item -> authSet.add(CloudConstant.ROLE_PREFIX + item));
authSet.addAll(sysUser.getPermissions());
}
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(authSet.toArray(new String[0]));
return new SecurityUser(
sysUser.getId(),
sysUser.getMobile(),
sysUser.getUsername(),
sysUser.getPassword(),
authorityList
);
}
- 在ehance方法中获取当前用户并设置用户信息
public class CustomJwtTokenConverter extends JwtAccessTokenConverter{
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication authentication) {
SecurityUser securityUser = (SecurityUser) authentication.getUserAuthentication().getPrincipal();
final Map<String,Object> additionalInformation = new HashMap<>(4);
additionalInformation.put("userId", securityUser.getId());
additionalInformation.put("mobile", securityUser.getMobile());
...
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInformation);
return super.enhance(oAuth2AccessToken,authentication);
}
}
如何在资源服务器中获取这些自定义信息
通过上面的配置我们可以往jwt的token中添加上用户的数据信息,但是在资源服务器中还是获取不到,通过
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
获取到的用户信息还是只包含用户名。
这里还是得从token的转换器入手,默认情况下 JwtAccessTokenConverter
会调用 DefaultUserAuthenticationConverter
中的 extractAuthentication
方法从token中获取用户信息。
我们先看看具体实现逻辑:
public class DefaultUserAuthenticationConverter implements UserAuthenticationConverter {
...
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
...
}
在没有注入 UserDetailService
的情况下oauth2只会获取用户名 user_name
。如果注入了 UserDetailService
就可以返回所有用户信息。
所以这里我们对应的实现方式也有两种:
-
在资源服务器中也注入
UserDetailService
,这种方法不推荐,资源服务器与认证服务器分开的情况下强行耦合在一起,也需要加入用户认证的功能。 -
扩展
DefaultUserAuthenticationConverter
,重写extractAuthentication
方法,手动取出额外数据,然后在资源服务器配置中将其注入到AccessTokenConverter
中。
这里我们采用第二种方法实现,实现顺序如下:
- 自定义token解析器,从jwt token中解析用户信息。
package com.javadaily.common.security.component;
import com.javadaily.common.security.user.SecurityUser;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Map;
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
/**
* 重写抽取用户数据方法
* @author mx
* @date 2023/11/18 10:56
* @param map 用户认证信息
* @return Authentication
*/
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
String username = (String) map.get(USERNAME);
Integer id = (Integer) map.get("userId");
String mobile = (String) map.get("mobile");
SecurityUser user = new SecurityUser(id, mobile, username,"N/A", authorities);
return new UsernamePasswordAuthenticationToken(user, "N/A", authorities);
}
return null;
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
- 编写自定义token转换器,注入自定义解压器
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter{
public CustomAccessTokenConverter(){
super.setUserTokenConverter(new CustomUserAuthenticationConverter());
}
}
- 在资源服务器中配置类ResourceServerConfig中注入自定义token转换器
@Bean
public JwtAccessTokenConverter jwtTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("javadaily");
jwtAccessTokenConverter.setAccessTokenConverter(new CustomAccessTokenConverter());
return jwtAccessTokenConverter;
}
通过上面三步配置我们再调用 SecurityContextHolder.getContext().getAuthentication().getPrincipal()
方法时就可以获取到用户的额外信息了。
当然我们可以再来一个工具类,从上下文中直接获取用户信息:
@UtilityClass
public class SecurityUtils {
/**
* 获取Authentication
*/
public Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
public SecurityUser getUser(){
Authentication authentication = getAuthentication();
if (authentication == null) {
return null;
}
return getUser(authentication);
}
/**
* 获取当前用户
* @param authentication 认证信息
* @return 当前用户
*/
private static SecurityUser getUser(Authentication authentication) {
Object principal = authentication.getPrincipal();
if(principal instanceof SecurityUser){
return (SecurityUser) principal;
}
return null;
}
}