文章目录


JWT学习笔记

一、什么是JWT

官网:​​JSON Web Tokens - jwt.io​

JWT——跨域认证解决方案_服务器

官方定义:

JWT——跨域认证解决方案_json_02

其大致意思就是:JWT是一个以JSON格式传输信息,且传输过程中是安全的,因为他有数字签名,所以可以做验证(其中的加密或者签名算法有RSA/CDSA)

自我理解就是:通过以JSON的形式把数据封装成一个令牌,用于保证数据交互过程中的安全性(在传输过程中可以进行加密和签名)。

二、JWT能做什么

2.1、授权

这是使用JWT常见方案,一旦用户登录,每个后续请求将包含JWT,从而允许用户访问令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能。因为它开销小并且可以在不同的域中使用。

2.2、信息交换

JSON Web Token是在各方面之间安全的传输信息的方法。因为可以对JWT进行签名(例如公钥/私钥),所以可以确保发件人是他们所说的人,此外,由于签名时使用的标头和有效负载计算的,因此还可以验证内容是否遇到篡改。

三、为什么是JWT

3.1、传统的Session认证

JWT——跨域认证解决方案_jwt_03

传统的Session认证流程:不同的客户端去访问服务器,由于发送到是http请求(无状态的),所以服务器端不知道谁发的,为了解决引入了Session(服务器端的对象,存储访问服务器的用户信息),但是Session知识解决了http请求是无状态的,也就是说Session只是起到一个标识作用,让服务器知道有人请求我了,但是具体是谁还不清楚。所以这是又出现了一个cookie的东西。当客户端第一次访问服务器的时候,服务器会给发送的客户端返回一个sessionId,而在客户端是以cookie的形式存在。这样就达到了确认的作用。

但是缺点也显而易见,当访问量上来后,服务器因为存储过多的session而造成运行效率降低,服务质量下降的不良影响。

3.2、基于JWT的认证

JWT——跨域认证解决方案_json_04

基于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编码的】

JWT——跨域认证解决方案_spring_05

【被Base64编码的】

JWT——跨域认证解决方案_运维_06

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());

}
}

JWT——跨域认证解决方案_json_07

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、创建数据库

JWT——跨域认证解决方案_spring_08

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;
}

JWT——跨域认证解决方案_spring_09

JWT——跨域认证解决方案_jwt_10

六、优化程序

上述的程序,会有很高的冗余,如果在真正的开发中。因为,每次登录访问资源都要去校验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;
}