JWT学习
概述
什么是JSON网络令牌
JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。由于此信息经过数字签名,因此可以验证和信任。JWT 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
虽然 JWT 可以加密以在各方之间提供保密,但我们将重点关注签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则对其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。
什么时候可以使用jwt
- 授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小,并且能够轻松跨不同域使用。
- 信息交换:JSON Web Tokens 是一种在各方之间安全传输信息的好方法。因为 JWT 可以被签名——例如,使用公钥/私钥对——你可以确定发件人就是他们所说的那样。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。
JWT结构
在其紧凑形式中,JSON Web Tokens 由用点 ( .
)分隔的三个部分组成,它们是:
- header 标题
- payload 有效载荷
- signature 签名
因此,JWT 通常如下所示。
xxxxx.yyyyy.zzzzz
header 标题
标头通常由两部分组成:
- 令牌的类型,即 JWT
- 签名算法,例如 HMAC SHA256 或 RSA。
{
"alg": "HS256",
"typ": "JWT"
}
然后,这个 JSON 被Base64Url编码以形成 JWT 的第一部分。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload有效载荷
令牌的第二部分是负载,其中包含声明。声明是关于实体(通常是用户)和附加数据的声明。共有三种类型的声明:注册声明、公共声明和私人声明。
- 注册声明:这些是一组预定义的声明,这些声明不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。其中一些是: iss(发行者)、 exp(到期时间)、 sub(主题)、 aud(受众)等.
请注意,声明名称只有三个字符,因为 JWT 是紧凑的。
- 公共声明:这些可以由使用 JWT 的人随意定义。但是为了避免冲突,它们应该在IANA JSON Web Token Registry中定义,或者定义为包含抗冲突命名空间的 URI。
- 私人权利:这些都是使用它们同意并既不是当事人之间建立共享信息的自定义声明注册或公众的权利要求。
一个示例有效载荷可能是:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后对有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。
eyJzdWIiOiAiMTIzNDU2Nzg5MCIsIm5hbWUiOiAiSm9obiBEb2UiLCJhZG1pbiI6IHRydWV9
注意:对于已签名的令牌,此信息虽然受到防篡改保护,但任何人都可以读取。除非加密,否则不要将机密信息放入 JWT 的负载或标头元素中。
signature 签名
要创建签名部分,您必须获取编码的标头、编码的有效载荷、秘密、标头中指定的算法,并对其进行签名。
例如,如果要使用 HMAC SHA256 算法,则签名将通过以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在此过程中没有更改,并且在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者是它所说的那个人。
放在一起
输出是三个由点分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更加紧凑。
下面显示了一个 JWT,它具有先前的标头和有效负载编码,并使用机密进行签名。 (密钥:John Doe)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.vr-WmLzwYXpVp48hQIEfHgQgamRsHbzlpsn6hE4oAwg
总结
- 使用签名令牌,令牌中包含的所有信息都会暴露给用户或其他方,即使他们无法更改它。这意味着您不应将秘密信息放入令牌中。
- 必须保存好密钥secret防止泄漏,这是token验证的核心所在,一旦被别人获取,jwt也就形同虚设了
使用流程
- 初次登录时使用账号密码登录
- 服务器判断账号密码的正确性,验证是否登陆成功
- 登录成功则生成token令牌返回给客户端
- 客户端存储令牌,下次发起请求的时候都带上令牌
- 服务器验证令牌是否正确,验证通过才能进行相应的业务操作
一般用户把令牌存放在请求头的Authorization中发起请求
Authorization: Bearer <token>
当然你也可以放在请求体或者直接放在url中,但不推荐使用
优势
对比传统的session认证
我们知道,http协议本身是一种无状态的协议(短连接,发起请求到得到响应就断开),而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
问题
- 用户每次认证在服务器中保存session,随着认证用户的增多,服务器开销会越来越大,最后可能会挂掉
- 扩展性差:服务器存储认证信息,意味着认证信息是与具体服务器绑定的,说明用户下次请求必须还是在这台服务器上认证才有效。在服务器集群中,必须实现session共享
- CSRF:session是基于cookie存储的,如果cookie被截获,用户很容易收到伪造攻击
- 用户每次使用cookie携带的知识sessionId,还需要到服务器上使用sessionid查询相应的用户信息,效率不高。
jwt优势
- 简洁:可以通过url,post参数,或者header发送。数据量小,传输速度快
- 自包含:负载中包含了用户相关信息,不用再查询数据库获取
- 支持跨语言:以json格式存储在客户端
- 跨域:不需要在服务端保存会话信息,适用于分布式服务,移动端服务
JWT的使用
导入jwt
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
生成token
@Test
void contextLoads() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,200); //200秒后过期
String token = JWT.create()
// .withHeader() 默认类型jwt , 加密算法HS256,可以写也可以不写
.withClaim("userId",21) //payload
.withClaim("username","xx")
.withExpiresAt(calendar.getTime()) //令牌过期时间
.sign(Algorithm.HMAC256("woshimiyao")); //签名密钥
System.out.println(token);
}
验证token
@Test
public void test(){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("woshimiyao")).build();
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Mzk5MjA3MDQsInVzZXJJZCI6MjEsInVzZXJuYW1lIjoieHgifQ.0MDi2dUPjJ7AAuVlCQtTaW9kEP4saYTF_ORKHinmcGE"); //token
System.out.println(verify.getClaim("userId").asInt());
System.out.println(verify.getClaim("username").asString());
System.out.println(verify.getExpiresAt());
}
常见异常信息
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMismatchException: 算法不匹配异常
InvalidClaimException: 失效的payload异常
封装jwtutil工具类
package com.xc.jwt.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Map;
@Component
public class JWTUtils {
private static final String secret = "woshimiyao"; //密钥
private static int expire = 200; //过期时间
/**
* 生成token
* @return
*/
public static String createToken(Map<String,String> map){
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,expire); //200秒后过期
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{ //payload数据
builder.withClaim(k,v);
});
String token = builder.withExpiresAt(calendar.getTime()) //令牌过期时间
.sign(Algorithm.HMAC256(secret)); //签名密钥
return token;
}
/**
* 验证token
* @param token
* @return
*/
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}
}
jwt整合springboot
主要代码
userController
package com.xc.jwt.controller;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.xc.jwt.pojo.User;
import com.xc.jwt.service.UserService;
import com.xc.jwt.util.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/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 login = userService.login(user); //数据库中验证账号密码
Map<String,String> payload = new HashMap<>();
payload.put("name",login.getUsername());
payload.put("id",String.valueOf(login.getId()));
String token = JWTUtils.createToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token);
}catch (Exception e){
e.printStackTrace();
map.put("state",false);
map.put("msg","认证失败");
}
return map;
}
@PostMapping("/user/test")
public Map<String,Object> test(HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//处理自己的业务逻辑
String token = request.getHeader("token");
DecodedJWT verify = JWTUtils.verify(token);
log.info("用户id:【{}】",verify.getClaim("id").asString());
log.info("用户名:【{}】",verify.getClaim("name").asString());
map.put("state",true);
map.put("msg","请求成功");
return map;
}
}
编写拦截器
package com.xc.jwt.interceptor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xc.jwt.util.JWTUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map = new HashMap<>();
//获取请求头的令牌
String token = request.getHeader("token");
try {
DecodedJWT verify = JWTUtils.verify(token);
return true;
}catch (SignatureVerificationException e){
e.printStackTrace();
map.put("msg","无效签名");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","token算法不一致");
} catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token过期");
}catch (Exception e){
e.printStackTrace();
map.put("msg","token无效");
}
map.put("state",false);
//将map转为json
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(json);
return false;
}
}
注册拦截器
package com.xc.jwt.config;
import com.xc.jwt.interceptor.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class JWTConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test")
.excludePathPatterns("/user/login");
}
}