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@Slf4jpublic class UserController {@Autowiredprivate UserService userService;/*** 测试登录* @param loginVo* @return*/@PostMapping("/login")public BaseResult<Object> login(@RequestBody @Valid LoginVo loginVo) {log.info("loginVo: {}", loginVo);User currentUser = userService.login(loginVo);//登录成功 返回tokenString token = JWTUtil.sign(currentUser.getId(), currentUser.getUsername());return BaseResult.success(CommonEnum.SUCCESS, token);}/*** 需要登录才能访问的东西*/@GetMapping("/admin/info")@UserLoginTokenpublic 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 {@Overridepublic 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();//验证tokenif (loginToken.required()) {//获取tokenString 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;}}
注册拦截器
@Configurationpublic class InterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new VueInterceptor()).addPathPatterns("/**");}}
其他不重要的配置
application.yml
debug: true # 打开调试模式server:port: 8088spring:datasource:username: rootpassword: 123456url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=truedevtools:restart:enabled: truejpa: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
@Datapublic 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@Transactionalpublic class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;//根据用户名密码查询用户@Overridepublic 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;}@Overridepublic Integer getCode() {return code;}@Overridepublic 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;}@Overridepublic Integer getCode() {return code;}@Overridepublic String getMessage() {return message;}}
自定义异常
@Getter@Setter@NoArgsConstructorpublic 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@NoArgsConstructorpublic 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@Slf4jpublic 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;}}

