refactor: SpringSecurity + JWT 认证鉴权重构

This commit is contained in:
haoxr
2023-09-13 18:27:15 +08:00
parent ae59e5251e
commit 3ff462305b
21 changed files with 160 additions and 214 deletions

View File

@@ -1,11 +1,10 @@
package com.youlai.system.aspect;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.exception.BusinessException;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.util.RequestUtils;
import com.youlai.system.common.annotation.PreventDuplicateSubmit;
import com.youlai.system.security.JwtTokenManager;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.exception.BusinessException;
import com.youlai.system.security.jwt.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -25,7 +24,7 @@ import java.util.concurrent.TimeUnit;
* 处理重复提交的切面
*
* @author haoxr
* @since 3.0.0
* @since 2.3.0
*/
@Aspect
@Component
@@ -35,7 +34,7 @@ public class DuplicateSubmitAspect {
private final RedissonClient redissonClient;
private final JwtTokenManager jwtTokenManager;
private final JwtTokenProvider jwtTokenProvider;
private static final String RESUBMIT_LOCK_PREFIX = "LOCK:RESUBMIT:";
/**
@@ -68,9 +67,10 @@ public class DuplicateSubmitAspect {
private String generateResubmitLockKey() {
String resubmitLockKey = null;
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String jwt = RequestUtils.resolveToken(request);
if (StrUtil.isNotBlank(jwt)) {
String jti = (String) jwtTokenManager.getTokenClaims(jwt).get("jti");
String token = jwtTokenProvider.resolveToken(request);
if (StrUtil.isNotBlank(token)) {
String jti = (String) jwtTokenProvider.getTokenClaims(token).get("jti");
resubmitLockKey = RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI();
}
return resubmitLockKey;

View File

@@ -4,10 +4,8 @@ import java.lang.annotation.*;
/**
* MP数据权限注解
* <p>
*
* @author zc
* @link https://gitee.com/baomidou/mybatis-plus/issues/I37I90
* @since 2.0.0
*/
@Documented

View File

@@ -1,4 +1,4 @@
package com.youlai.system.util;
package com.youlai.system.common.util;
import com.alibaba.excel.EasyExcel;
import com.youlai.system.listener.easyexcel.MyAnalysisEventListener;

View File

@@ -1,6 +1,7 @@
package com.youlai.system.util;
package com.youlai.system.common.util;
import cn.hutool.json.JSONUtil;
import com.youlai.system.common.result.IResultCode;
import com.youlai.system.common.result.Result;
import com.youlai.system.common.result.ResultCode;
@@ -10,6 +11,8 @@ import org.springframework.http.MediaType;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.youlai.system.common.result.ResultCode.*;
/**
* 响应工具类
*

View File

@@ -1,4 +1,4 @@
package com.youlai.system.util;
package com.youlai.system.common.util;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;

View File

@@ -1,11 +1,11 @@
package com.youlai.system.config;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.filter.JwtAuthenticationFilter;
import com.youlai.system.security.exception.MyAccessDeniedHandler;
import com.youlai.system.security.exception.MyAuthenticationEntryPoint;
import com.youlai.system.security.JwtTokenManager;
import com.youlai.system.security.jwt.JwtTokenFilter;
import com.youlai.system.filter.VerifyCodeFilter;
import com.youlai.system.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -36,7 +36,7 @@ public class SecurityConfig {
private final MyAuthenticationEntryPoint authenticationEntryPoint;
private final MyAccessDeniedHandler accessDeniedHandler;
private final JwtTokenManager jwtTokenManager;
private final JwtTokenProvider jwtTokenProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
@@ -58,7 +58,7 @@ public class SecurityConfig {
// 验证码校验过滤器
http.addFilterBefore(new VerifyCodeFilter(),UsernamePasswordAuthenticationFilter.class);
// JWT 校验过滤器
http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}

View File

@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.system.common.constant.ExcelConstants;
import com.youlai.system.common.result.PageResult;
import com.youlai.system.common.result.Result;
import com.youlai.system.util.ExcelUtils;
import com.youlai.system.common.util.ExcelUtils;
import com.youlai.system.common.annotation.PreventDuplicateSubmit;
import com.youlai.system.listener.easyexcel.UserImportListener;
import com.youlai.system.model.vo.UserImportVO;

View File

@@ -1,63 +0,0 @@
package com.youlai.system.filter;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.util.RequestUtils;
import com.youlai.system.util.ResponseUtils;
import com.youlai.system.security.JwtTokenManager;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT 校验过滤器
*
* @author haoxr
* @since 2022/10/1
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");
private final JwtTokenManager tokenManager;
public JwtAuthenticationFilter(JwtTokenManager tokenManager) {
this.tokenManager = tokenManager;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 登录接口放行是走过滤器链的方式(验证码校验过滤器),这里拦截到登录接口需要手动放行
if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
// 手动放行登录接口
chain.doFilter(request, response);
}else{
String jwt = RequestUtils.resolveToken(request);
if (StrUtil.isNotBlank(jwt) && SecurityContextHolder.getContext().getAuthentication() == null) {
try {
// 解析&验证 JWT
Claims claims = this.tokenManager.parseAndValidateToken(jwt);
// JWT验证有效获取Authentication存入Security上下文
Authentication authentication = this.tokenManager.getAuthentication(claims);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}catch (Exception e){
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
}
}else{
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
}
}
}
}

View File

@@ -5,7 +5,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.util.ResponseUtils;
import com.youlai.system.common.util.ResponseUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;

View File

@@ -7,7 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.youlai.system.common.annotation.DataPermission;
import com.youlai.system.common.base.IBaseEnum;
import com.youlai.system.common.enums.DataScopeEnum;
import com.youlai.system.util.SecurityUtils;
import com.youlai.system.common.util.SecurityUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;

View File

@@ -1,9 +1,7 @@
package com.youlai.system.interceptor;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.security.JwtTokenManager;
import io.jsonwebtoken.Claims;
import com.youlai.system.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
@@ -25,13 +23,13 @@ import java.security.Principal;
@RequiredArgsConstructor
public class AuthChannelInterceptor implements ChannelInterceptor {
private final JwtTokenManager jwtTokenManager;
private final JwtTokenProvider jwtTokenProvider;
/**
* 连接前监听
*
* @param message
* @param channel
* @param message 消息
* @param channel 通道
* @return
*/
@Override
@@ -41,14 +39,12 @@ public class AuthChannelInterceptor implements ChannelInterceptor {
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// get token from header
String token = accessor.getFirstNativeHeader("Authorization");
String bearerToken = accessor.getFirstNativeHeader("Authorization");
// if token is not null
if (StrUtil.isNotBlank(token)) {
if (StrUtil.isNotBlank(bearerToken)) {
token = token.substring(SecurityConstants.TOKEN_PREFIX.length());
Claims claims = jwtTokenManager.parseAndValidateToken(token);
String username = claims.get("username", String.class);
bearerToken = bearerToken.substring(7);
String username = jwtTokenProvider.getUsername(bearerToken);
// if the username is not null, assign it to the Principal.
if (StrUtil.isNotBlank(username)) {
Principal principal = () -> username;

View File

@@ -1,7 +1,7 @@
package com.youlai.system.security.exception;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.util.ResponseUtils;
import com.youlai.system.common.util.ResponseUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

View File

@@ -0,0 +1,44 @@
package com.youlai.system.security.jwt;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.common.util.ResponseUtils;
import com.youlai.system.exception.BusinessException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* @author haoxr
* @since 2023/9/13
*/
public class JwtTokenFilter extends OncePerRequestFilter {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(request);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (BusinessException ex) {
//this is very important, since it guarantees the user is not authenticated at all
SecurityContextHolder.clearContext();
ResponseUtils.writeErrMsg(response,(ResultCode)ex.getResultCode());
return;
}
filterChain.doFilter(request, response);
}
}

View File

@@ -1,13 +1,17 @@
package com.youlai.system.security;
package com.youlai.system.security.jwt;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.security.model.SysUserDetails;
import io.jsonwebtoken.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.DecodingException;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -15,62 +19,53 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
/**
* JWT token 管理器
* JWT token 管理器
*
* @author haoxr
* @since 2022/10/22
* @since 2023/9/13
*/
@Component
public class JwtTokenManager {
/**
* token加密密钥
*/
@Value("${auth.token.secret_key}")
private String secretKey;
/**
* token有效期(单位:)
*/
@Value("${auth.token.ttl}")
private Long tokenTtl;
/**
* secret key byte array.
*/
private byte[] secretKeyBytes;
private JwtParser jwtParser;
public class JwtTokenProvider {
@Resource
private RedisTemplate redisTemplate;
@Value("${jwt.secret-key:123456}")
private String secretKey;
@Value("${jwt.expiration:7200}")
private int expiration;
private byte[] secretKeyBytes;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
/**
* Create token.
* 创建Token
*
* @param authentication auth info
* @return token
* @param authentication
* @return
*/
public String createToken(Authentication authentication) {
Claims claims = Jwts.claims().setSubject(authentication.getName());
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
claims.put("jti",IdUtil.fastSimpleUUID());
claims.put("userId", userDetails.getUserId());
claims.put("username", claims.getSubject());
claims.put("deptId", userDetails.getDeptId());
claims.put("dataScope", userDetails.getDataScope());
// 角色放入JWT的claims
Set<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
claims.put("authorities", roles);
@@ -79,48 +74,57 @@ public class JwtTokenManager {
Set<String> perms = userDetails.getPerms();
redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userDetails.getUserId(), perms);
// 过期时间
Date expirationTime = new Date(System.currentTimeMillis() + tokenTtl * 1000L);
Date now = new Date();
Date expirationTime = new Date(now.getTime() + expiration * 1000L);
return Jwts.builder()
//.setId(IdUtil.fastSimpleUUID()) TODO 设置jti无效
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expirationTime)
.signWith(Keys.hmacShaKeyFor(this.getSecretKeyBytes()), SignatureAlgorithm.HS256).compact();
.signWith(Keys.hmacShaKeyFor(getSecretKeyBytes()), SignatureAlgorithm.HS256).compact();
}
/**
* 获取认证信息
*/
public Authentication getAuthentication( Claims claims) {
SysUserDetails principal = new SysUserDetails();
principal.setUserId(Convert.toLong(claims.get("userId"))); // 用户ID
principal.setUsername(Convert.toStr(claims.get("username"))); // 用户名
principal.setDeptId(Convert.toLong(claims.get("deptId"))); // 部门ID
principal.setDataScope(Convert.toInt(claims.get("dataScope"))); // 数据权限
public Authentication getAuthentication(String token) {
Claims claims = this.getTokenClaims(token);
SysUserDetails userDetails = new SysUserDetails();
userDetails.setUserId(Convert.toLong(claims.get("userId"))); // 用户ID
userDetails.setUsername(Convert.toStr(claims.get("username"))); // 用户
userDetails.setDeptId(Convert.toLong(claims.get("deptId"))); // 部门ID
userDetails.setDataScope(Convert.toInt(claims.get("dataScope"))); // 数据权限范围
List<SimpleGrantedAuthority> authorities = ((ArrayList<String>) claims.get("authorities"))
.stream()
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
.map(SimpleGrantedAuthority::new)
.toList();
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
/**
* 解析 & 验证 token
*/
public Claims parseAndValidateToken(String token) {
// 解析成功说明JWT有效
Claims claims = this.getTokenClaims(token);
// 验证JWT 是否在黑名单(注销场景会存入黑名单)
Boolean isBlack = redisTemplate.hasKey(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + claims.get("jti"));
if (isBlack) {
throw new RuntimeException("token 已被禁用");
public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
public boolean validateToken(String token) {
Jwts.parserBuilder().setSigningKey(getSecretKeyBytes()).build().parseClaimsJws(token);
return true;
}
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public Claims getTokenClaims(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build().parseClaimsJws(token).getBody();
return claims;
}
public byte[] getSecretKeyBytes() {
if (secretKeyBytes == null) {
try {
@@ -133,14 +137,4 @@ public class JwtTokenManager {
}
/**
* get token claims
*/
public Claims getTokenClaims(String token) {
if (jwtParser == null) {
jwtParser = Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build();
}
Claims claims = jwtParser.parseClaimsJws(token).getBody();
return claims;
}
}
}

View File

@@ -3,7 +3,7 @@ package com.youlai.system.security.service;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.util.SecurityUtils;
import com.youlai.system.common.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;

View File

@@ -5,11 +5,10 @@ import cn.hutool.captcha.GifCaptcha;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.security.jwt.JwtTokenProvider;
import com.youlai.system.service.AuthService;
import com.youlai.system.util.RequestUtils;
import com.youlai.system.model.dto.CaptchaResult;
import com.youlai.system.model.dto.LoginResult;
import com.youlai.system.security.JwtTokenManager;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@@ -34,8 +33,8 @@ import java.util.concurrent.TimeUnit;
public class AuthServiceImpl implements AuthService {
private final AuthenticationManager authenticationManager;
private final JwtTokenManager jwtTokenManager;
private final RedisTemplate redisTemplate;
private final JwtTokenProvider jwtTokenProvider;
/**
* 登录
@@ -49,7 +48,7 @@ public class AuthServiceImpl implements AuthService {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username.toLowerCase().trim(), password);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
String accessToken = jwtTokenManager.createToken(authentication);
String accessToken = jwtTokenProvider.createToken(authentication);
return LoginResult.builder()
.tokenType("Bearer")
.accessToken(accessToken)
@@ -62,9 +61,9 @@ public class AuthServiceImpl implements AuthService {
@Override
public void logout() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = RequestUtils.resolveToken(request);
String token = jwtTokenProvider.resolveToken(request);
if (StrUtil.isNotBlank(token)) {
Claims claims = jwtTokenManager.getTokenClaims(token);
Claims claims = jwtTokenProvider.getTokenClaims(token);
String jti = claims.get("jti", String.class);
Date expiration = claims.getExpiration();
if (expiration != null) {

View File

@@ -20,7 +20,7 @@ import com.youlai.system.model.vo.RolePageVO;
import com.youlai.system.service.SysRoleMenuService;
import com.youlai.system.service.SysRoleService;
import com.youlai.system.service.SysUserRoleService;
import com.youlai.system.util.SecurityUtils;
import com.youlai.system.common.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

View File

@@ -11,7 +11,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.system.common.constant.SecurityConstants;
import com.youlai.system.common.constant.SystemConstants;
import com.youlai.system.converter.UserConverter;
import com.youlai.system.util.SecurityUtils;
import com.youlai.system.common.util.SecurityUtils;
import com.youlai.system.mapper.SysUserMapper;
import com.youlai.system.model.dto.UserAuthInfo;
import com.youlai.system.model.bo.UserBO;

View File

@@ -1,24 +0,0 @@
package com.youlai.system.util;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.constant.SecurityConstants;
import jakarta.servlet.http.HttpServletRequest;
/**
* 请求工具类
*
* @author haoxr
*/
public class RequestUtils {
/**
* 请求头解析获取 Token
*/
public static String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(SecurityConstants.TOKEN_KEY);
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith(SecurityConstants.TOKEN_PREFIX)) {
return bearerToken.substring(SecurityConstants.TOKEN_PREFIX.length());
}
return null;
}
}