JWT
1、什么是JWT
官网:https://jwt.io/
学习资料:https://baobao555.tech/archives/40
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于作为JSON对象在各方之间安全地传输信息。可以验证和信任该信息,因为它是数字签名的。jwt可以使用一个秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
简单来说,就是通过JSON形式作为Web应用中的令牌,用于在各方面之间安全的将信息作为JSON对象传输,在数据传输过程中还可以完成数据加密、签名等相关处理。
2、应用领域
- 授权
这是JWT最常见的应用,一旦用户登录,每个后续请求都会包含JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是广泛使用JWT的一项功能,因为他开销小,在不同领域都能轻松使用。
- 信息交换
由于JWT可以进行签名,可以用作各方之间的信息传输,此外,由于签名是使用标头和有效负载进行计算的,因此还可以验证内容是否被篡改。
3、为什么使用JWT
3.1 传统session认证
存在的问题
1、每个用户经过认证之后,都会在服务器做一次记录,以方便用户下次请求的鉴别,通常session都是保存在内存中,随着用户的不断增多,服务器的压力会变大。
2、传统session,对于分布式的应用并不适用,因为每个服务都用自己的session,并不会通用,这也导致了单点登录问题;再者如果部署服务集群,会限制了其相应的负载均衡能力。
3、session基于cookie来进行验证识别,如果cookie别截取,就很容易受到跨站伪造的攻击。
4、在前后端分离的项目中session会大大增加部署的复杂性,通常用户一次请求就要多次转发,如果用session,每次携带sessionid到服务器,服务器还要查询用户信息,在用户很多的情况下。这些信息存储在服务器内存中,给服务器增加负担。还有就是csrf(跨站伪造请求攻击)。再者,sessionid就是一个特征值,表达的信息不够丰富,不容易扩展,如果后端是多节点部署,就需要实现session的共享,不方便集群的部署。
3.2 JWT认证
- 流程
- 优势
1、 简介(Compact):可以通过URL,post参数或者在http请求头(Header)发送,数据量小,传输速度很快。
2、自包含(Self-contained):负载中包含了用户所需要的信息,避免了多次查询数据库。
3、token是以Json加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何形式的web都支持。
4、不需要在服务端保存会话信息,特别适用于分布式微服务。
4、JWT的结构
4.1、令牌(token)组成
- 标头:Header
- 有效负载:Payload
- 签名:Signature
- JWT是一个字符串String,样式为xxxx.yyyy.zzzz分别对应Header.Payload.Signature
- 具体结构为:JWTString = Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
4.2、Header
- 由两部分组成:令牌的类型,即JWT和所使用的签名算法,如HMAC SHA256等。然后使用Base64编码组成JWT结构的第一部分
- {
“alg”:“HS256”,
“typ”:“JWT”
}
4.3、Payload(自包含)
- 有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据,JWT指定七个默认字段供选择
- 除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:
- {
“sub”: “1234567890”,
“name”: “Helen”,
“admin”: true - }
- 同样的,他也会使用Base64编码组成JWT的第二部分。
- 默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息
4.4、Signature
- 签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名:
- HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
- 注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:
- header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据.
- signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值.
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.
分隔,就构成整个JWT对象:
5、基本使用
-
导坐标
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.2</version> </dependency>
-
编写测试类
@Test void test1() { Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, 10); Map<String, Object> map = new HashMap<>(); String token = JWT.create() .withHeader(map) //使用默认的header,可以不写这一步 .withClaim("userId", 10) //payload .withClaim("username", "zhangsan") .withExpiresAt(instance.getTime()) //指定token过期时间 .sign(Algorithm.HMAC256("$%QWEwe14"));//签名 System.out.println(token); }
-
获取token相关内容
@Test void test2(){ JWTVerifier verifier = JWT.require(Algorithm.HMAC256("$%QWEwe14")).build(); DecodedJWT verify = verifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTM1NDIxNDMsInVzZXJJZCI6MTAsInVzZXJuYW1lIjoiemhhbmdzYW4ifQ.pI0ohIUYdODqQ80GcH63rdngiJGP4IiB6Bjh9t6iO-s"); System.out.println(verify.getClaim("userId").asInt()); System.out.println(verify.getClaim("username").asString()); System.out.println("过期时间:"+verify.getExpiresAt()); }
异常信息:
SignatureVerificationException 签名不一致异常
TokenExpiredException 令牌过期异常
AlgorithmMismatchException 算法不匹配异常
InvalidClaimException 失效的payload异常
6、Springboot整合JWT
-
导坐标
<!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.2</version> </dependency> <!-- mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.9</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.32</version> </dependency>
-
编写jwt工具类
public class JWTUtils { private static final String SIGN = "QW!@#as17$&#"; /** * 获取token * * @param map payload * @return */ public static String getToken(Map<String, String> map) { //设置过期时间 Calendar instance = Calendar.getInstance(); instance.add(Calendar.DATE, 7); //7 天过期 //创建jwt builder JWTCreator.Builder builder = JWT.create(); //payload map.forEach((k, v) -> { builder.withClaim(k, v); }); //设置过期时间 String token = builder.withExpiresAt(instance.getTime()) .sign(Algorithm.HMAC256(SIGN)); return token; } /** * 验证token * * @param token * @return jwt信息 */ public static DecodedJWT verify(String token) { return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token); } }
-
jwt拦截器
public class JWTInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); Map<String, Object> map = new HashMap<>(); try { JWTUtils.verify(token); //验证token return true; } catch (SignatureVerificationException e) { map.put("msg", "签名异常"); e.printStackTrace(); } catch (TokenExpiredException e) { map.put("msg", "token过期"); e.printStackTrace(); } catch (AlgorithmMismatchException e) { map.put("msg", "算法不匹配"); e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); map.put("msg", "token无效"); } map.put("status", false); //map->json ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(map); response.setContentType("application/json;charset=UTF-8"); response.getWriter().print(json); return HandlerInterceptor.super.preHandle(request, response, handler); } }
-
配置JWTConfig
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/**") //拦截 .excludePathPatterns("/user/**"); //放行 } }
-
测试
@RestController @Slf4j public class UserController { @Autowired private UserService userService; @RequestMapping("/user/login") public Map<String, Object> login(User user) { log.info("用户名:{}", user.getUsername()); log.info("密码:{}", user.getPassword()); Map<String, Object> map = new HashMap<>(); try { User loginUser = userService.login(user); //payload Map<String, String> payload = new HashMap<>(); payload.put("id", loginUser.getId()); payload.put("username", loginUser.getUsername()); //生成JWT token String token = JWTUtils.getToken(payload); map.put("status", true); map.put("msg", "登陆成功"); map.put("token", token); } catch (Exception e) { map.put("status", false); map.put("msg", e.getMessage()); } return map; } @RequestMapping("/test") public Map<String, Object> test(HttpServletRequest request) { String token = request.getHeader("token"); //处理业务逻辑 DecodedJWT verify = JWTUtils.verify(token); log.info("userId:{}",verify.getClaim("id")); log.info("username:{}",verify.getClaim("username")); Map<String, Object> map = new HashMap<>(); map.put("status", true); map.put("msg", "请求成功"); return map; } }