JWT: JSON Web Token(JSON Web令牌)
JWT是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
JWT认证流程:
JWT优点:
简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,数据量小,传输速度也很快;
自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库;
Token是以JSON加密的形式保存在客户端,所以JWT是跨语言的,原则上任何web形式都支持。
不需要在服务端保存会话信息,特别适用于分布式微服务。
JWT结构:
JWT令牌token,是一个String字符串,由3部分组成,中间用点隔开
令牌组成:标头(Header).有效载荷(Payload).签名(Signature)
token格式:head.payload.singurater 如:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoie1wiZWRpdGVkXCI6MTY3OTIyODMwNjAwMCxcImVkaXRvclwiOlwiXCIsXCJoZWFkUG9ydHJhaXRcIjpcImh0dHBzOi8vemhpaHVpcGFpYmFuLTEzMTcwNTExMjkuY29zLmFwLW5hbmppbmcubXlxY2xvdWQuY29tL2hlYWRQb3J0cmFpdC8yNTM1MTY3OTIyOTU0MjI0MC5qcGdcIixcImlkXCI6MjIsXCJqb2JOdW1iZXJcIjpcIjIwMjA0MjI4MDEzXCIsXCJtYWlsYm94XCI6XCIyMzIyNjU2NDMwQHFxLmNvbVwiLFwibmFtZVwiOlwi6ams5pmT5aSpXCIsXCJwYXNzd29yZFwiOlwiNjNlZTQ1MTkzOWVkNTgwZWYzYzRiNmYwMTA5ZDFmZDBcIixcInBob25lXCI6XCIxODc3MjYzODE2MFwiLFwicm9sZWlkXCI6MTAsXCJzZXhcIjpcIjBcIixcInVzZXJuYW1lXCI6XCIxODc3MjYzODE2MFwifSIsImlzcyI6IkZhbmppbmd4dWFuIiwiaWF0IjoxNjc5MjMwOTAyLCJleHAiOjE2NzkyMzA5NjJ9.j0WFGkEACHpTnvEc9EiTEJCQKJ20oIptfrL_kMPz4kU
(参考项目:intelligent_scheduling)
com.auth0
java-jwt
3.2.0
io.jsonwebtoken
jjwt
0.7.0
package cn.com.fanjingxuan.model;
import io.jsonwebtoken.Claims;
/**
* @className: CheckResult.java
* @methodName: CheckResult
* @effect: jwt验证信息实体封装类
* @author: JingxuanFan
* @date: 2023/3/19 15:16
**/
public class CheckResult {
//
private int errCode;
//
private boolean success;
//
private Claims claims;
public int getErrCode(){ return errCode;}
public void setErrCode(int errCode){ this.errCode = errCode;}
public boolean isSuccess(){ return success;}
public void setSuccess(boolean success){ this.success = success;}
public Claims getClaims(){ return claims;}
public void setClaims(Claims claims){ this.claims = claims;}
}
package cn.com.fanjingxuan.constant;
/**
*系统级静态变量
* @author JingxuanFan
* @date: 2023/3/19 15:13
*/
public class SystemConstant {
/**
*token
*/
public static final int JWT_ERRCODE_NULL = 4000; //Token不存在
public static final int JWT_ERRCODE_EXPIRE = 4001; //Token过期
public static final int JWT_ERRCODE_FAIL = 4002; //验证不通过
/**
* JWT
*/
public static final String JWT_SECERT = "8677df7fc3a34e26a61c034d5ec8245d"; //密匙
public static final long JWT_TTL = 30 * 60 * 1000; //token有效时间为半小时
}
package cn.com.fanjingxuan.util;
import cn.com.fanjingxuan.constant.SystemConstant;
import cn.com.fanjingxuan.model.CheckResult;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.*;
import org.bouncycastle.util.encoders.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
/**
* @className: JwtUtil.java
* @methodName: JwtUtil
* @effect: jwt加密和解密的工具类
* @author: JingxuanFan
* @date: 2023/3/19 14:52
**/
public class JwtUtil {
/**
* 签发JWT
* @param id
* @param subject 可以是JSON数据 尽可能少
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id)
.setSubject(subject) // 主题
.setIssuer("Java1234") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate); // 过期时间
}
return builder.compact();
}
/**
* 验证JWT
* @param jwtStr
* @return
*/
public static CheckResult validateJWT(String jwtStr) {
CheckResult checkResult = new CheckResult();
Claims claims = null;
try {
claims = parseJWT(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_EXPIRE);
checkResult.setSuccess(false);
} catch (SignatureException e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}
/**
* 生成加密Key
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.decode(SystemConstant.JWT_SECERT);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析JWT字符串
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
//进行测试的方法
public static void main(String[] args) throws InterruptedException {
//小明有效时间 1小时
User user = new User();
user.setId(1001l);
user.setName("123123");
user.setPassword("111111");
user.setEdited(new Date());
String userStr = JSON.toJSONString(user);
String sc = createJWT("1","20,"+userStr, SystemConstant.JWT_TTL);
Thread.sleep(3000);
System.out.println(validateJWT(sc).getClaims().getSubject());
}
}
package cn.com.fanjingxuan.model;
import java.util.HashMap;
import java.util.Map;
/**
* @className: R.java
* @methodName: R
* @effect: 页面响应的实体类
* @author: JingxuanFan
* @date: 2023/3/19 17:36
**/
public class R extends HashMap {
private static final long serialVersionUID = 1L;
public R() {
put("code", 0);
}
public static R error() {
return error(500, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(500, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
/**
* @className: AdministratorController.java
* @methodName: login
* @effect: 定义带token的登入方法
* @author: JingxuanFan
* @date: 2023/3/19 16:46
**/
@RequestMapping("login")
@ResponseBody
public Map login(Administrator administrator, HttpServletRequest request) throws Exception {
Map result = new HashMap<>();
//校验用户名和密码
Administrator admin = administratorService.login(administrator);
if (admin != null){
if (admin.getPassword().equals(administrator.getPassword())){
//将用户信息存储到Session //登入成功
HttpSession session = request. getSession();
session. setAttribute("userInfo", admin); //1.用于拦截器的判断 2.界面显示用户信息
result. put("Id", admin.getId());
//把token返回给客户端-->客户端保存至localStorage-->客户端每次请求附带localStorage参数
//SystemConstant.JWT_TTL:token有效时间
String JWT = JwtUtil.createJWT("1", JSON.toJSONString(admin), SystemConstant.JWT_TTL);
log.info(JWT);
result.put("JWT",JWT);
return result;
}
//登录失败 ,密码错误
result. put("success", false);
result. put("msg", "密码错误!");
return result;
}
//登录失败,用户名错误或不存在
result. put("success", false);
result. put("msg", "用户名错误或不存在!");
return result;
}
(与5在同一个类里面)
/**
* @className: AdministratorController.java
* @methodName: refreshToken
* @effect: 刷新用户token
* @author: JingxuanFan
* @date: 2023/3/19 16:38
**/
@GetMapping(value = "/refreshToken")
public Map refreshToken(HttpServletRequest request){
Map result = new HashMap<>();
//旧的token令牌
log.info(request.getHeader("token"));
Claims claims = JwtUtil.validateJWT(request.getParameter("token")).getClaims();
String subject = claims.getSubject();
System.out.println(subject);//20,{"edited":1650100778678,"id":1001,"name":"123123","password":"111111"}
String JWT = JwtUtil.createJWT(claims.getId(),claims.getSubject(), SystemConstant.JWT_TTL);
log.info("新token"+JWT);
result.put("JWT",JWT);
return result;
}
(在interceptor(拦截器)包中创建自定义拦截器SysInterceptor.java)
package cn.com.fanjingxuan.interceptor;
import cn.com.fanjingxuan.constant.SystemConstant;
import cn.com.fanjingxuan.model.CheckResult;
import cn.com.fanjingxuan.model.R;
import cn.com.fanjingxuan.util.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @className: SysInterceptor.java
* @methodName: SysInterceptor
* @effect: 拦截器 用户权限校验
* @author: JingxuanFan
* @date: 2023/3/19 17:25
**/
public class SysInterceptor implements HandlerInterceptor {
//日志对象
private final static Logger logger= LoggerFactory.getLogger(SysInterceptor.class);
//拦截的核心方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("执行了拦截的核心方法");
//获取页面的请求地址
String contextPath = request.getRequestURI();
logger.info("路径"+contextPath);
//判断请求对象中有没有请求方法
if (handler instanceof HandlerMethod){
//从请求中取到token令牌
String authHeader = request.getHeader("token");
logger.info(authHeader);
//先判断token令牌是否为空,
if (StringUtils.isEmpty(authHeader)) {
logger.info("验证失败,签名验证不存在");
//调用自定义方法print
print(response, R.error(SystemConstant.JWT_ERRCODE_NULL,"签名验证不存在"));
return false;
}else{
//验证JWT的签名,返回CheckResult对象,将JWT对象(令牌token)进行解密
CheckResult checkResult = JwtUtil.validateJWT(authHeader);
if (checkResult.isSuccess()) {
logger.info("签名验证通过");
return true;
} else {
switch (checkResult.getErrCode()) {
// 签名验证不通过
case SystemConstant.JWT_ERRCODE_FAIL:
logger.info("签名验证不通过");
//调用自定义方法print
print(response,R.error(checkResult.getErrCode(),"签名验证不通过"));
break;
// 签名过期,返回过期提示码
case SystemConstant.JWT_ERRCODE_EXPIRE:
logger.info("签名过期");
//调用自定义方法print
print(response,R.error(checkResult.getErrCode(),"签名过期"));
break;
default:
break;
}
return false;
}
}
}else{
return true;
}
}
//拦截的执行POST请求的方法
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
logger.info("拦截的执行POST请求的方法");
}
/**
* 该方法也是需要当前对应的Interceptor的preHandle方法的返回值为true时才会执行。该方法将在整个请求完成之后,也就是DispatcherServlet渲染了视图执行,
* 这个方法的主要作用是用于清理资源的,当然这个方法也只能在当前这个Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
logger.info("执行了afterCompletion方法");
}
//自定义响应方法
//将请求处理结果响应回浏览器
public void print(HttpServletResponse response, Object message){
try {
//设置响应头的响应状态
response.setStatus(HttpStatus.OK.value());
//设置响应头的响应内容,并转成UTF8
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
//设置头部信息
response.setHeader("Cache-Control", "no-cache, must-revalidate");
//字符输出流
PrintWriter writer = response.getWriter();
//将message写入到浏览器当中
writer.write(message.toString());
//刷新流
writer.flush();
//关闭流
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Configuration 注解可以控制是否开启JWT拦截验证
package cn.com.fanjingxuan.config;
import cn.com.fanjingxuan.interceptor.SysInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @className: WebAppConfigurer.java
* @methodName: WebAppConfigurer
* @effect: 拦截配置--调用链
* @author: JingxuanFan
* @date: 2023/3/19 17:57
**/
// 使用 @Configuration 注解的类就可以被 Spring 识别为配置类,并处理该类上的相关功能注解
@Configuration
public class WebAppConfigurer extends WebMvcConfigurerAdapter {
/**
* 配置不需要拦截和需要拦截的请求
* @param registry
*/
//@Override 注解是用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素。它可以强制一个子类必须重写父类方法或者实现接口的方法。
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加不拦截的方法(登入,注册)
String[] patterns = new String[] {"/*/login","/administrator/saveT","/employee/saveT"};
registry.addInterceptor(new SysInterceptor())
.addPathPatterns("/**") //先拦截所有方法
.excludePathPatterns(patterns); //在拦截的方法中剔除掉 patterns 中的方法(不拦截)
}
}
登入成功生成的token令牌
不带token令牌执行被拦截的命令
带token令牌执行被拦截的命令(同一命令)可以查询
1.在pom中加入相关依赖
com.auth0
java-jwt
4.0.0
com.10duke.client.jwt
jjwt
1.0.0
2.在utils中加入JWT工具类JwtUtil
package cn.com.fjxuan.utils;
import cn.com.fjxuan.model.User;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import java.util.Calendar;
import java.util.Map;
/**
* @className: JwtUtil.java
* @methodName: JwtUtil
* @effect: 生成token的工具类
* @author: JingxuanFan
* @date: 2023/9/20 21:58
**/
public class JwtUtil {
/**
* @methodName testJwtCreate
* @effect: 生成token
*/
public static String testJwtCreate(User user) {
//创建了一个 Calendar 类的实例,表示当前的日期和时间。
Calendar instance = Calendar.getInstance();
//设置过期时间:将当前的日期和时间增加了6个月
instance.add(Calendar.MONTH, 6);
String token = JWT.create()
//头可以不指定一般用默认值
.withClaim("userId", user.getId())
.withClaim("username", user.getUsername())//payLoad
.withClaim("pwd", user.getPwd())
.withExpiresAt(instance.getTime()) //指定令牌的过期时间
//使用HMAC256算法和密钥"qweqwrpf1"对JWT进行签名。密钥用于验证JWT的完整性。
.sign(Algorithm.HMAC256("qweqwrpf1")); // Singnature
//Algorithm.ECDSA256()使用哪种加密算法 括号中写自己指定的密钥
return token;
}
/**
* @methodName testRequire
* @effect: 解析token
* @return
*/
public static Map testRequire(String jwt) {
//require 解密传入加密的签名
//创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("qweqwrpf1")).build();
//使用验证对象中的验证方法 传入生成的JWT串 会返回一个解码的JWT
DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
//返回加密对象
return decodedJWT.getClaims();
}
}
3.调用工具类生成JWT令牌