结构
Header 头部信息,主要声明了JWT的签名算法等信息
Payload 载荷信息,主要承载了各种声明并传递明文数据
Signature 签名,拥有该部分的JWT被称为JWS,也就是签了名的JWT,用于校验数据
整体结构是:
header.payload.signature
参考文档:https://doc.hutool.cn/pages/jwt/
token被解密:如工具包被获取。可通过增加“盐值”来解决。
token被拿到第三方使用:如被包装到第三方使用(ChatGPT工具),可以通过限流来解决。
封装hutool工具类:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
public class JwtUtil { private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);
/** * 盐值很重要,不能泄漏,且每个项目都应该不一样,可以放到配置文件中 */ private static final String key = "xxx";
public static String createToken(Long id, String mobile) { LOG.info("开始生成JWT token,id:{},mobile:{}", id, mobile); GlobalBouncyCastleProvider.setUseBouncyCastle(false); DateTime now = DateTime.now(); DateTime expTime = now.offsetNew(DateField.HOUR, 24); // DateTime expTime = now.offsetNew(DateField.SECOND, 10);
Map<String, Object> payload = new HashMap<>(); // 签发时间 payload.put(JWTPayload.ISSUED_AT, now); // 过期时间 payload.put(JWTPayload.EXPIRES_AT, expTime); // 生效时间 payload.put(JWTPayload.NOT_BEFORE, now); // 内容 payload.put("id", id); payload.put("mobile", mobile); String token = JWTUtil.createToken(payload, key.getBytes()); LOG.info("生成JWT token:{}", token); return token; }
public static boolean validate(String token) { LOG.info("开始JWT token校验,token:{}", token); GlobalBouncyCastleProvider.setUseBouncyCastle(false); JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes()); // validate包含了verify boolean validate = jwt.validate(0); LOG.info("JWT token校验结果:{}", validate); return validate; }
public static JSONObject getJSONObject(String token) { GlobalBouncyCastleProvider.setUseBouncyCastle(false); JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes()); JSONObject payloads = jwt.getPayloads(); payloads.remove(JWTPayload.ISSUED_AT); payloads.remove(JWTPayload.EXPIRES_AT); payloads.remove(JWTPayload.NOT_BEFORE); LOG.info("根据token获取原始内容:{}", payloads); return payloads; }
public static void main(String[] args) { createToken(1L, "123");
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3MzY0ODczMDQsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE3MzY1NzM3MDQsImlhdCI6MTczNjQ4NzMwNH0.Bui7guCvPEF557eqxRLwmt5tO-W-3oVLnn37H4qOVfA"; validate(token);
getJSONObject(token); } } |
后端定义登录业务:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public MemberLoginResp login(MemberLoginReq memberLoginReq){ String mobile = memberLoginReq.getMobile(); String code = memberLoginReq.getCode(); Member memberDB = selectByMobile(mobile);
if (ObjectUtil.isEmpty(memberDB)){ throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST); }
if(!code.equals("8888")){ throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR); }
MemberLoginResp memberLoginResp = new MemberLoginResp(); memberLoginResp.setId(memberDB.getId()); memberLoginResp.setMobile(mobile);
String token = JwtUtil.createToken(memberDB.getId(), memberDB.getMobile()); memberLoginResp.setToken(token);
return memberLoginResp; } |
通过调用封装的JwtUtil生成token并返回前端

成功返回Token结果
Vuex全局保存Token到store中
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { createStore } from 'vuex'
const MEMBER = "MEMBER";
export default createStore({ state: { member: {} }, getters: { }, mutations: { setMember (state, _member) { state.member = _member; } }, actions: { }, modules: { } }) |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const login = () => { axios.post("/member/member/login", loginForm).then((response) => { let data = response.data; if (data.success) { notification.success({ description: '登录成功!' }); // 登录成功,跳到控台主页 router.push("/welcome"); store.commit("setMember", data.content); } else { notification.error({ description: data.message }); } }) }; |
store存放用户信息后,如果刷新页面,那么信息也会消失!store可以理解为缓存,一旦重新加载,则缓存全都没了。
解决方法:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 所有的session key都在这里统一定义,可以避免多个功能使用同一个key SESSION_ORDER = "SESSION_ORDER"; SESSION_TICKET_PARAMS = "SESSION_TICKET_PARAMS";
SessionStorage = { get: function (key) { var v = sessionStorage.getItem(key); if (v && typeof(v) !== "undefined" && v !== "undefined") { return JSON.parse(v); } }, set: function (key, data) { sessionStorage.setItem(key, JSON.stringify(data)); }, remove: function (key) { sessionStorage.removeItem(key); }, clearAll: function () { sessionStorage.clear(); } }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico" rel="external nofollow" > <!-- 引入js --> <script src="<%= BASE_URL %>js/session-storage.js"></script> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const MEMBER = "MEMBER";
export default createStore({ state: { member: window.SessionStorage.get(MEMBER) || {} # 读取 }, getters: { }, mutations: { setMember (state, _member) { state.member = _member; window.SessionStorage.set(MEMBER, _member); # 设置 } }, |
不再是把member定义为{},而是首先在缓存中获取,如果没有则设置为{}。同时避免空指针
同时在用户登录后设置MEMBER缓存
|
1 2 3 4 5 |
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.10</version> </dependency> |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
@Component public class LoginMemberFilter implements Ordered, GlobalFilter {
private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class);
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath();
// 排除不需要拦截的请求 if (path.contains("/admin") || path.contains("/redis") || path.contains("/test") || path.contains("/member/member/login") || path.contains("/member/member/send-code")) { LOG.info("不需要登录验证:{}", path); return chain.filter(exchange); } else { LOG.info("需要登录验证:{}", path); } // 获取header的token参数 String token = exchange.getRequest().getHeaders().getFirst("token"); LOG.info("会员登录验证开始,token:{}", token); if (token == null || token.isEmpty()) { LOG.info( "token为空,请求被拦截" ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); }
// 校验token是否有效,包括token是否被改过,是否过期 boolean validate = JwtUtil.validate(token); if (validate) { LOG.info("token有效,放行该请求"); return chain.filter(exchange); } else { LOG.warn( "token无效,请求被拦截" ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); }
}
/** * 优先级设置 值越小 优先级越高 * * @return */ @Override public int getOrder() { return 0; } } |
|
1 2 3 4 5 6 7 8 9 |
@RestController public class TestController {
@GetMapping("/test") public String test(){ return "test"; }
} |

调用需要登录的接口方法(未登录)

同时服务器端没有打印,表示请求已被拦截
调用login登陆后再次执行上述请求
login打印日志:

调用请求打印日志:

可见成功校验token,并读取登录用户信息,通过校验