如何优雅地使用 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 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异