介绍 JWT全称JSON WEB TOKENS,是当下非常流行的跨域身份验证方法。传统的身份验证需要服务端将用户信息,比如登录信息存入session,然后在用户想要访问后端服务时,通过比对用户的登录状态和session中的数据来进行身份验证。但这种方式在分布式中就显得很麻烦了,JWT 可以跨域传输,适用于微服务、API网关等场景,因为它在客户端存储用户信息,减轻了服务器的内存压力
JWT的组成 JWT整体由三部分组成:Header、Payload、Signature
Header通常由令牌的类型和加密的算法组成,例如:
1 2 3 4 { "alg" : "HS256" , "typ" : "JWT" }
Payload 这部分主要是记录我们所存储的简单且不重要的信息。例如:用户名,过期时间,用户id等等,注意payload中的数据为公开 的,不能在里面存放敏感数据,例如password
1 2 3 4 5 { "sub" : "1234567890" , "name" : "John Doe" , "admin" : true }
Signature 签名里是由三部分组成,Header的Base64编码,Payload的Base64编码,还有secret,然后通过指定的加密方式,例如HS256,进行加密后得出的字符串
1 2 3 4 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
这部分是jwt中最重要的一部分,有两个功能:
1.验证该Token在发送过程中是否被修改
因为token中存在secret,是我们指定的密钥,该密钥保存在服务端且不会向用户公开,这样根据请求token中的secret就能判断是否有被修改
2.验证签发人的身份
第二个功能是是从第一个功能中体现出来的,因为只有自己才知道自己的token
在计算出签名哈希后,JWT头(Header),有效载荷(Payload)和签名哈希(Signature)的三个部分组合成一个字符串,每个部分用”.”分隔,就构成整个JWT对象。
JWT使用逻辑
用户发送登录请求,将用户名和密码传入后端
后端接收后核对账号密码无误,则根据规则生成jwt token
后端将生成的token返回给前端,前端保存在本地如localStorage或sessionStorage,退出登录时删除即可
前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
具体使用示例 导入依赖 1 2 3 4 5 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.7.0</version > </dependency >
创建TokenUtils类用来放置常用方法 获取token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static final long EXPIRE_TIME = 1000 * 60 * 60 * 24 ;public static String genToken (String username,String secret) { Date date = new Date (System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim("username" , username) .withExpiresAt(date) .sign(algorithm); }
创建拦截器
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 @Slf4j public class JwtFilter implements HandlerInterceptor { @Autowired private UserService userService; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token" ); if (!(handler instanceof HandlerMethod)){ return true ; } if (token != null ){ String username = TokenUtils.getUserNameByToken(request); LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(StringUtils.isNotEmpty(username),User::getName,username); User user = userService.getOne(queryWrapper); boolean result = TokenUtils.verify(token,username,user.getPassword()); if (result){ log.info("通过拦截器" ); return true ; } }else { throw new ServiceException (Constants.CODE_401,"token认证失败,请重新登录" ); } return false ; } }
配置拦截器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**" ) .excludePathPatterns("/user/login" , "/user/register" , "**/" ); } @Bean public JwtFilter authenticationInterceptor () { return new JwtFilter (); } }
这样当前端的请求发送过来时首先会被拦截,从中取出token,如果没有token则说明未登录,返回未登录的错误信息。有token则验证token中的信息是否正确,是否被篡改过,验证通过后,拦截器放行,前端能正常收到后端返回的数据。
jwt的一大特点就是服务端不保存用户的身份信息,而是在每次请求时验证用户发送的token,token首次交给用户,用户可以将其保存在localstorage中,然后在每次请求头中添加token,后端收到并验证正确后即可开始正常的业务逻辑,其中返回给用户的token可以在用户登录成功时一并返回
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 @PostMapping("/login") public Res<User> Login (@RequestBody UserDto user, HttpServletRequest request) { String userName = user.getUsername(); log.info("用户{}正在登录" ,userName); String password = DigestUtils.md5DigestAsHex(user.getPassword().getBytes()); LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(User::getName,userName); User one = userService.getOne(queryWrapper); if (one == null ){ log.error("登录失败,用户名不存在" ); return Res.error("用户名不存在,请先注册!" ); } if (password.equals(one.getPassword())){ log.info("登录成功" ); }else { log.error("密码错误!" ); log.info("传入:{},正确:{}" ,password,one.getPassword()); return Res.error("密码错误!" ); } String token = TokenUtils.genToken(userName, password); Res<User> res = Res.success(one); res.setToken(token); return res; }
前端在请求头中添加token 1 2 3 4 5 6 7 8 9 10 11 12 13 request.interceptors .request .use (config => { config.headers ['Content-Type' ] = 'application/json;charset=utf-8' ; let token = localStorage .getItem ("token" ); if (token != null ){ config.headers ['token' ] = token; } return config }, error => { return Promise .reject (error) });