JWT请求流程
JWT的结构
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
- Header
- Payload
- Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品)Signature 签名/签证
Header
JWT的头部承载两部分信息:token类型和采用的加密算法(默认就有无需设置)。
{
"alg": "HS256",
"typ": "JWT"
}
声明类型:这里是jwt
声明加密的算法:通常直接使用 HMAC SHA256
**
Payload
标准中注册的声明 (建议但不强制使用) :
iss
: jwt签发者sub
: 面向的用户(jwt所面向的用户)aud
: 接收jwt的一方exp
: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)nbf
: 定义在什么时间之前,该jwt都是不可用的.iat
: jwt的签发时间jti
: jwt的唯一身份标识,主要用来作为一次性token
,从而回避重放攻击。
Signature
jwt的第三部分是一个签证信息
这个部分需要base64
加密后的header
和base64
加密后的payload
使用.
连接组成的字符串,然后通过header
中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt
的第三部分。
密钥secret
是保存在服务端的,服务端会根据这个密钥进行生成token
和进行验证,所以需要保护好。
总结:
加密:header和payload分别使用base64加密,使用.
号连接,再把这个得到的字符串与header中的加密方式进行加盐(盐 自定义secret)加密,就构成第三部分,三个部分全部.
用连接起来就是token
验证:拿到字符串(token),获取header和payload,和上面相同的算法加盐加密,得到的字符串再和第三部分的signature进行比较。
SpringBoot集成JWT
需要导入maven
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
JWT工具类
public class JWTUtil {
/**
* 过期时间 15天
*/
private static final long EXPIRE_TIME = 15 * 24 * 60 * 60 * 1000;
/**
* token 私钥
*/
private static final String TOKEN_SECRET = "07d319d1860c463e8a18b723d35902fc";
/**
* 加密算法
*/
private static final Algorithm ALGORITHM = Algorithm.HMAC256(TOKEN_SECRET);
/**
* 生成token15分钟后过期
*/
public static String sign(Integer id, String username) {
//过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//生成token 默认header是有数据的 {type:JWT,alg:HS256}
return JWT.create()
.withClaim("id", id) //把id和name存入进去
.withClaim("username", username)
.withExpiresAt(date)
.sign(ALGORITHM);
}
/**
* 校验token是否正确
*/
public static DecodedJWT verify(String token) {
return JWT.require(ALGORITHM).build().verify(token);
}
public static Integer getTokenClaimId(String token) {
return JWT.decode(token).getClaim("id").asInt();
}
public static String getTokenClaimUsername(String token) {
return JWT.decode(token).getClaim("username").asString();
}
}
Controller
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* 测试登录
* @param loginVo
* @return
*/
@PostMapping("/login")
public BaseResult<Object> login(@RequestBody @Valid LoginVo loginVo) {
log.info("loginVo: {}", loginVo);
User currentUser = userService.login(loginVo);
//登录成功 返回token
String token = JWTUtil.sign(currentUser.getId(), currentUser.getUsername());
return BaseResult.success(CommonEnum.SUCCESS, token);
}
/**
* 需要登录才能访问的东西
*/
@GetMapping("/admin/info")
@UserLoginToken
public BaseResult<Object> admin() {
return BaseResult.success(CommonEnum.SUCCESS, "已有token 可以成功访问后台");
}
}
拦截器注解:当使用了这个注解 就必须登录才能继续访问
/**
* 拦截器注解:需要登录携带token才能继续访问
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
拦截器
抛全局异常和response返回数据效果一样
public class VueInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 先看有没有注解 没有就放行
* 有注解就验证token的合法性
*/
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod hm = (HandlerMethod) handler;
UserLoginToken loginToken = hm.getMethodAnnotation(UserLoginToken.class);
//UserLoginToken注解,有则认证
if (loginToken == null) {
return true;
}
response.setContentType("application/json;charset=utf-8");
String msg = null;
ObjectMapper objectMapper = new ObjectMapper();
//验证token
if (loginToken.required()) {
//获取token
String token = request.getHeader("token");
try {
JWTUtil.verify(token);
return true;
} catch (SignatureVerificationException e) {
msg = "无效签名";
} catch (TokenExpiredException e) {
// msg = "token过期";
throw new GlobalException("已经过期了嘻嘻");
} catch (AlgorithmMismatchException e) {
msg = "token算法不一致";
} catch (Exception e) {
msg = "token无效";
}
} else {
return true;
}
response.getWriter().println(objectMapper.writeValueAsString(BaseResult.error(-1, msg)));
return false;
}
}
注册拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new VueInterceptor())
.addPathPatterns("/**");
}
}
其他不重要的配置
application.yml
debug: true # 打开调试模式
server:
port: 8088
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
devtools:
restart:
enabled: true
jpa:
show-sql: true
User
@Data
@Accessors(chain = true)
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY ) //数据库
private Integer id;
private String username;
private String password;
}
LoginVo
@Data
public class LoginVo {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
dao
public interface UserRepository extends JpaRepository<User,Integer> {
/**
* 根据 username ,password查询用户
* @param username
* @param password
* @return
*/
User findByUsernameAndPassword(String username,String password);
}
Service
public interface UserService {
User login(LoginVo login);
}
ServiceImpl
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
//根据用户名密码查询用户
@Override
public User login(LoginVo login) {
User dbUser = userRepository.findByUsernameAndPassword(login.getUsername(), login.getPassword());
if (dbUser == null) {
throw new GlobalException(UserEnum.USER_IS_NULL);
}
return dbUser;
}
}
数据模块枚举
public interface BaseErrorInfoInterface {
/** 错误码*/
Integer getCode();
/** 错误描述*/
String getMessage();
}
CommonEnum
public enum CommonEnum implements BaseErrorInfoInterface {
// 数据操作错误定义
SUCCESS(200, "成功!"),
BODY_NOT_MATCH(400, "请求的数据格式不符!"),
SIGNATURE_NOT_MATCH(401, "请求的数字签名不匹配!"),
NOT_FOUND(404, "未找到该资源!"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误!"),
SERVER_BUSY(503, "服务器正忙,请稍后再试!");
/**
* 错误码
*/
private Integer code;
/**
* 错误描述
*/
private String message;
CommonEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
UserEnum
public enum CommonEnum implements BaseErrorInfoInterface {
// 数据操作错误定义
SUCCESS(200, "成功!"),
BODY_NOT_MATCH(400, "请求的数据格式不符!"),
SIGNATURE_NOT_MATCH(401, "请求的数字签名不匹配!"),
NOT_FOUND(404, "未找到该资源!"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误!"),
SERVER_BUSY(503, "服务器正忙,请稍后再试!");
/**
* 错误码
*/
private Integer code;
/**
* 错误描述
*/
private String message;
CommonEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
自定义异常
@Getter
@Setter
@NoArgsConstructor
public class GlobalException extends RuntimeException{
private Integer code;
public GlobalException(String message){
super(message);
}
/**
* 枚举父接口 BaseErrorInfoInterface
*/
public GlobalException(BaseErrorInfoInterface errorInfoInterface) {
super(errorInfoInterface.getMessage());
this.code = errorInfoInterface.getCode();
}
}
数据返回格式
@Getter
@Setter
@NoArgsConstructor
public class BaseResult<T> {
//返回代码
private Integer code;
//返回消息
private String message;
//返回对象
private T data;
private BaseResult(Integer code, String message) {
this.code = code;
this.message = message;
}
private BaseResult(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功
*/
public static <T> BaseResult<T> success(Integer code, String message,T data) {
return new BaseResult<T>(code, message,data );
}
public static <T> BaseResult<T> success(BaseErrorInfoInterface base,T data) {
return new BaseResult<T>(base.getCode(), base.getMessage(),data );
}
/**
* 失败
*/
public static <T> BaseResult<T> error(Integer code, String message) {
return new BaseResult<T>(code, message);
}
public static <T> BaseResult<T> error(BaseErrorInfoInterface base) {
return new BaseResult<T>(base.getCode(), base.getMessage());
}
}
全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获 自定义的DemoException
*/
@ExceptionHandler(GlobalException.class)
public BaseResult<Object> globalExceptionHandler(GlobalException e) {
log.error("GlobalException:{}", e.getMessage());
return BaseResult.error(e.getCode(), e.getMessage());
}
/**
* jsr303 参数校验异常
*/
@ExceptionHandler(BindException.class)
public BaseResult<Object> validExceptionHandler(BindException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
if(fieldError!=null){
log.error("参数校验异常:{}({})", fieldError.getDefaultMessage(), fieldError.getField());
// 将错误的参数的详细信息封装到统一的返回实体
return BaseResult.error(-1, fieldError.getDefaultMessage());
}
return null;
}
}