JWT
# JWT学习
JWT(JSON WEB TOKEN)是一种安全的令牌,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。
# JWT能做什么
# 授权
这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
# 信息交换
JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。
# 传统的session认证
# 认证方式
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.
# 暴露的问题
每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
前后端分离过程中:通常用户一次请求就要转发多次。如果用session 每次携带sessionid 到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制,不方便集群应用。
# JWT认证
我们先去使用,然后我们使用之后,再回过来思考我们为什么要使用JWT认证,和JWT认证的好处那里。
# JWT初体验
引入依赖
<!--引入jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
生成token
@Test void contextLoads() { HashMap<String, Object> map = new HashMap<>(); Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, 90); String token = JWT.create() // 结构-头部(Header) .withHeader(map) // 结构-有效载荷(payload) .withClaim("username", "wxx") .withExpiresAt(instance.getTime()) // 设置令牌的过期时间 // 结构-签名(singnature) .sign(Algorithm.HMAC256("token!Q2W#E$RW")); System.out.println(token); }
输出结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzQ3ODQzNzksInVzZXJJZCI6MjEsInVzZXJuYW1lIjoid3h4In0.rlXxgPwoPSEVi26ypSI5T0DRq__Ujc3ulFAWecKicrU
分析JWT结构
token string => header.payload.singnature
我们的输出结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9对应头部(header)
eyJleHAiOjE2MzQ3ODQzNzksInVzZXJJZCI6MjEsInVzZXJuYW1lIjoid3h4In0对应有效载荷(payload)
rlXxgPwoPSEVi26ypSI5T0DRq__Ujc3ulFAWecKicrU对应签名(singnature)
# 令牌组成 - 1.标头(Header) - 2.有效载荷(Payload) - 3.签名(Signature) - 因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature
# Header - 标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。 - 注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
# Payload - 令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分
# Signature - 前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
# JWT认证
@Test
void check() {
// 这里的参数要和Algorithm.HMAC256("token!Q2W#E$RW")里面的参数保存一致,
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
// 这里是生成的token
DecodedJWT decodedJWT = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzQ3ODU5NDAsInVzZXJJZCI6MjEsInVzZXJuYW1lIjoid3h4In0._Uk3574iI_xH9LVI2XRSFdG-rffu1AN3EUtzhL1eQSk");
System.out.println("用户id: " + decodedJWT.getClaim("userId").asInt());
System.out.println("用户名: " + decodedJWT.getClaim("username").asString());
System.out.println("过期时间: " + decodedJWT.getExpiresAt());
}
SignatureVerificationException: 签名不一致异常
当Algorithm.HMAC256("token!Q2W#E$RW")的token!Q2W#E$RW不一致的时候,就会抛出签名不一致的异常。
TokenExpiredException: 令牌过期异常
当超过时间之后,就会抛出令牌过期的异常。
AlgorithmMismatchException: 算法不匹配异常
Algorithm.HMAC256中的HMAC256这个算法在生成和校验的时候不一致的时候,就会抛出算法不匹配的异常。
InvalidClaimException: 失效的payload异常
# JWT封装
public class JWTUtils {
/**
* 签名
*/
private static final String SIGN = "token!Q2W#E$RW1998102418";
/**
* 生成token
* @param map
* @return
*/
public static String getToken(Map<String, String> map) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 7);
// 创建构造对象
JWTCreator.Builder builder = JWT.create();
// 头部省略
// payload部分
map.forEach(builder::withClaim);
// 设置令牌过期时间
builder.withExpiresAt(instance.getTime());
// 签名部分
String token = builder.sign(Algorithm.HMAC256(SIGN));
return token;
}
/**
* 验证token合法性
* @param token
*/
public static void verify(String token) {
JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
/**
* 返回token的信息
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token) {
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return verify;
}
}
# SpringBoot整合JWT
# 依赖引入
<!--引入mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
# 配置application.yml
spring:
# 引入MySQL配置
datasource:
url: jdbc:mysql://localhost:3306/jwt?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8&tinyInt1isBit=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
profiles:
active: dev
server:
port: 8080
mybatis-plus:
configuration:
# 配置日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 创建数据库
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(80) DEFAULT NULL COMMENT '用户名',
`password` varchar(40) DEFAULT NULL COMMENT '用户密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
# 代码(这里推荐直接推荐生成User表相关的代码)
controller层:
@RestController
@Slf4j
@RequestMapping("/user")
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping(value = "/login",method = RequestMethod.POST)
public Map<String,Object> login(@RequestBody User user) {
Map<String,Object> result = new HashMap<>();
log.info("用户名: [{}]", user.getName());
log.info("密码: [{}]", user.getPassword());
try {
User loginUser = userService.login(user);
//用来存放payload
Map<String, String> map = new HashMap<>();
map.put("id",String.valueOf(loginUser.getId()));
map.put("username", loginUser.getName());
String token = JWTUtils.getToken(map);
result.put("state",200);
result.put("msg","登录成功!!!");
//成功返回token信息
result.put("token",token);
} catch (Exception e) {
e.printStackTrace();
result.put("state",401);
result.put("msg",e.getMessage());
}
return result;
}
@RequestMapping(value = "/test",method = RequestMethod.POST)
public Map<String, Object> test(@RequestParam(value = "token") String token) {
Map<String, Object> map = new HashMap<>();
try {
JWTUtils.verify(token);
map.put("msg", "验证通过~~~");
map.put("state", 200);
} catch (TokenExpiredException e) {
map.put("state", 401);
map.put("msg", "Token已经过期!!!");
} catch (SignatureVerificationException e){
map.put("state", 401);
map.put("msg", "签名错误!!!");
} catch (AlgorithmMismatchException e){
map.put("state", 401);
map.put("msg", "加密算法不匹配!!!");
} catch (Exception e) {
e.printStackTrace();
map.put("state", 401);
map.put("msg", "无效token~~");
}
return map;
}
}
运行截图:
- 登录部分
- 测试部分
# 验证封装
在上面的过程中,我们那些特殊请求,或者需要保护的请求,都需要去传递一个token,这样显得特别麻烦,下面我们对这个进行升级。
书写拦截器:
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 {
JWTUtils.verify(token);
return true;
} catch (TokenExpiredException e) {
map.put("state", 401);
map.put("msg", "Token已经过期!!!");
} catch (SignatureVerificationException e){
map.put("state", 401);
map.put("msg", "签名错误!!!");
} catch (AlgorithmMismatchException e){
map.put("state", 401);
map.put("msg", "加密算法不匹配!!!");
} catch (Exception e) {
e.printStackTrace();
map.put("state", 401);
map.put("msg", "无效token~~");
}
// 将map转换为json
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()).
excludePathPatterns("/user/login")
.addPathPatterns("/user/test");
}
}
# 参考
简书:https://www.jianshu.com/p/576dbf44b2ae
B站:https://www.bilibili.com/video/BV1i54y1m7cP