登录令牌JWT — JSON WEB TOKEN 原创
登录令牌JWT — JSON WEB TOKEN
春节不停更,此文正在参加「星光计划-春节更帖活动」
关于作者
- 作者介绍
🍓 博客主页:作者主页
🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。
🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨💻。
JWT简介
1、概述
传统的Web应用中,使用session来存在用户的信息,每次用户认证通过以后,服务器需要创建一条记录 保存用户信息,通常是在内存中。
- 随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大
- 由于Session是在内存中的,这就带来一些扩展性的问题
- 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题
- 需要客户端(浏览器)中使用cookie存储session的ID值,但是移动端设备没有cookie
2、什么是JWT?
JWT是是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。并且是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT
就是token的一种具体实现方式,其全称是JSON Web Token
官网地址:https://jwt.io/
先来说一下基本的流程:
- 跨域是一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域,要想跨域肯定要实现不同的端到端的通信。
- 客户端使用用户名和密码进行登录
- 服务端收到请求,验证客户端的用户名和密码
- 验证成功后,服务端会签发一个类似于钥匙一样的私钥token,再把这个token返回给客户端
- 客户端收到token后可以把它存储到cookie、session、redis等
- 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie、header中携带
- 服务端接收到请求,然后验证客户端请求里面是否携带着token,如果验证成功,就向客户端返回请求数据
与传统的认证方式相比有哪些好处
- json的通用性非常好,JWT支持多种语言实现,如JAVA,JavaScript,JS,PHP等很多语言都可以使用。
- 因为有了payload部分,可以携带非敏感信息。
- 方便信息传递,jwt的组成简单,占用字节小。
- 易于应用的扩展,不需要在服务端保存会话信息。
原理图
1、前端也就是客户端,通过表单提交用户名和密码信息发送到后端(服务的)
2、后端(服务端)验证该用户的用户名和密码是否正确,验证通过通过代码规定生成相对应的token令牌,token令牌将包含用户的数据i西信息作为Payload,与JWT Header分别进行Base64编码拼接后签名,生产类似于zzz.sss.rrr的字符串
3、后端(服务端)将生成的token作为判断用户登录成功的依据
4、前端拿到后端发来的token令牌后存储起来,等下一次用户需要再次请求服务器时,该用户将携带token(未过期的)请求服务器端以获取数据
5、后端拦截该用户的请求,判断token是否过期,未过期则执行业务逻辑,返回用户需要的数据
6、后端拦截该用户的请求,判断token是否过期,token令牌过期则返回错误的登录信息,这是需要后端再次生成token令牌,此时又会回到步骤1。
<font color=“red”>注意,session和JWT的主要区别就是保存的位置,session是保存在服务端的,而JWT是保存在客户 端的,JWT就是一个固定格式的字符串</font>
3、结构
JWT固定各种的字符串,由三部分组成:
- Header,头部
- Payload,载荷
- Signature,签名
<font color=“red”>注意,把这三部分使用点(.)连接起来,就是一个JWT字符串</font>
1)头部
header一般的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。 JWT里验证和签名使用的算法列表如下:
JWS | 算法名称 |
---|---|
HS256 | HMAC256 |
HS384 | HMAC384 |
HS512 | HMAC512 |
RS256 | RSA256 |
RS384 | RSA384 |
RS512 | RSA512 |
ES256 | ECDSA256 |
ES384 | ECDSA384 |
ES512 | ECDSA512 |
例如,
{
"typ": "JWT",
"alg": "HS256"
}
2)载荷
payload主要用来包含声明(claims ),这个声明一般是关于实体(通常是用户)和其他数据的声明。 声明有三种类型:
- registered
- public
- private
具体如下:
Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
Public claims : 可以随意定义
- 自定义数据:存放在token中存放的key-value值
Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明
例如:
{
"iss": "sxau",
"iat": 1446593502,
"exp": 1446594722,
"aud": "sxau.edu.com",
"sub": "1016942589@qq.com",
"username": "admin"
}
<font color=“red”>注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的</font>
把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号 . 连接在一起(头部在前),形成新的字符串:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y.WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9
3)签名
最后,将上面拼接完的字符串用HS256算法进行加密,在加密的时候,还需要提供一个密钥(secret)。 加密后的内容也是一个字符串,这个字符串就是签名。
把这个签名拼接在刚才的字符串后面就能得到完整的JWT字符串。 header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分, 服务端也就无法通过。 在JWT中,消息体是透明的,使用签名可以保证消息不被篡改。 例如,使用HMACSHA256加密算法,配合秘钥,将前俩部进行加密,生成签名
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
例如,将Header、Payload、Signature三部分使用点(.)连接起来
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9.DNVhr36j66JpQBfcYoo64IRp84dKiQeaq7axHTBcP9 E
例如,使用官网提供的工具,可以对该JWT进行验证和解析
<font color=“red”>注意,在代码中,我们使用JWT封装的工具类,也可以完成此操作</font>
4、使用
在springboot中可以很容易的使用JWT,只需要引入相关依赖,封装一个JWT的工具类,并且编写 Controller的拦截器,对指定路径进行拦截验证token即可。
1)新建项目springboot-jwt
2)pom文件中,引入操作jwt相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zmz</groupId>
<artifactId>springboot-jjwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot-jjwt</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3)Controller层
package com.zmz.springbootjjwt.api;
import com.alibaba.fastjson.JSONObject;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.TokenService;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
/**
* @author zhangshengrui
* @date 2022-01-06 20:45
*/
@Controller
@RequestMapping("api")
public class UserApi {
@Autowired
UserService userService;
@Autowired
TokenService tokenService;
@GetMapping("/loginto")
public String loginto(String username,String password ,ModelMap map){
map.addAttribute("name","ceshi");
System.out.println(username+" "+password);
return "login";
}
//登录
@ResponseBody
@PostMapping("/login")
public Object login(@RequestBody User user){
System.out.println(user);
JSONObject jsonObject=new JSONObject();
User userForBase=userService.findByUsername(user);
if(userForBase==null){
jsonObject.put("message","登录失败,用户不存在");
return jsonObject;
}else {
if (!userForBase.getPassword().equals(user.getPassword())){
jsonObject.put("message","登录失败,密码错误");
return jsonObject;
}else {
String token = tokenService.getToken(userForBase);
jsonObject.put("token", token);
jsonObject.put("user", userForBase);
return jsonObject;
}
}
}
@ResponseBody
@UserLoginToken
@GetMapping("/getMessage")
public String getMessage(){
return "你已通过验证";
}
}
- 拦截器
package com.zmz.springbootjjwt.interceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.zmz.springbootjjwt.annotation.PassToken;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @author zhangshengrui
* @date 2022-01-06 20:41
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
5)全局异常处理GloablExceptionHandler.java
package com.zmz.springbootjjwt.interceptor;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author zhangshengrui
* @date 2022-01-06 22:37
*/
@ControllerAdvice
public class GloablExceptionHandler {
@ResponseBody
@ExceptionHandler(Exception.class)
public Object handleException(Exception e) {
String msg = e.getMessage();
if (msg == null || msg.equals("")) {
msg = "服务器出错";
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("message", msg);
return jsonObject;
}
}
6)javaconfig配置类
package com.zmz.springbootjjwt.config;
import com.zmz.springbootjjwt.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author zhangshengrui
* @date 2022-01-06 22:33
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**")// 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
.excludePathPatterns("/loginto","/static/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
7)启动项目,直接访问http://localhost:8888/api/login
8)登录成功拿到Token之后访问http://localhost:8888/api/getMessage
不携带token
注意:这里的key
一定不能错,因为在拦截器中是取关键字token
的值String token = httpServletRequest.getHeader("token");
加上token
之后就可以顺利通过验证和进行接口访问了