手把手教你完成springboot的jwt认证
参考
本文内容基本参考B站视频BV1vj411D7X4与文章https://mp.weixin.qq.com/s/cvwuE3nDnnUiTN_Qa58kAQ。写这个是防止后续忘记相关操作与帮助自己梳理流程
1.JWT结构
JWT由Header、Playload、Signature三部分组成,由.拼接。令牌最终的格式像这样:xxxxx.yyyyy.zzzzz。在传输的时候,会将JWT的三个部分分别进行Base64编码后,拼接成最终传输的字符串,也就是我们的Json Web Token
1.1 Header
Header-标头。JWT头是一个描述JWT元数据的JSON对象。报头通常由两部分组成:令牌的类型和所使用的签名算法。
- typ属性:表示令牌的类型,JWT令牌统一写为"JWT"
- alg属性:表示使用的签名算法,默认为HMAC SHA256(写为HS256),或者RSA
{
"alg": "HS256",
"typ": "JWT"
}
然后,这个JSON被Base64Url编码,以形成JWT的第一部分。
1.2 Payload
playload-载荷。存放用户自定义的信息,通常会把用户信息和令牌到期时间放在这里,同样是一个JSON对象。
{
"name": "123456",
"password": "John Doe",
}
然后,有效负载被Base64Url编码,以形成JWT的第二部分。
请注意,对于已签名的令牌,这些信息虽然受到保护,不会被篡改,但任何人都可以读取。除非经过加密,否则不要将机密信息放入JWT的payload或header中。
Payload中有七个默认字段供选择,默认字段并不要求强制使用。我们还可以自定义私有字段,一般会把包含用户信息的非保密数据放到Payload中。
{
// 默认7个字段
"iss": 发行人,
"exp": 到期时间,
"sub": 主题,
"aud": 用户,
"nbf": 在此之前不可用,
"iat": 发布时间,
"jti": JWT ID用于标识该JWT,
// 自定义字段
"uid": 1001,
"username": "admin",
"nickname": "管理员"
}
1.3 Signature
Signature-签名。要创建签名部分,您必须获取编码的标头(Header)、编码的有效载荷(Payload)、机密(secret)、标头中指定的算法(如HS256),并对其进行签名。
例如,如果要使用HMAC SHA256算法,则将以以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
使用标头的算法和私钥对第一部分和第二部分进行加密,通过Base64Url编码后形成JWT的第三部分。
Signature用于验证消息在发送过程中没有更改,在使用私钥签名的令牌的情况下,它还可以验证JWT的发送者是否就是它所说的那个人。
2.JWT的使用
2.1 相关依赖包的安装
1.使用IDEA创建一个【Spring Initializr】类型的项目名称为【springboot_jwt】的工程。
2.在项目的pom.xml配置文件中添加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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/>
</parent>
<groupId>com.ytx</groupId>
<artifactId>springboot_jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot_jwt</name>
<description>springboot_jwt</description>
<properties>
<java.version>11</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>compile</scope>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- JWT相关依赖,jdk1.8以上版本还需引入以下依赖 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.在com.ytx.springboot_jwt.test包下创建JwtTests测试类,演示JWT的基本使用。
package com.ytx.springboot_jwt.test;//这里是你自己的路径
import io.jsonwebtoken.*;
import org.junit.Test;
import java.util.Date;
import java.util.UUID;
public class JwtTests {
private long time = 1000 * 60 * 60 * 1;
private String signature = "admin";
// 创建JWT
@Test
public void createJwt() {
JwtBuilder jwtBuilder = Jwts.builder();
String jwtToken = jwtBuilder
// 1.Header
.setHeaderParam("typ", "JWT") // 向Header追加参数
.setHeaderParam("alg", "HS256")
/**
* 2.Payload
* claim():如果builder中Claims属性为空,则创建DefaultClaims对象,并把键值放入;
* 如果Claims属性不为空,获取之后判断键值,存在则更新,不存在则直接放入。
*/
.claim("username", "tom")
.claim("role", "admin")
.setSubject("admin-test") // 设置主题
.setExpiration(new Date(System.currentTimeMillis() + time)) // 过期时间
.setId(UUID.randomUUID().toString())
// 3.Signature
.signWith(SignatureAlgorithm.HS256, signature) // 两个参数分别是签名算法和自定义的签名Key(盐)
// 使用"."符号连接
.compact();
System.out.println(jwtToken);
}
// 校验JWT
@Test
public void checkJwt() {
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2ODY5NTk1NjAsImp0aSI6ImZhMDMzMGVhLTc0YTMtNGI4My1hNmZiLTg1MjA0ZGE2NDMyMCJ9.a0WKFt2rU-SVvWTrk_fqjDX_-Z6YLnxjVjD05oXajk4";
// isSigned():校验JWT是否进行签名。方法很简单,以分隔符".",截取JWT第三段,即签名部分进行判断
boolean result = Jwts.parser().isSigned(token);
System.out.println(result);
}
// 解析JWT
@Test
public void parseJwt() {
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2ODY5NTk1NjAsImp0aSI6ImZhMDMzMGVhLTc0YTMtNGI4My1hNmZiLTg1MjA0ZGE2NDMyMCJ9.a0WKFt2rU-SVvWTrk_fqjDX_-Z6YLnxjVjD05oXajk4";
JwtParser jwtParser = Jwts.parser();
Jws<Claims> claimsJws = jwtParser
.setSigningKey(signature) // 与JwtBuilder中签名方法signWith()对应
.parseClaimsJws(token);
Claims claims = claimsJws.getBody();
System.out.println(claims.get("username"));
System.out.println(claims.get("role"));
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getExpiration());
}
}
2.2 SpringBoot+JWT
1.在com.ytx.springboot_jwt.domain包下创建User实体类。
package com.ytx.springboot_jwt.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private Integer id;
private String username;
private String password;
}
2.在com.ytx.springboot_jwt.utils包下创建响应统一结果集ResponseResult工具类。
package com.ytx.springboot_jwt.utils;
import java.io.Serializable;
/**
* 响应结果类
* @param <E> 响应数据的类型
*/
public class ResponseResult<E> implements Serializable {
/** 操作成功的状态码 */
public static final int OK = 200;
/** 状态码 */
private Integer state;
/** 状态描述信息 */
private String message;
/** 数据 */
private E data;
public ResponseResult() {
}
public ResponseResult(Integer state, String message) {
this.state = state;
this.message = message;
}
public ResponseResult(Integer state, String message, E data) {
this.state = state;
this.message = message;
this.data = data;
}
public Integer getState() {
return state;
}
public void setState(Integer state) {
this.state = state;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public E getData() {
return data;
}
public void setData(E data) {
this.data = data;
}
public static <E> ResponseResult<E> getResponseResult() {
return new ResponseResult<>(OK, null, null);
}
public static <E> ResponseResult<E> getResponseResult(Integer state) {
return new ResponseResult<>(state, null, null);
}
public static <E> ResponseResult<E> getResponseResult(String message) {
return new ResponseResult<>(OK, message, null);
}
public static <E> ResponseResult<E> getResponseResult(E data) {
return new ResponseResult<>(OK, null, data);
}
public static <E> ResponseResult<E> getResponseResult(Integer state, String message) {
return new ResponseResult<>(state, message, null);
}
public static <E> ResponseResult<E> getResponseResult(String message, E data) {
return new ResponseResult<>(OK, message, data);
}
}
3.在com.ytx.springboot_jwt.utils包下创建JWT操作的工具类(此处工具类其实是测试类的相关代码)。
package com.ytx.springboot_jwt.utils;
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.UUID;
public class JwtUtils {
private static final long time = 1000 * 60 * 60 * 1;
private static final String signature = "admin";
public static String createToken() {
JwtBuilder jwtBuilder = Jwts.builder();
String jwtToken = jwtBuilder
// Header
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
// Payload
.claim("username", "tom")
.claim("role", "admin")
.setSubject("admin-test")
.setExpiration(new Date(System.currentTimeMillis() + time))
.setId(UUID.randomUUID().toString())
// Signature
.signWith(SignatureAlgorithm.HS256, signature)
// 使用"."符号连接
.compact();
return jwtToken;
}
public static boolean checkToken(String token) {
if (token == null || token == "") {
return false;
}
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(signature).parseClaimsJws(token);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
4.在com.ytx.springboot_jwt.controller包下创建UserController控制层类。
package com.ytx.springboot_jwt.controller;
import com.ytx.springboot_jwt.domain.User;
import com.ytx.springboot_jwt.utils.JwtUtils;
import com.ytx.springboot_jwt.utils.ResponseResult;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("users")
public class UserController {
@PostMapping("login")
public ResponseResult<String> login(@RequestBody User user) {
String username = "tom";
String password = "123456";
if (username.equals(user.getUsername()) && password.equals(password)) {
// 添加Token
String token = JwtUtils.createToken();
return ResponseResult.getResponseResult("登录成功", token);
}
return ResponseResult.getResponseResult(4000, "登录失败");
}
@PostMapping("check_token")
public ResponseResult<Boolean> checkToken(HttpServletRequest request) {
String token = request.getHeader("token");
if (JwtUtils.checkToken(token)) {
return ResponseResult.getResponseResult("Token验证成功");
}
return ResponseResult.getResponseResult(4001, "Token验证失败");
}
}
5.接下来就可以用postman或者其他相关软件测试token发放与验证接口,time是token的过期时间。
2.3 HandlerInterceptor拦截器
完成jwt的验证与发放之后,还需要注册拦截器,拦截未有token的请求。
本文参考的视频在这一步没有注册拦截器,参考的文章定义拦截器时逻辑写反,如果有人参考文章与视频在这一步要注意。
1.创建拦截器类com.cy.store.interceptor.LoginInterceptor,并实现org.springframework.web.servlet.HandlerInterceptor接口。
package com.ytx.springboot_jwt.interceptor;
import com.ytx.springboot_jwt.utils.JwtUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** 定义处理器拦截器 */
public class LoginInterceptor implements HandlerInterceptor {
// 该方法将在请求处理之前被调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (JwtUtils.checkToken(token)) {
// 当返回值true时,就会继续调用下一个Interceptor的preHandle方法,如果已经是最后一个Interceptor的时,就会调用当前请求的Controller方法
return true;
}
// 重定向到指定的页面
response.sendRedirect("/");
// 当返回false时,表示请求结束,后续的Interceptor和Controller都不会再执行
return false;
}
}
2.创建LoginInterceptorConfigurer拦截器的配置类并实现org.springframework.web.servlet.config.annotation.WebMvcConfigurer接口,配置类需要添加@Configruation注解修饰。
package com.ytx.springboot_jwt.interceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/** 注册处理器拦截器 */
@Configuration
public class LoginInterceptorConfigurer implements WebMvcConfigurer {
// 拦截器配置
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 创建拦截器对象
HandlerInterceptor interceptor = new LoginInterceptor();
// 白名单
List<String> patterns = new ArrayList<>();
patterns.add("/users/login");
patterns.add("/users/register");
patterns.add("/users/check_token");
// 通过注册工具添加拦截器
registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(patterns);
}
}
2.4 跨域解决方案
解决在前后端分离项目中的跨域问题。通过实现WebMvcConfigurer接口,并重写addCorsMappings(CorsRegistry registry)方法来实现。
package com.ytx.springboot_jwt.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/**
* addMapping("/**"):配置可以被跨域的路径,可以任意配置,可以具体到直接请求路径
* allowedOrigins("*"):允许所有的请求域名访问我们的跨域资源,可以固定单条或者多条内容,如"http://www.yx.com",只有该域名可以访问我们的跨域资源
* allowedHeaders("*"):允许所有的请求header访问,可以自定义设置任意请求头信息
* allowedMethods():允许所有的请求方法访问该跨域资源服务器,如GET、POST、DELETE、PUT、OPTIONS、HEAD等
* maxAge(3600):配置客户端可以缓存pre-flight请求的响应的时间(秒)。默认设置为1800秒(30分钟)
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")
.maxAge(3600);
}
}