JSON Web令牌(JWT)是一种开放的标准(RFC 7519),它定义了一种紧凑而独立的方式在各方之间安全地传输信息为JSON对象。该信息可以被验证和信任,因为它是数字签名的。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公开/私有密钥类型签名。 虽然JWT可以被加密以提供各方之间的保密,但我们将重点关注签名的令牌。被签名的令牌可以验证包含在其中的声明的完整性,而加密的令牌对其他各方隐藏这些声明。当使用公钥/私钥对签名时,签名还证明只有持有私钥的一方才是签名方。
授权(Authorization):这是使用JWT最常见的场景。一旦用户登录,每个后续请求都将包括JWT,允许用户访问该令牌允许的路由、服务和资源。
单点登录(Single Sign On ):单点登录是当今广泛使用的JWT特性,因为它的小规模和易于跨不同领域使用的能力。
信息交换(lnformation Exchange):信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
传输信息(transmitting information):在各方之间传输信息。由于JWT可以签名--例如,使用公共/私钥对--您可以确保发件人是他们所说的发送者。此外,由于签名是使用头和有效载荷计算的,您还可以验证内容没有被篡改。
- 用户使用账号和密码发出post请求;
- 服务器使用私钥创建一个jwt;
- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串在请求头中像服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器。
JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品)
Signature 签名/签证
com.auth0
java-jwt
3.10.3
package com.wxz.utils;
import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.wxz.entity.User;
import com.wxz.mapper.UserMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* TODO
* token的工具类
* @author wxz
* @date 2023/9/10 13:18
*/
@Component
public class TokenUtils {
private static UserMapper staticUserMapper;
@Resource
UserMapper userMapper;
@PostConstruct
public void setUserService() {
staticUserMapper = userMapper;
}
/**
* 生成token
*
* @return
*/
public static String genToken(String userId, String sign) {
return JWT.create().withAudience(userId) // 将 user id 保存到 token 里面,作为载荷
.withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期
.sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
}
/**
* 获取当前登录的用户信息
*
* @return user对象
*/
public static User getCurrentUser() {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("token");
if (StringUtils.isNotBlank(token)) {
String userId = JWT.decode(token).getAudience().get(0);
return staticUserMapper.selectByPrimaryKey(Integer.valueOf(userId));
}
} catch (Exception e) {
return null;
}
return null;
}
}
Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。
withAudience():存入需要保存在token的信息,这里我们把用户ID存入token中
package com.wxz.common;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthAccess {
}
package com.wxz.interceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.wxz.common.AuthAccess;
import com.wxz.common.Constants;
import com.wxz.entity.User;
import com.wxz.exception.ServiceException;
import com.wxz.mapper.UserMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* TODO
*
* @author wxz
* @date 2023/9/10 10:56
*/
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private UserMapper userMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
token = request.getParameter("token");
}
// 如果不是映射到方法直接通过
if (handler instanceof HandlerMethod) {
AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class);
if (annotation != null) {
return true;
}
}
// 执行认证
if (StringUtils.isBlank(token)) {
throw new ServiceException(Constants.AUTH_CODE, "请先登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new ServiceException(Constants.AUTH_CODE, "请先登录");
}
// 根据token中的userid查询数据库
User user = userMapper.selectById(Integer.valueOf(userId));
if (user == null) {
throw new ServiceException(Constants.AUTH_CODE, "请先登录");
}
// 用户密码加签生成验证器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token); // 验证token
} catch (JWTVerificationException e) {
throw new ServiceException(Constants.AUTH_CODE, "请先登录");
}
return true;
}
}
拦截器的主要流程:
从 http 请求头中取出 token,
判断是否映射到方法
检查是否有passtoken注解,有则跳过认证
检查有没有需要用户登录的注解,有则需要取出并验证
认证通过则可以访问,不通过会报相关错误信息
boolean preHandle ():
预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。
package com.wxz.config;
import com.wxz.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器的配置类
* @author wxz
* @date
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
/**
* 重写addInterceptors()实现拦截器
* 配置:要拦截的路径以及不拦截的路径
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor()) //配置拦截规则
.addPathPatterns("/**") // 拦截所有请求,通过判断token是否合法来决定是否需要登录
.excludePathPatterns(
"/login/doLogin", //登录
"/login/register", //注册
"/file/uploadFile",//文件上传
"/user/sendMsg/*" //发送邮件
);//放行的请求
}
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}
package com.wxz.common;
/*
* 系统使用的常量
*/
public interface Constants {
/**
* 权限错误
*/
String AUTH_CODE = "401";
/**
* 系统错误
*/
String SYSTEM_CODE = "500";
}
package com.wxz.exception;
import lombok.Data;
/**
* TODO
* 自定义异常
* @author wxz
* @date 2023/9/9 17:34
*/
@Data
public class ServiceException extends RuntimeException {
private String code;
private String msg;
public ServiceException(String code,String msg) {
this.code =code;
this.msg =msg;
}
}
package com.wxz.common;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* TODO
*
* @author wxz
* @date 2023/9/7 18:21
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Result {
public static final String CODE_SUCCESS = "200";
public static final String CODE_AUTH_ERROR = "401";
public static final String CODE_SYS_ERROR = "500";
private String code;
private String msg;
private Object data;
public static Result success() {
return new Result(CODE_SUCCESS, "请求成功", null);
}
public static Result success(Object data) {
return new Result(CODE_SUCCESS, "请求成功", data);
}
public static Result error(String msg) {
return new Result(CODE_SYS_ERROR, msg, null);
}
public static Result error(String code, String msg) {
return new Result(code, msg, null);
}
public static Result error() {
return new Result(CODE_SYS_ERROR, "系统错误", null);
}
}