文章目录
JWT学习笔记
一、什么是JWT
官方定义:
其大致意思就是:JWT是一个以JSON格式传输信息,且传输过程中是安全的,因为他有数字签名,所以可以做验证(其中的加密或者签名算法有RSA/CDSA)
自我理解就是:通过以JSON的形式把数据封装成一个令牌,用于保证数据交互过程中的安全性(在传输过程中可以进行加密和签名)。
二、JWT能做什么
2.1、授权
这是使用JWT常见方案,一旦用户登录,每个后续请求将包含JWT,从而允许用户访问令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能。因为它开销小并且可以在不同的域中使用。
2.2、信息交换
JSON Web Token是在各方面之间安全的传输信息的方法。因为可以对JWT进行签名(例如公钥/私钥),所以可以确保发件人是他们所说的人,此外,由于签名时使用的标头和有效负载计算的,因此还可以验证内容是否遇到篡改。
三、为什么是JWT
3.1、传统的Session认证
传统的Session认证流程:不同的客户端去访问服务器,由于发送到是http请求(无状态的),所以服务器端不知道谁发的,为了解决引入了Session(服务器端的对象,存储访问服务器的用户信息),但是Session知识解决了http请求是无状态的,也就是说Session只是起到一个标识作用,让服务器知道有人请求我了,但是具体是谁还不清楚。所以这是又出现了一个cookie的东西。当客户端第一次访问服务器的时候,服务器会给发送的客户端返回一个sessionId,而在客户端是以cookie的形式存在。这样就达到了确认的作用。
但是缺点也显而易见,当访问量上来后,服务器因为存储过多的session而造成运行效率降低,服务质量下降的不良影响。
3.2、基于JWT的认证
基于JWT的认证是保存在客户端的,所以不会造成服务端的资源浪费。
认证流程
- 首先,前端将通过Web表单将自己的用户名和密码发送给后端的接口,这一过程一般是一个Http POST请求。建议的方式是通过SSL加密传输(Https),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT的palyload(负载),将其与头部分别进行Base64的编码拼接后签名,形成一个JWT(Token)。生成的JWT就是一个形态xxx.xxx.xxx的字符串。
- 后端将JWT字串作为登陆成功的返回结果返回给前端。前端将返回的结果保存在LocalStorage或者SessionStorage上,退出登录是前端删除JWT即可。
- 前端每次请求是将JWT保存在Http Header中的Authorization位(解决SXX和XSRF问题)
- 后端检查是否存在,如果存在验证JWT有效性;Token是否过期;Token是否是自己的Token。
- 验证通过后后端使用JWT中包含的用户信息机型逻辑处理,返回相应的结果。
3.3、JWT的优势
- 简介(通过URL,POST参数或者在HTTP Header中发送,数据量小,传输速度快)
- 自包含(负载中包含了所需要的用户信息)
- 跨语言的(基于JSON加密形式保存在客户端)
- 不需要在服务器端保存会话,特别适用于微服务分布式
四、JWT的结构是什么
4.1、令牌构成
token —> String —> x.y.z
我们看JWT是由三部分组成的,大致分为x,y,z
- 第一部分(x):表示header,标头
- 第二部分(y):表示payload,负载
- 第三部分(z):表示signature,签名
所以token的结构式
标头.负载.签名
4.2、header 标头
标头通常由两部分组成,令牌的类型(JWT)和所使用的签名算法(RSA、SHA),他会使用Base64编码组成JWT的第一部分
注意:Base64是一种编码,也就是说,他是可以被翻译回原来的样子的,他并不是一种加密过程
{
"alg":"HS256",
"typ":"JWT"
}
4.3、Payload 负载
令牌的第二部分是有效负载,其中包含声明,声明是有关实体(通常使用户或者其他实体的声明),同样的,也是用Base64编码组成JWT的第二部分
{
"name":"zhangsan",
"sub":"123456789",
"admin":"true"
}
但是官方建议不要放用户私密信息放到Payload,因为Base64可以被翻译。🔥🔥🔥
4.4、Signature 签名
前面两部分都是使用Base64进行编码的,及前端可以被解开token里的信息,Signature需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header所声明的签名算法进行签名。作用是保证JWT没被篡改
。
【未被Base64编码的】
【被Base64编码的】
4.5、常见的异常信息
- SignatureVerifyException:签名不一致异常
- TokenExpiredException:令牌过期异常
- AlgorithmMismatchaException:算法不匹配异常
- InvalidClaimException:失效的payload异常
五、JWT的第一个程序
由于JWT可以支持和各种程序集成,所以我先以一个Java程序
引入依赖
<!-- 引入JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
public class TestGenerateJwt {
/**
* 描述:Token的获取
* @Title: test1
* @author weiyongpeng
* @date 2022年10月5日 上午8:26:35
*/
@Test
public void test1() {
HashMap<String, Object> headerMap = new HashMap<>();
Calendar d =Calendar.getInstance();
d.add(Calendar.MINUTE, 20);
// 设置头信息
String token = JWT.create()
.withHeader(headerMap) // header
.withClaim("username", "zhangsan") // payload 默认是只能放一个 放第二个会把第一个给覆盖掉 放多个可以使用Array
.withClaim("userId", 21) // payload payload里面放的是什么类型,那边获取asxxx就比要与者的类型对应
.withExpiresAt(d.getTime()) // 过期时间
.sign(Algorithm.HMAC256("AWEDSRF")); // signature
System.out.println(token);
}
/**
* 描述:令牌的验证
* @Title: test2
* @author weiyongpeng
* @date 2022年10月5日 上午8:27:07
*/
@Test
public void test2() {
JWTVerifier require = JWT.require(Algorithm.HMAC256("AWEDSRF")).build();
DecodedJWT verify = require.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjQ5MzExNTksInVzZXJJZCI6MjEsInVzZXJuYW1lIjoiemhhbmdzYW4ifQ.c7NTuFiAZw45aCYYwUSo7zac3oNStkNVoasBSeFp1Wk");
System.out.println(verify.getClaim("username").asString());
System.out.println(verify.getClaim("userId").asInt());
System.out.println(verify.getClaims().get("username").asString());
System.out.println(verify.getClaims().get("userId").asInt());
System.out.println("过期时间:"+verify.getExpiresAt());
}
}
5.1、JWT整合SpringBoot
5.1.1、封装工具类
经过上述的使用,我们不难发现,当我们一旦生成JWT,就需要验证JWT是否是我们的,再加上如果设置了过期时间,还需要考虑是否需要续签。所以经常要频繁的操作JWT。
为了方便使用,我们需要封装JWT的工具类用于生成和验证等方法
【JWTUtils】
public class JWTUtils {
private static final String SECRTEKEY = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
/**
* 描述:生成Token
* @Title: getToken
* @param map
* @return
* @author weiyongpeng
* @date 2022年10月5日 上午8:58:32
*/
public static String getToken(Map<String,String> map) {
Calendar d =Calendar.getInstance();
d.add(Calendar.DATE, 7); // 默认是7天过期
Builder builder = JWT.create();
// 设置头信息
map.forEach((key,value)->{
builder.withClaim(key, value);
});
String token = builder
.withExpiresAt(d.getTime()) // 过期时间
.sign(Algorithm.HMAC256(SECRTEKEY)); // signature
System.out.println(token);
return token;
}
/**
* 描述:验证并返回DecoderJWT
* @Title: verifyToken
* @param token
* @return
* @author weiyongpeng
* @date 2022年10月5日 上午9:04:53
*/
public static DecodedJWT verifyToken(String token) {
DecodedJWT verify = null;
try {
verify = JWT.require(Algorithm.HMAC256(SECRTEKEY))
.build()
.verify(token);
} catch (Exception e) {
// TODO: handle exception
return verify;
}
return verify;
}
}
5.1.2、创建数据库
5.1.3、引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 引入JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!-- 引入Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- 引入druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
5.1.4、创建接口
在这里省略包的创建以及配置的相关设置,只展示关键的接口演示
在控制层创建两个接口。一个用于登录生成token,一个用于校验token。在实际开发过程中,一般会把校验放在登陆的接口里。
@GetMapping("/user/login")
public Map<String,Object> login(User user) {
System.out.println("用户名:"+user.getName());
System.out.println("密码:"+user.getPassword());
HashMap<String, Object> map = null;
try {
User u = userService.login(user);
if (u!=null) {
map = new HashMap<>();
// JWT生成令牌
HashMap<String, String> params = new HashMap<>();
params.put("userId", u.getId().toString());
params.put("username", u.getName());
String token = JWTUtils.getToken(params);
// 设置验证用户成功的map
map.put("state", true);
map.put("msg", "登陆成功");
map.put("token", token);
}
} catch (Exception e) {
// TODO: handle exception
map.put("state", false);
map.put("msg", e.getMessage());
}
return map;
}
@PostMapping("/user/verify")
public Map<String,Object> verifyToken(String token){
Map<String, Object> map = new HashMap<>();
try {
DecodedJWT verifyToken = JWTUtils.verifyToken(token);
// 处理业务逻辑
map.put("state", true);
map.put("msg", "处理成功");
return map;
} catch (SignatureVerificationException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "无效签名");
}catch (TokenExpiredException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "token过期");
}catch (AlgorithmMismatchException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "算法不一致");
}catch (InvalidClaimException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "payload失效");
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "登陆失败");
}
map.put("state", false);
return map;
}
六、优化程序
上述的程序,会有很高的冗余,如果在真正的开发中。因为,每次登录访问资源都要去校验Token。所以,如果在单体架构中,我们可以使用拦截器去操作,如果实在分布式我们可以采用网关拦截操作。
public class JWTInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO Auto-generated method stub
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
这里由于官方建议我们把token放到请求头里面,所以前端在发送登录请求的时候,还是可以把token放到header里面
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO Auto-generated method stub
Map<String, Object> map = new HashMap<>();
// 获取请求头中的token
String token = request.getHeader("token");
try {
DecodedJWT verifyToken = JWTUtils.verifyToken(token);
// 处理业务逻辑
return true;
} catch (SignatureVerificationException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "无效签名");
} catch (TokenExpiredException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "token过期");
} catch (AlgorithmMismatchException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "算法不一致");
} catch (InvalidClaimException e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "payload失效");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
map.put("msg", "登陆失败");
}
map.put("state", false);
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
然后再声明一个配置类,用于配置拦截器。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/jwt/user/login");
}
}
最后修改控制器里面的校验接口,即在真正的业务操作中业务逻辑。这里只是简单的演示:
@PostMapping("/test/verify")
public Map<String,Object> verifyToken(){
Map<String, Object> map = new HashMap<>();
// 处理业务逻辑
map.put("state", true);
map.put("msg", "处理成功");
return map;
}