(处理器级别的拦截器)org.springframework.web.servlet.HandlerInterceptor

发布于:2025-02-11 ⋅ 阅读:(42) ⋅ 点赞:(0)
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) 进行认证和授权,并进行一些额外的处理:

  1. Token 提取: 从请求头中名为 “token” 的字段获取 JWT。
  2. @PassToken 处理: 如果方法上标记了 @PassToken 注解且 required=true,则跳过认证。
  3. Token 认证:
    • 检查 token 是否为空,如果为空则抛出 TokenException 异常。
    • 从 token 中解析 userId, typeuserName
    • 根据 type 的值执行不同的用户认证逻辑:
      • 如果 type 是 “user”,则从 userService 获取用户,验证 userName,并将用户 ID 存入 session。
      • 如果 type 不是 “user”(默认视为 “admin”),则从 adminCacheService 获取管理员信息,验证 userName,并将管理员 ID 和名称存入 session。
    • 根据方法上的 @CallFrequencyCheck 注解执行频率限制。
    • 设置 SecurityContextHolder 的认证信息。
    • 使用 JwtUtils.verifyToken() 验证 token 的有效性。

改进建议:

  1. 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");
            }
        
  2. 用户类型 (User Type) 处理:

    • 问题: 代码使用字符串字面量 "user" 来判断用户类型,维护性较差。
    • 建议: 使用 enum 来表示用户类型:
        enum UserType {
              USER, ADMIN
          }
      
      这样可以提高代码的可读性和可维护性。
  3. 异常处理:

    • 问题: 使用 RuntimeException 处理 “未找到用户信息” 的错误,不够具体。
    • 建议: 创建自定义异常,例如 UserNotFoundExceptionAdminNotFoundException,可以更精准的处理异常,并配合 @ControllerAdvice 进行统一的异常处理。
  4. Session 使用:

    • 问题: 使用 session 存储用户信息在微服务架构下不是最佳实践。
    • 建议: 考虑使用分布式缓存(如 Redis)存储用户信息,提高可扩展性。
  5. 代码重复:

    • 问题: useradmin 的处理逻辑有一定的重复,可以提取公共方法进行优化。
    • 建议: 将通用的认证逻辑提取到一个方法中,根据用户类型进行不同的处理。
  6. 日志记录:

    • 问题: 缺乏日志记录,不利于问题排查。
    • 建议: 在关键位置添加日志,方便调试和监控。

示例代码 (部分改进示例):

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来演示“用户未找到”的异常情况,实际项目中需要替换成自定义的异常类。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到