目录标题
-
-
- 一、前置类和配置等
- [二、JwtAuthenticationTokenFilter token过滤器(之前判断逻辑有问题,要先判断用户是否存在或者版本是否一致,再判断SecurityContext 中没有认证信息)](#二、JwtAuthenticationTokenFilter token过滤器(之前判断逻辑有问题,要先判断用户是否存在或者版本是否一致,再判断SecurityContext 中没有认证信息))
- 三、TokenService
- [四、spring security配置](#四、spring security配置)
-
参考之前blog: 多版本共用redis导致数据没及时更新报错
里面有bug,就是用户token即使失效或者不存在也是重新获取用户信息重新写入redis
导致一直用户可以访问且是登录状态
前置知识:
springSecurity 的config 配置里面 anonymous 和 permitAll 区别
permitAll() 是"无条件敞开大门",而 anonymous() 是"为未登录者发一张临时门卡"
| 特性 | .permitAll() | .anonymous() |
|---|---|---|
| 官方定义 | 允许所有人访问,无论是否登录 | 允许匿名用户访问,即未认证的用户(认证后的就不能访问) |
| 身份标识 | 不关心用户身份 | 会为未认证用户创建一个 AnonymousAuthenticationToken 对象作为身份标识 |
| 后续权限判断 | 直接放行,不再进行任何权限检查 | 放行后,如果其他配置(如 hasRole("ANONYMOUS"))需要,可以基于匿名身份进行判断 |
| 对已登录用户 | 允许访问。 | 禁止访问(因为已登录用户不再是匿名身份) |
一、前置类和配置等
yml
因为 JwtAuthenticationTokenFilter 和 SecurityConfig 都用到白名单配置,所以抽取到yml中
yaml
# 安全配置 - 白名单路径
security:
paths:
ANONYMOUS_PATHS:
- /login
- /register
- /captchaImage
- /v2/kms/init/**
- /swagger-ui.html
- /swagger-resources/**
- /swagger-ui/**
- /*/api-docs
- /isNeedKey/**
- /randomString16
- /encryptionKey/info/**
- /base/symmetricKey/getKeyText/**
PERMIT_ALL_PATHS:
- /
- /*.html
- /**/*.html
- /**/*.css
- /**/*.js
- /*.ico
- /favicon.ico
- /profile/**
- /**/*.png
- /**/*.woff
- /**/*.ttf
- /**/*.jpg
- /**/*.svg
- /static/**
- /webjars/**
- /druid/**
- /api/**
- /data/upgrade
- /system/config/configKey/*
对应配置类SecurityProperties
java
/**
* 安全配置属性
*
* @author skms
*/
@Data
@Component
@Slf4j
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
/**
* 白名单路径配置
* 配置位置:application.yml 中 security.paths
*/
private Map<String, List<String>> paths = new HashMap<>();
/**
* 获取需要 anonymous 的路径数组(登录接口等)
*/
public String[] getAnonymousArray() {
List<String> paths = this.paths.get("ANONYMOUS_PATHS");
return paths != null ? paths.toArray(new String[0]) : new String[0];
}
/**
* 获取需要 permitAll 的路径数组(静态资源等)
*/
public String[] getPermitAllArray() {
List<String> paths = this.paths.get("PERMIT_ALL_PATHS");
return paths != null ? paths.toArray(new String[0]) : new String[0];
}
/**
* 获取所有白名单路径数组(用于 JwtAuthenticationTokenFilter)
*/
public String[] getAllWhiteListArray() {
List<String> allPaths = new ArrayList<>();
if (this.paths != null) {
for (List<String> pathList : this.paths.values()) {
if (pathList != null) {
allPaths.addAll(pathList);
}
}
}
return allPaths.toArray(new String[0]);
}
/**
* 检查路径是否在白名单中(支持通配符)
*/
public boolean isWhiteListPath(String requestPath) {
if (paths == null || paths.isEmpty()) {
return false;
}
for (List<String> pathList : paths.values()) {
if (pathList == null) continue;
for (String path : pathList) {
// 1. 匹配 /** 结尾(如 /swagger-ui/**)
if (path.endsWith("/**")) {
String prefix = path.substring(0, path.length() - 3);
if (requestPath.startsWith(prefix)) {
// log.info("DEBUG: whitelist match: {} matches {} via /**", requestPath, path);
return true;
}
}
// 2. 匹配 */xxx 模式(如 */api-docs)
else if (path.indexOf("/*") >= 0) {
// 找出 /* 的位置
int starSlashIndex = path.indexOf("/*");
String before = path.substring(0, starSlashIndex); // /* 前面的部分
String after = path.substring(starSlashIndex + 2); // /* 后面的部分(不包括 /*)
// 检查 requestPath 是否满足:以 before 开头,以 after 结尾,中间至少有一个字符
if (requestPath.startsWith(before) && requestPath.endsWith(after)) {
int beforeEnd = before.length();
int afterStart = requestPath.length() - after.length();
if (beforeEnd < afterStart) {
String middle = requestPath.substring(beforeEnd, afterStart);
// 中间部分不能为空,且如果 before 以 / 结尾,middle 应该以 / 开头
if (!middle.isEmpty()) {
// log.info("DEBUG: whitelist match: {} matches {} via */*", requestPath, path);
return true;
}
}
}
}
// 3. 匹配 /*.xxx 结尾(如 /*.ico)
else if (path.length() >= 3 && path.substring(0, 3).equals("/*.")) {
String ext = path.substring(1); // 提取 .ico 部分
if (requestPath.endsWith(ext)) {
// log.info("DEBUG: whitelist match: {} matches {} via /*.", requestPath, path);
return true;
}
}
// 4. 精确匹配
else if (requestPath.equals(path)) {
// log.info("DEBUG: whitelist match: {} equals {}", requestPath, path);
return true;
}
}
}
// log.info("DEBUG: NO whitelist match for: {}", requestPath);
return false;
}
/**
* 获取指定路径的访问类型
*/
public AccessType getAccessType(String path) {
if (paths == null || paths.isEmpty()) {
return AccessType.PERMIT_ALL;
}
// 先检查 ANONYMOUS_PATHS
List<String> anonymousPaths = paths.get("ANONYMOUS_PATHS");
if (anonymousPaths != null) {
for (String p : anonymousPaths) {
if (p.endsWith("/**")) {
if (path.startsWith(p.substring(0, p.length() - 3))) return AccessType.ANONYMOUS;
} else if (p.endsWith("/*")) {
if (path.startsWith(p.substring(0, p.length() - 2))) return AccessType.ANONYMOUS;
} else if (path.equals(p)) {
return AccessType.ANONYMOUS;
}
}
}
// 再检查 PERMIT_ALL_PATHS
List<String> permitAllPaths = paths.get("PERMIT_ALL_PATHS");
if (permitAllPaths != null) {
for (String p : permitAllPaths) {
if (p.endsWith("/**")) {
if (path.startsWith(p.substring(0, p.length() - 3))) return AccessType.PERMIT_ALL;
} else if (p.endsWith("/*")) {
if (path.startsWith(p.substring(0, p.length() - 2))) return AccessType.PERMIT_ALL;
} else if (path.equals(p)) {
return AccessType.PERMIT_ALL;
}
}
}
return AccessType.PERMIT_ALL;
}
/**
* 访问类型枚举
*/
public enum AccessType {
PERMIT_ALL, // 允许任意访问(不创建认证主体)
ANONYMOUS // 允许匿名访问(创建 AnonymousAuthenticationToken)
}
}
涉及的实体类
java
/**
* 获取用户登录结果
*
* @author skms
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserResult {
/**
* 用户信息
*/
private LoginUser loginUser;
/**
* 用户状态
*/
private LoginUserStatus status;
/**
* 是否有效
*/
public boolean isValid() {
return status == LoginUserStatus.VALID;
}
/**
* 是否需要重新登录
*/
public boolean needReLogin() {
return status == LoginUserStatus.CACHE_NOT_FOUND
|| status == LoginUserStatus.INACTIVE_TIMEOUT
|| status == LoginUserStatus.TOKEN_PARSE_ERROR;
}
}
java
/**
* 登录用户身份权限
*
* @author skms
*/
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
/**
* 应用版本标识
*/
private String appVersion;
public LoginUser() {
}
public LoginUser(SysUser user, Set<String> permissions) {
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, SysUser user, Set<String> permissions) {
this.userId = userId;
this.user = user;
this.permissions = permissions;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@JSONField(serialize = false)
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return 结果
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return 结果
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
public Long getLoginTime() {
return loginTime;
}
public void setLoginTime(Long loginTime) {
this.loginTime = loginTime;
}
public String getIpaddr() {
return ipaddr;
}
public void setIpaddr(String ipaddr) {
this.ipaddr = ipaddr;
}
public String getLoginLocation() {
return loginLocation;
}
public void setLoginLocation(String loginLocation) {
this.loginLocation = loginLocation;
}
public String getBrowser() {
return browser;
}
public void setBrowser(String browser) {
this.browser = browser;
}
public String getOs() {
return os;
}
public void setOs(String os) {
this.os = os;
}
public Long getExpireTime() {
return expireTime;
}
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public SysUser getUser() {
return user;
}
public void setUser(SysUser user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public String getAppVersion() {
return appVersion;
}
public void setAppVersion(String appVersion) {
this.appVersion = appVersion;
}
}
java
/**
* 用户登录状态枚举
*
* @author skms
*/
public enum LoginUserStatus {
/**
* 有效
*/
VALID,
/**
* 缓存不存在
*/
CACHE_NOT_FOUND,
/**
* 版本不匹配
*/
VERSION_MISMATCH,
/**
* 令牌过期
*/
TOKEN_TIMEOUT,
/**
* 无操作超时
*/
INACTIVE_TIMEOUT,
/**
* Token 解析失败
*/
TOKEN_PARSE_ERROR
}
java
/**
* 通用常量信息
*
* @author skms
*/
public class Constants {
/**
* 验证码有效期(分钟)
*/
public static final Integer CAPTCHA_EXPIRATION = 2;
/**
* 令牌
*/
public static final String TOKEN = "token";
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 令牌前缀
*/
public static final String LOGIN_USER_KEY = "login_user_key";
/**
* 用户ID
*/
public static final String JWT_USERID = "userid";
/**
* 应用版本标识-v1
*/
public static final String APP_VERSION_V1 = "v1";
/**
* 应用版本标识-v2
*/
public static final String APP_VERSION_V2 = "v2";
}
二、JwtAuthenticationTokenFilter token过滤器(之前判断逻辑有问题,要先判断用户是否存在或者版本是否一致,再判断SecurityContext 中没有认证信息)
重点刷新或者重新从用户中心获取用户信息等都在 tokenService 中,里面判断缓存是否过期及快过期续期等操作
java
/**
* token 过滤器 验证 token 有效性
*
* @author skms
*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private TokenService tokenService;
@Resource
private SecurityProperties securityProperties;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String requestPath = request.getRequestURI();
// 检查是否在白名单中 (因为这个 filter 在 SecurityConfig 前面执行,所以这里也要共用同一份白名单)
if (isWhiteListPath(requestPath)) {
chain.doFilter(request, response);
return;
}
// 获取用户信息及状态
LoginUserResult userResult = tokenService.getLoginUser(request);
// 1.先判断用户是否有效(有效:更新缓存过期时间和登录时间,无效直接返回 throw),是否版本不匹配(通过用户中心重新获取权限并刷新缓存),然后再判断SecurityContext 中没有认证信息
// 否则 缓存不存在、无操作超时、缓存过期或 token 解析失败 就不用在判断 SecurityContext 中有没有认证信息
if(userResult.getStatus() == LoginUserStatus.VALID){
// 用户有效但 token 即将过期,刷新缓存并重置登录时间
if (tokenService.isTokenExpired(userResult.getLoginUser())) {
// token 已过期,删除缓存并强制重新登录
log.info("Token 已过期,userId: {}", userResult.getLoginUser().getUserId());
tokenService.delLoginUser(userResult.getLoginUser().getToken());
throw new AuthenticationServiceException("用户会话已过期,请重新登录");
} else if (!tokenService.isTokenValid(userResult.getLoginUser())) {
tokenService.verifyToken(userResult.getLoginUser());
}
if(StringUtils.isNull(SecurityUtils.getAuthentication())){
UsernamePasswordAuthenticationToken authenticationToken = buildAuthentication(userResult.getLoginUser());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
} else if(userResult.getStatus() == LoginUserStatus.VERSION_MISMATCH){
// 版本不匹配,通过用户中心重新获取权限并刷新缓存
LoginUserResult refreshResult = tokenService.refreshUserFromUserCenter(request);
if (refreshResult != null && refreshResult.isValid()) {
if(StringUtils.isNull(SecurityUtils.getAuthentication())){
UsernamePasswordAuthenticationToken authenticationToken = buildAuthentication(refreshResult.getLoginUser());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
} else {
throw new AuthenticationServiceException("用户会话已超时,请重新登录");
}
} else {
// 缓存不存在、无操作超时、缓存过期或 token 解析失败,抛出异常触发认证失败处理
throw new AuthenticationServiceException("用户会话已超时,请重新登录");
}
chain.doFilter(request, response);
}
/**
* 构建 Authentication 对象
*/
private UsernamePasswordAuthenticationToken buildAuthentication(LoginUser loginUser) {
return new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
}
/**
* 检查请求路径是否在白名单中
*/
private boolean isWhiteListPath(String requestPath) {
return securityProperties.isWhiteListPath(requestPath);
}
}
三、TokenService
java
/**
* token 验证处理
*
* @author skms
*/
@Slf4j
@Component
public class TokenService {
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
/**
* 令牌自定义标识
*/
@Value("${token.header}")
private String header;
/**
* 令牌秘钥
*/
@Value("${token.secret}")
private String secret;
/**
* 令牌有效期(默认 30 分钟)
*/
@Value("${token.expireTime}")
private int expireTime;
/**
* 连续无操作超时时间 (默认 24 小时=1440 分钟)
*/
@Value("${token.inactiveTime}")
private int inactiveTime;
/**
* 应用版本标识
*/
@Value("${token.appVersion:v2}")
private String appVersion;
@Resource
private RedisCache redisCache;
@Resource
private ISysUserService userService;
@Resource
private UserClientApi userClientApi;
@Resource
private SysPermissionService permissionService;
@Resource
private ISysUserDeviceService sysUserDeviceService;
@Resource
private ICertService certService;
@Resource
@Qualifier("UserDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 获取用户身份信息
*
* @return 用户信息及状态
*/
public LoginUserResult getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser loginUser = redisCache.getCacheObject(userKey);
// 缓存不存在
if (loginUser == null) {
log.debug("用户缓存不存在,token: {}", token);
return LoginUserResult.builder()
.loginUser(null)
.status(LoginUserStatus.CACHE_NOT_FOUND)
.build();
}
// 验证令牌是否已过期(先验证令牌是否过期,拿一个过期令牌过来,也不用判断是不是版本不一致了)
if(isTokenExpired(loginUser)){
log.info("用户token令牌过期,userId: {}", loginUser.getUserId());
return LoginUserResult.builder()
.loginUser(loginUser)
.status(LoginUserStatus.TOKEN_TIMEOUT)
.build();
}
// 验证版本是否匹配
if (!isCurrentVersion(loginUser)) {
log.info("用户版本不匹配,userId: {}", loginUser.getUserId());
return LoginUserResult.builder()
.loginUser(loginUser)
.status(LoginUserStatus.VERSION_MISMATCH)
.build();
}
return LoginUserResult.builder()
.loginUser(loginUser)
.status(LoginUserStatus.VALID)
.build();
} catch (Exception e) {
log.error("获取用户身份信息失败", e);
return LoginUserResult.builder()
.loginUser(null)
.status(LoginUserStatus.TOKEN_PARSE_ERROR)
.build();
}
}
return LoginUserResult.builder()
.loginUser(null)
.status(LoginUserStatus.CACHE_NOT_FOUND)
.build();
}
/**
* 设置用户身份信息
*/
public void setLoginUser(LoginUser loginUser) {
if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
refreshToken(loginUser);
}
}
/**
* 删除用户身份信息
*/
public void delLoginUser(String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>(16);
claims.put(Constants.LOGIN_USER_KEY, token);
claims.put(Constants.JWT_USERID, loginUser.getUserId());
return createToken(claims);
}
/**
* 验证令牌是否已过期
*
* @param loginUser 登录用户
* @return true=已过期,false=未过期
*/
public boolean isTokenExpired(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
return expireTime - currentTime <= 0;
}
/**
* 验证令牌是否有效(未过期且剩余时间充足)
*
* @param loginUser 登录用户
* @return true=有效且充足,false=即将过期
*/
public boolean isTokenValid(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
// Token 未过期且有效期不足 20 分钟时才认为是有效的
return expireTime - currentTime > MILLIS_MINUTE_TEN;
}
/**
* 验证令牌有效期,相差不足 20 分钟,自动刷新缓存
* 同时更新登录时间以重置无操作超时计时器
*
* @param loginUser 登录用户
*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshToken(loginUser);
}
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 暂时注释,token有效期临时使用 10 分钟
// loginUser.setExpireTime(loginUser.getLoginTime() + 10 * MILLIS_MINUTE);
// 设置应用版本标识
loginUser.setAppVersion(appVersion);
// 根据 uuid 将 loginUser 缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
// redisCache.setCacheObject(userKey, loginUser, 10, TimeUnit.MINUTES);
}
/**
* 验证用户缓存是否为当前应用版本
*/
private boolean isCurrentVersion(LoginUser loginUser) {
String currentVersion = appVersion != null ? appVersion : Constants.APP_VERSION_V2;
return currentVersion.equals(loginUser.getAppVersion());
}
/**
* 设置用户代理信息
*
* @param loginUser 登录信息
*/
public void setUserAgent(LoginUser loginUser) {
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 获取请求 token
*
* @param request 请求
* @return token
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
private String getTokenKey(String uuid) {
return Constants.LOGIN_TOKEN_KEY + uuid;
}
/**
* 从用户中心重新获取用户信息(适用于版本不匹配场景)
* 会检查 Token 中的 loginTime,如果连续无操作超时则拒绝登录
*/
public LoginUserResult refreshUserFromUserCenter(HttpServletRequest request) {
String token = getToken(request);
if (StringUtils.isEmpty(token)) {
return LoginUserResult.builder()
.loginUser(null)
.status(com.skms.common.core.domain.model.LoginUserStatus.CACHE_NOT_FOUND)
.build();
}
try {
Claims claims = parseToken(token);
Object userIdObj = claims.get(Constants.JWT_USERID);
Long userId;
if (userIdObj instanceof Integer) {
userId = ((Integer) userIdObj).longValue();
} else if (userIdObj instanceof Long) {
userId = (Long) userIdObj;
} else {
log.error("重新获取用户信息:userId 类型错误,类型为{}", userIdObj == null ? "null" : userIdObj.getClass().getName());
return null;
}
if (userId == null) {
log.error("重新获取用户信息:userId 不存在.");
return null;
}
// 通过用户中心 API 根据 userId 获取最新用户信息
FeignSysUser feignSysUser = userClientApi.getInternalInfo(userId);
if (feignSysUser == null) {
log.error("重新获取用户信息:userId={} 不存在.", userId);
return null;
}
// 重新加载本地角色和设备信息(使用 v2 的数据)
List<Long> roleIds = userService.selectRoleIdsByUserId(feignSysUser.getUserId());
feignSysUser.setRoleIds(roleIds.toArray(new Long[0]));
List<SysRole> roles = userService.selectRolesByUserId(feignSysUser.getUserId());
feignSysUser.setRoles(roles);
List<String> deviceIds = sysUserDeviceService.selectSysDeviceIdsByUserId(feignSysUser.getUserId());
feignSysUser.setDeviceIds(deviceIds);
// 构建新的 LoginUser(使用 v2 的权限)
SysUser user = FeignTransferUser.toFeignSysUser(feignSysUser);
if (StringUtils.isNull(user)) {
log.error("重新获取用户信息:userId={} 不存在.", userId);
throw new UsernameNotFoundException("重新获取用户信息:userId=" + userId + " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.error("重新获取用户信息:userId={} 已被删除.", userId);
throw new BaseException("对不起,您的账号:" + userId + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.error("重新获取用户信息:userId={} 已被停用.", userId);
throw new BaseException("对不起,您的账号:" + userId + " 已停用");
}
LoginUser loginUser = new LoginUser(feignSysUser.getUserId(), user, permissionService.getMenuPermission(user));
// 从 claims 中取 UUID 作为 token(与 Redis key 保持一致)
loginUser.setToken((String) claims.get(Constants.LOGIN_USER_KEY));
// 刷新 Redis 缓存(带新版本号)
refreshToken(loginUser);
return LoginUserResult.builder()
.loginUser(loginUser)
.status(com.skms.common.core.domain.model.LoginUserStatus.VALID)
.build();
} catch (Exception e) {
log.error("重新获取用户信息失败", e);
return LoginUserResult.builder()
.loginUser(null)
.status(com.skms.common.core.domain.model.LoginUserStatus.TOKEN_PARSE_ERROR)
.build();
}
}
}
四、spring security配置
java
import com.skms.framework.config.properties.SecurityProperties;
import com.skms.framework.security.filter.JwtAuthenticationTokenFilter;
import com.skms.framework.security.handle.AuthenticationEntryPointImpl;
import com.skms.framework.security.handle.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import javax.annotation.Resource;
/**
* spring security配置
*
* @author skms
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Resource
@Qualifier("UserDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Resource
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Resource
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Resource
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Resource
private CorsFilter corsFilter;
/**
* 安全配置属性(白名单)
*/
@Resource
private SecurityProperties securityProperties;
/**
* 解决 无法直接注入 AuthenticationManager
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF 禁用,因为不使用 session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于 token,所以不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// OPTIONS 请求全部允许(跨域预检)
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 从配置文件读取 permitAll 路径
.antMatchers(securityProperties.getPermitAllArray()).permitAll()
// 从配置文件读取 anonymous 路径
.antMatchers(securityProperties.getAnonymousArray()).anonymous()
// 除白名单外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加 JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加 CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}