JWT
本文共 9,395 字,预计阅读时间 31 分钟
1.概述
1.1定义
jwt(json web token)也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。
1.2主要功能
1)授权
这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
2)信息交换
JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对)所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。
2.基本原理
2.1jwt的认证流程
1)首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
2)后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)形成的JWT就是一个形同l11.Zzz.xxx的字符串。
3)后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
4)前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)HEADER
5)后端检查是否存在,如存在验证JWT的有效性。
6)验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
2.2令牌组成
header.payload .singnature。组成的是一个字符串。
1)标题(Header)
由两部分组成∶令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。
格式如下,是固定的。
{ "alg":"HS256", "typ":"JWT" }
2)有效载荷(Payload)
令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。它也会使用Base64编码组成JWT结构的第二部分。
3)签名(Singnature)
signature需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header 中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过
3.项目实战
源码:https://github.com/zhongyushi-git/springboot-jwt.git
3.1项目准备
本项目是在springboot的基础上开发的,结合了redis。简单起见,登录并没有使用数据库来验证用户信息。
1)首先,新建一个springboot项目,导入redis、fastjson等坐标以及redis工具类。
2)然后编写登录接口LoginController,实现简单的登录功能。
3)再开发一个接口UserController,用于后面测试。
3.2实战演练
1)导入坐标
<!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
2)编写配置信息
#忽略的url,以逗号分隔 system.IgnoreUrl=/login/login #jwt相关配置 #配置请求头中token名称 jwt.config.header=token #加密的秘钥 jwt.config.secret=asdfghjkl123.. #token有效时长,单位是分钟 jwt.config.expire=30
3)编写工具类
package com.zys.springbootjwt.util; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Calendar; import java.util.HashMap; import java.util.Map; /** * @author zhongyushi * @date 2020/9/22 0022 * @dec 描述 */ @Component public class JWTUtil { @Value("${jwt.config.secret}") private String SING; @Value("${jwt.config.expire}") private Integer expire; @Value("${jwt.config.header}") private String header; /** * 生成token * @param map * @return */ public String createToken(Map<String,String> map){ //创建jwt构建器 JWTCreator.Builder builder= JWT.create(); //设置token的过期时间 Calendar calendar=Calendar.getInstance(); //默认是30分钟 calendar.add(Calendar.MINUTE,expire); builder.withExpiresAt(calendar.getTime()); //设置payload,存储需要的一些参数 map.forEach((key,value)->{ builder.withClaim(key,value); }); //加密后生成token String token = builder.sign(Algorithm.HMAC256(SING)); return token; } /** * 验证token,验证通过返回参数信息 * @param token * @return */ public DecodedJWT verifyToken(String token){ JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SING)).build(); //验证token,如果验证失败会抛出异常 DecodedJWT decodedJWT = jwtVerifier.verify(token); return decodedJWT; } /** * 获取登录的用户信息 * @param request * @return */ public Map<String,Object> getLoginUser(HttpServletRequest request){ //获取请求头信息 String token = request.getHeader(header); DecodedJWT decodedJWT = verifyToken(token); //获取payload中设置的参数 Map<String, Claim> claims = decodedJWT.getClaims(); Map<String,Object> result=new HashMap<>(); result.put("username",claims.get("username").asString()); return result; } }
4)编写拦截器
package com.zys.springbootjwt.config; import com.alibaba.fastjson.JSON; import com.auth0.jwt.exceptions.*; import com.auth0.jwt.interfaces.Claim; import com.zys.springbootjwt.util.JWTUtil; import com.zys.springbootjwt.util.RedisUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; /** * @author zhongyushi * @date 2020/9/22 0022 * @dec jwt拦截器,验证token */ @Component public class JWTInterceptor implements HandlerInterceptor { @Value("${jwt.config.header}") private String header; @Autowired private JWTUtil jwtUtil; @Autowired private RedisUtil redisUtil; //在请求之前进行拦截 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取请求头信息 String token = request.getHeader(header); //从redis获取token副本,解决退出问题。当用户退出后,清空redis的token boolean keyIsExists = redisUtil.keyIsExists(header); if (keyIsExists) { String redisToken = redisUtil.getValue(header); //判断副本和原token是否相同 if (!redisToken.equals(token)) { token = null; } } else { token = null; } Map<String, Object> result = new HashMap<>(); //如果token为空直接返回错误信息 if (StringUtils.isEmpty(token)) { result.put("msg", "未授权,无法访问资源!"); } else { try { //验证token jwtUtil.verifyToken(token); //验证通过就放行 return true; } catch (SignatureVerificationException e) { //签名不一致 result.put("msg", "抱歉,签名不一致!"); } catch (TokenExpiredException e) { //token过期 e.printStackTrace(); result.put("msg", "抱歉,token已过期!"); } catch (AlgorithmMismatchException e) { //验证算法不一致 e.printStackTrace(); result.put("msg", "抱歉,验证算法不一致!"); } catch (InvalidClaimException e) { //payload失效 e.printStackTrace(); result.put("msg", "抱歉,token已失效!"); } catch (Exception e) { //其他异常 e.printStackTrace(); result.put("msg", "认证失败,无法访问资源!"); } } result.put("status", false); //验证不通过,就给浏览器返回错误信息 response.setCharacterEncoding("utf-8"); response.setHeader("Content-type", "text/html;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(result)); return false; } }
5)配置拦截器
package com.zys.springbootjwt.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author zhongyushi * @date 2020/9/22 0022 * @dec 拦截器配置 */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Autowired private JWTInterceptor interceptor; //从配置文件读取忽略的url,不拦截这些请求 @Value("${system.IgnoreUrl}") private String ignoreUrl; //添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //addPathPatterns表示拦截所有请求,excludePathPatterns表示不拦截的请求 registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(ignoreUrl); } }
6)修改登录接口,加入jwt
@PostMapping("/login") public JSONObject login(User user) { boolean u = loginService.login(user); JSONObject json = new JSONObject(); if (u) { //设置payload中存储的参数,方便在后台获取 Map<String,String> params=new HashMap<>(); params.put("username",user.getUsername()); //生成token并返回 String token = jwtUtil.createToken(params); //保存token副本到redis redisUtil.setValue(header,token); json.put("msg","登录成功"); json.put("status",true); json.put("token",token); }else{ json.put("msg","用户名或密码错误"); json.put("status",false); } return json; }
7)在登录中添加退出登录接口
@GetMapping("/logout") public JSONObject logout(){ //删除缓存信息 redisUtil.deleteKey(header); JSONObject json = new JSONObject(); json.put("msg","退出成功"); json.put("status",true); return json; }
8)测试
启动项目,使用postman进行测试。
第一,使用get请求访问http://localhost:8080/api/user/name会返回认证失败,原因就是没登录不能直接访问。
第二,使用post方式访问http://localhost:8080/login/login,需要携带用户名和密码参数。正确后会返回一个token。
第三,在请求头携带token使用get请求去访问http://localhost:8080/api/user/name会返回正确的数据。
第四,携带token使用get请求去访问http://localhost:8080/login/logout会返回退出成功。
第五,再次携带token使用get请求去访问http://localhost:8080/api/user/name会返回认证失败,原因是用户已退出。
测试都是模拟进行的,如果是前端直接请求后台,也是类似的,登录之后把token放到请求头中,才能去访问资源。退出时,不仅仅要请求后台,还要把前端的token清空。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!