package com.productQualification.api.filter;
import com.productQualification.common.annotation.callFrequency.CallFrequencyCheck;
import com.productQualification.common.annotation.PassToken;
import com.productQualification.common.constants.Constants;
import com.productQualification.common.exception.TokenException;
import com.productQualification.common.annotation.callFrequency.CallFrequency;
import com.productQualification.common.util.JwtUtils;
import com.productQualification.user.domain.Admin;
import com.productQualification.user.domain.User;
import com.productQualification.user.service.AdminCacheService;
import com.productQualification.user.service.UserService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
@Resource
private AdminCacheService adminCacheService;
@Resource
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 从请求头中取出 token 这里需要和前端约定好把jwt放到请求头一个叫token的地方
String token = request.getHeader("token");
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//默认全部检查
else {
// System.out.println("被jwt拦截需要验证");
// 执行认证
if (token == null) {
//这里其实是登录失效,没token了 这个错误也是我自定义的,读者需要自己修改
throw new TokenException("token 为空");
}
// 获取 token 中的 user Name
String userId = JwtUtils.getAudience(token);
String type = JwtUtils.getClaimByName(token, "type").asString();
String userName = JwtUtils.getClaimByName(token, "userName").asString();
if ("user".equals(type)) {//小程序用户
User user = userService.findById(Integer.parseInt(userId)).orElseThrow(() -> new RuntimeException("未找到用户信息"));
if (!userName.equals(user.getWeChatOpenId())) {
throw new TokenException("用户信息异常");
}
request.getSession().setAttribute(Constants.USER_ID, user.getId());
} else {//后台管理用户
Admin admin = adminCacheService.findById(Integer.parseInt(userId)).orElseThrow(() -> new RuntimeException("未找到用户信息"));
if (!userName.equals(admin.getUsername())) {
throw new TokenException("用户信息异常");
}
request.getSession().setAttribute(Constants.ADMIN_ID, admin.getId());
request.getSession().setAttribute(Constants.ADMIN_NAME, admin.getUsername());
}
//检查是否有CallFrequencyCheck注释,有则需要校验请求频率
if (method.isAnnotationPresent(CallFrequencyCheck.class)) {
CallFrequencyCheck callFrequencyCheck = method.getAnnotation(CallFrequencyCheck.class);
CallFrequency.frequencyCheck(request, Integer.parseInt(userId), callFrequencyCheck.second());
}
Authentication authentication = JwtUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 验证 token
JwtUtils.verifyToken(token, userId);
return true;
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
代码分析:
这个 JwtAuthenticationInterceptor
拦截器的主要功能是在请求到达 Controller 之前,对 JWT (JSON Web Token) 进行认证和授权,并进行一些额外的处理:
- Token 提取: 从请求头中名为 “token” 的字段获取 JWT。
@PassToken
处理: 如果方法上标记了@PassToken
注解且required=true
,则跳过认证。- Token 认证:
- 检查 token 是否为空,如果为空则抛出
TokenException
异常。 - 从 token 中解析
userId
,type
和userName
。 - 根据
type
的值执行不同的用户认证逻辑:- 如果
type
是 “user”,则从userService
获取用户,验证userName
,并将用户 ID 存入 session。 - 如果
type
不是 “user”(默认视为 “admin”),则从adminCacheService
获取管理员信息,验证userName
,并将管理员 ID 和名称存入 session。
- 如果
- 根据方法上的
@CallFrequencyCheck
注解执行频率限制。 - 设置
SecurityContextHolder
的认证信息。 - 使用
JwtUtils.verifyToken()
验证 token 的有效性。
- 检查 token 是否为空,如果为空则抛出
改进建议:
Token 头部 (Header) 问题:
- 问题: 目前代码从自定义的
token
头部获取 JWT,这不符合标准。 - 建议: 应该使用
Authorization
头部,并采用Bearer
模式。- 请求头:
Authorization: Bearer <your_token>
- 后端代码:
String authorizationHeader = request.getHeader("Authorization"); if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { String token = authorizationHeader.substring(7); // ... 后续的 token 处理 ... } else { throw new TokenException("Invalid Authorization header"); }
- 请求头:
- 问题: 目前代码从自定义的
用户类型 (User Type) 处理:
- 问题: 代码使用字符串字面量
"user"
来判断用户类型,维护性较差。 - 建议: 使用
enum
来表示用户类型:
这样可以提高代码的可读性和可维护性。enum UserType { USER, ADMIN }
- 问题: 代码使用字符串字面量
异常处理:
- 问题: 使用
RuntimeException
处理 “未找到用户信息” 的错误,不够具体。 - 建议: 创建自定义异常,例如
UserNotFoundException
和AdminNotFoundException
,可以更精准的处理异常,并配合@ControllerAdvice
进行统一的异常处理。
- 问题: 使用
Session 使用:
- 问题: 使用 session 存储用户信息在微服务架构下不是最佳实践。
- 建议: 考虑使用分布式缓存(如 Redis)存储用户信息,提高可扩展性。
代码重复:
- 问题:
user
和admin
的处理逻辑有一定的重复,可以提取公共方法进行优化。 - 建议: 将通用的认证逻辑提取到一个方法中,根据用户类型进行不同的处理。
- 问题:
日志记录:
- 问题: 缺乏日志记录,不利于问题排查。
- 建议: 在关键位置添加日志,方便调试和监控。
示例代码 (部分改进示例):
import com.productQualification.common.annotation.callFrequency.CallFrequencyCheck;
import com.productQualification.common.annotation.PassToken;
import com.productQualification.common.constants.Constants;
import com.productQualification.common.exception.TokenException;
import com.productQualification.common.annotation.callFrequency.CallFrequency;
import com.productQualification.common.util.JwtUtils;
import com.productQualification.user.domain.Admin;
import com.productQualification.user.domain.User;
import com.productQualification.user.service.AdminCacheService;
import com.productQualification.user.service.UserService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
// ... 其他 import
enum UserType {
USER, ADMIN
}
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
@Resource
private AdminCacheService adminCacheService;
@Resource
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(PassToken.class) && method.getAnnotation(PassToken.class).required()) {
return true;
}
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
throw new TokenException("Invalid Authorization header");
}
String token = authorizationHeader.substring(7);
if (token == null) {
throw new TokenException("Token is missing");
}
String userId = JwtUtils.getAudience(token);
String typeString = JwtUtils.getClaimByName(token, "type").asString();
String userName = JwtUtils.getClaimByName(token, "userName").asString();
UserType userType;
try {
userType = UserType.valueOf(typeString.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new TokenException("Invalid user type: " + typeString);
}
authenticateAndSetSession(request, token, userId, userName, userType);
if (method.isAnnotationPresent(CallFrequencyCheck.class)) {
CallFrequencyCheck callFrequencyCheck = method.getAnnotation(CallFrequencyCheck.class);
CallFrequency.frequencyCheck(request, Integer.parseInt(userId), callFrequencyCheck.second());
}
Authentication authentication = JwtUtils.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
JwtUtils.verifyToken(token, userId);
return true;
}
private void authenticateAndSetSession(HttpServletRequest request, String token, String userId, String userName, UserType userType) {
switch (userType) {
case USER: {
User user = userService.findById(Integer.parseInt(userId))
.orElseThrow(() -> new RuntimeException("User not found"));
if (!userName.equals(user.getWeChatOpenId())) {
throw new TokenException("Invalid username");
}
request.getSession().setAttribute(Constants.USER_ID, user.getId());
break;
}
case ADMIN: {
Admin admin = adminCacheService.findById(Integer.parseInt(userId))
.orElseThrow(() -> new RuntimeException("Admin not found"));
if (!userName.equals(admin.getUsername())) {
throw new TokenException("Invalid username");
}
request.getSession().setAttribute(Constants.ADMIN_ID, admin.getId());
request.getSession().setAttribute(Constants.ADMIN_NAME, admin.getUsername());
break;
}
default:
throw new TokenException("Unsupported user type: " + userType);
}
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
总结:
你目前的拦截器能够实现 JWT 认证的核心功能,但是还有改进的空间,包括使用标准的 Authorization
头部,使用 enum
来表示用户类型,更加细致的异常处理,以及考虑使用分布式缓存。 我提供的代码片段只是一个改进的思路,你需要根据自己的项目进行调整和完善。 请注意,代码中我用RuntimeException
来演示“用户未找到”的异常情况,实际项目中需要替换成自定义的异常类。