如何优雅地使用 jwt 鉴权

1、导入依赖#

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2、创建配置和工具类#

jwt:
  config:
    key: my-secret-salt # 盐
    ttl: 10080 # token存活时间,单位分钟
    expire: 120 # 无操作过期时间,单位分钟
    singleSignOn: true # true为启用单点登录
    singleSignOnKey: singleSignOnKey_
/**
 * jwt配置
 */
@Data
@Component
@ConfigurationProperties("jwt.config")
public class JwtConfig {

    private String key;

    private long expire;

    private long ttl;

    private boolean singleSignOn;
    
    private String singleSignOnKey;
}
@Component
public class JwtUtil {

    @Autowired
    private JwtConfig jwtConfig;

    /**
     * 生成token
     */
    public String createJWT(int userId, String userName, int userType) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder()
                .setId(String.valueOf(userId))
                .setSubject(userName)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey())
                .claim("userType", userType)
                ;
        if (jwtConfig.getTtl() > 0) {
            Date date = new Date(nowMillis + (jwtConfig.getTtl() * 60 * 1000));
            builder.setExpiration(date);
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     */
    public Claims parseJWT(String jwtStr) {
        return Jwts.parser()
                .setSigningKey(jwtConfig.getKey())
                .parseClaimsJws(jwtStr)
                .getBody()
                ;
    }
}

3、鉴权#

准备好需要使用的类

@Data
@NoArgsConstructor
public class UserDTO {

    private int id;
    private String name;
    private UserTypeEnum userType;

    private String mobile;
    private String password;
    private String token;

    public UserDTO(int id, String name, UserTypeEnum userType) {
        this.id = id;
        this.name = name;
        this.userType = userType;
    }
}

@Getter
@AllArgsConstructor
public enum UserTypeEnum {

    NONE(-1, null),
    ADMIN(1, "管理员"),
    USER(2, "用户"),
    ;

    @JsonValue
    private int value;

    private String desc;

    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static UserTypeEnum getByValue(int value) {
        for (UserTypeEnum userTypeEnum : values()) {
            if (userTypeEnum.getValue() == value) {
                return userTypeEnum;
            }
        }
        return null;
    }
}

public class ExpiredException extends RuntimeException {
}

public class MultiLoginException extends RuntimeException {
}

public class UnauthorizedException extends RuntimeException {
}

jwt拦截器

@Slf4j
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private JwtConfig jwtConfig;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 放行所有options请求
        if (RequestMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
            return true;
        }
        try {
            final String authHeader = request.getHeader("Authorization");
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                // The part after "Bearer "
                final String token = authHeader.substring(7);
                Claims claims = jwtUtil.parseJWT(token);
                if (claims != null) {
                    // 提取用户信息
                    int userId = Integer.parseInt(claims.getId());
                    String userName = claims.getSubject();
                    int userType = Integer.parseInt(claims.get("userType").toString());
                    log.info("[{}_{}] uri: {}", userId, userName, request.getRequestURI());

                    // 单点登录
                    if (jwtConfig.isSingleSignOn()) {
                        String singleSignOnKey = jwtConfig.getSingleSignOnKey() + userId;
                        String singleToken = redisTemplate.opsForValue().get(singleSignOnKey);
                        if (singleToken == null) {
                            throw new ExpiredException();
                        } else if (!token.equals(singleToken)) {
                            throw new MultiLoginException();
                        }
                        // 续签
                        redisTemplate.opsForValue().set(singleSignOnKey, token, jwtConfig.getExpire(), TimeUnit.MINUTES);
                    }

                    // 将用户信息存入 request,以便后续使用
                    request.setAttribute("currUser", new UserDTO(userId, userName, UserTypeEnum.getByValue(userType)));
                    return true;
                }
            }
        } catch (ExpiredJwtException | ExpiredException e) {
            throw new ExpiredException();
        } catch (MultiLoginException e) {
            throw new MultiLoginException();
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new UnauthorizedException();
    }
}

 配置拦截器,拦截除登录以外的所有接口

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    /***
     * addPathPatterns("/**"):拦截所有请求
     * excludePathPatterns: 不拦截的请求
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login")
    }
}

5、配置全局异常和响应处理#

@Data
public class JSONResult {

    /** 响应业务状态 */
    private Integer code;

    /** 响应消息 */
    private String message;

    /** 响应中的数据 */
    private Object data;

    public JSONResult() {

    }

    public JSONResult(Object data) {
        this.code = 20000;
        this.message = "OK";
        this.data = data;
    }

    public JSONResult(Integer code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static JSONResult ok() {
        return new JSONResult(null);
    }

    public static JSONResult ok(Object data) {
        return new JSONResult(data);
    }

    public static JSONResult build(Integer code, String message) {
        return new JSONResult(code, message, null);
    }

    public static JSONResult build(Integer code, String message, Object data) {
        return new JSONResult(code, message, data);
    }

    public static JSONResult errorException(String message) {
        return new JSONResult(500, message, null);
    }

    public static JSONResult errorMap(Object data) {
        return new JSONResult(501, "error", data);
    }

    public static JSONResult errorMsg(String message) {
        return new JSONResult(555, message, null);
    }

    public static JSONResult unauthorized() {
        return new JSONResult(401, "未授权", null);
    }

    public static JSONResult multiLogin() {
        return new JSONResult(402, "账号已在别处登陆!", null);
    }

    public static JSONResult expired() {
        return new JSONResult(403, "登陆超时,请重新登陆!", null);
    }
}
/**
 *  全局异常和响应处理
 */
@Slf4j
@RestControllerAdvice("com.xxx.controller")
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {

    @ExceptionHandler(UnauthorizedException.class)
    public JSONResult unauthorizedException() {
        return JSONResult.unauthorized();
    }

    @ExceptionHandler(MultiLoginException.class)
    public JSONResult multiLoginException() {
        return JSONResult.multiLogin();
    }

    @ExceptionHandler(ExpiredException.class)
    public JSONResult expiredException() {
        return JSONResult.expired();
    }

    /**
     * 拦截之前业务处理,请求先到supports再到beforeBodyWrite
     * 用法1:自定义是否拦截。若方法名称(或者其他维度的信息)在指定的常量范围之内,则不拦截。
     *
     * @return 返回true会执行拦截;返回false不执行拦截
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        final String returnTypeName = methodParameter.getParameterType().getName();
        return !"com.xxx.utils.JSONResult".equals(returnTypeName)
                && !"org.springframework.http.ResponseEntity".equals(returnTypeName);
    }

    /**
     * 向客户端返回响应信息之前的业务逻辑处理
     * 用法1:无论controller返回什么类型的数据,在写入客户端响应之前统一包装,客户端永远接收到的是约定格式的内容
     * 用法2:在写入客户端响应之前统一加密
     *
     * @return 最终响应内容
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter
            , MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass
            , ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (!mediaType.includes(MediaType.APPLICATION_JSON)) {
            return body;
        }
        return JSONResult.ok(body);
    }
}

5、登录测试#

@RequestMapping(value = "/login", method = RequestMethod.POST)
public UserDTO login(@RequestBody UserDTO userDTO) {
    String mobile = userDTO.getMobile();
    String password = userDTO.getPassword();
    Assert.isTrue(StringUtils.isNotEmpty(mobile) && StringUtils.isNotEmpty(password), "请输入登陆信息");
    // todo 做登录操作...
    userDTO = new UserDTO();
    userDTO.setId(1);
    userDTO.setName("管理员");
    userDTO.setUserType(UserTypeEnum.ADMIN);

    // 生成token
    String token = jwtUtil.createJWT(userDTO.getId(), userDTO.getName(), userDTO.getUserType().getId());
    userDTO.setToken(token);
    // 单点登录
    if (jwtConfig.isSingleSignOn()) {
        String singleSignOnKey = jwtConfig.getSingleSignOnKey() + userDTO.getId();
        log.info("singleSignOnKey key --> {}", singleSignOnKey);
        redisTemplate.opsForValue().set(singleSignOnKey, token, jwtConfig.getExpire(), TimeUnit.MINUTES);
    }
    return userDTO;
}

@RequestMapping(value = "/userinfo", method = RequestMethod.GET)
public UserDTO userinfo(HttpServletRequest request) {
    return (UserDTO) request.getAttribute("currUser");
}

访问登录接口即可获取token

然后在不带token的情况访问userinfo接口返回未授权

填入token后即可正确访问并获取当前用户信息

6、使用注解优雅地获取用户信息#

@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrUser {
}
public class CurrUserImpl implements HandlerMethodArgumentResolver {

    /**
     * 判断是否支持使用@CurrUser注解的参数
     */
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        // 如果该参数注解有@CurrUser且参数类型是UserModel
        return methodParameter.getParameterAnnotation(CurrUser.class) != null && methodParameter.getParameterType() == UserDTO.class;
    }

    /**
     * 注入参数值
     */
    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        // 取得HttpServletRequest
        HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();
        // 取出session中的数据
        return request.getAttribute("currUser");
    }
}

 在WebConfig继承addArgumentResolvers方法

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    /***
     * addPathPatterns("/**"):拦截所有请求
     * excludePathPatterns: 不拦截的请求
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login")
    }
    
    /**
     * 自定义参数处理器
     * @param argumentResolvers
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new CurrUserImpl());
    }
}

改造userinfo方法并再次访问

@RequestMapping(value = "/userinfo", method = RequestMethod.GET)
public UserDTO userinfo(@CurrUser UserDTO userDTO) {
    return userDTO;
}

7、使用注解限制接口的访问权限#

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiredPermission {

    /**
     * 允许访问的用户类型
     */
    UserTypeEnum[] userType() default UserTypeEnum.NONE;
}
@Component
public class RequiredPermissionImpl extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 获取方法上的注解
            RequiredPermission requiredPermission = handlerMethod.getMethod().getAnnotation(RequiredPermission.class);
            // 如果方法上的注解为空 则获取类的注解
            if (requiredPermission == null) {
                requiredPermission = handlerMethod.getMethod().getDeclaringClass().getAnnotation(RequiredPermission.class);
            }
            // 如果注解为null, 说明不需要拦截, 直接放过
            if (requiredPermission == null) {
                return true;
            }

            // 判断用户类型权限
            int userType = (int) request.getAttribute("userType");
            if (userType == UserTypeEnum.ADMIN.getId()) {
                return true;
            }
            UserTypeEnum[] userTypeEnums = requiredPermission.userType();
            for (UserTypeEnum userTypeEnum : userTypeEnums) {
                if (userTypeEnum.getId() == userType) {
                    return true;
                }
             }
            throw new UnauthorizedException();
        }
        return true;
    }
}

 这样被@RequiredPermission标记的接口普通用户就访问不了了

当要开放给某个用户类型时@RequiredPermission(userType = {UserTypeEnum.USER})

作者:revil

出处:https://www.cnblogs.com/revil/p/16273863.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   多久会在  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示