JWT 简介
概念
JWT全称是:json web token。它将用户信息加密到 token 里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证 token 的正确性,只要正确即通过验证。
JWT 的认证流程
1、用户输入用户名和密码,发送给服务器,服务器验证账号密码成功
2、服务器使用签名秘钥生成jwt,把用户id放到jwt中
3、把令牌返给客户端
4、下次请求的时候就把令牌放在请求头里带上
5、服务器使用签名秘钥验证jwt是否有效
6、有效后可以从jwt中获取到用户id
优缺点
优点:
1、 简洁,可以通过 URL POST 参数或者在 HTTP header 发送,因为数据量小,传输速度也很快;
2、自包含,负载中可以包含用户所需要的信息,避免了多次查询数据库;
3、跨平台,因为 Token 是以 JSON 加密的形式保存在客户端的,所以 JWT 是跨语言的,原则上任何 web 形式都支持;
4、 存储在客户端,不需要再服务端保存会话信息,特别适用于分布式微服务;
缺点:
1、无法作废已经发布的令牌
2、不易应对数据过期
JWT 消息构成
一个 Token 分三部分,按顺序为
1、头部(header)
2、载荷(payload)
3、签证(signature)
三个部分之间用.分割。例如:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIzOTZlNWZlNS0yZmQyLTQ3MjItYjQzOS0zYzY4NzA1OGMwZjAiLCJleHAiOjE2NjgxNzEyMTJ9.JUMFKdhzu1w_ecwHrOkqjKjosy3TOnaTrj1oFekG9HE
header
JWT的头部承载两部分信息:
1、声明类型,这里是JWT
2、声明加密的算法,通常直接使用 HMAC SHA256
JWT里验证和签名使用的算法列表如下:
playload
载荷就是存放有效信息的地方。基本上填两种类型的数据
1、标准中注册的声明的数据;
2、自定义数据;
由这两部分内部做 base64 加密。
标准中注册的声明(建议但不强制使用):
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
自定义数据:存放我们想放在 token 中存放的 key-value 值。
signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
1、base64 加密后的 header
2、base64 加密后的 payload 连接组成的字符串
3、然后通过 header 中声明的加密方式进行加盐 secret 组合加密
然后就构成了JWT的第三部分。
SpringBoot 集成 JWT 实战
源代码地址:https://gitee.com/leo825/springboot-learning-parents.git
项目整体结构:
maven 依赖
<?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.example</groupId>
<artifactId>springboot-learning-parents</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>springboot-demo</groupId>
<artifactId>springboot-h2</artifactId>
<version>1.0-SNAPSHOT</version>
<name>springboot-jwt</name>
<url>https://gitee.com/leo825/springboot-learning-parents.git</url>
<description>springboot集成jwt测试</description>
<properties>
<start-class>com.demo.SpringbootJwtApplication</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatisplus.version>3.5.1</mybatisplus.version>
<freemaker.version>2.3.31</freemaker.version>
<mysql.version>8.0.28</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<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>
<!-- mybatis-plus 所需依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<!-- 使用h2内存数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- jwt依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
JwtUtil
package com.demo.util;
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.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
/**
* jwt工具类
*/
public class JwtUtil {
/**
* 过期时间5分钟
*/
private static final long EXPIRE_TIME = 5 * 60 * 1000;
/**
* jwt 密钥
*/
private static final String SECRET = "jwt_secret";
/**
* 生成签名,五分钟后过期
*
* @param userId
* @return
*/
public static String sign(String userId) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
// 将 userId 保存到 token 里面
.withAudience(userId)
// 五分钟后token过期
.withExpiresAt(date)
// token 的密钥
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
/**
* 根据 token 获取 userId
*
* @param token
* @return
*/
public static String getUserId(String token) {
try {
String userId = JWT.decode(token).getAudience().get(0);
return userId;
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 校验token
*
* @param token
* @return
*/
public static boolean checkSign(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// .withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
throw new RuntimeException("token 无效,请重新获取");
}
}
}
JwtToken
package com.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 加上该注解的接口需要登录才能访问
*/
@Target({
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtToken {
boolean required() default true;
}
JwtInterceptor
package com.demo.interceptor;
import com.demo.annotation.JwtToken;
import com.demo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* jwt拦截器
*/
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) {
// 从 http 请求头中取出 token
String token = httpServletRequest.getHeader("token");
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(JwtToken.class)) {
JwtToken jwtToken = method.getAnnotation(JwtToken.class);
if (jwtToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 userId
String userId = JwtUtil.getUserId(token);
log.info("用户id: {}", userId);
// 验证 token
JwtUtil.checkSign(token);
}
}
return true;
}
}
WebConfig
package com.demo.config;
import com.demo.interceptor.JwtInterceptor;
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;
/**
* 注册拦截器
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加jwt拦截器
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
// 拦截所有请求,通过判断是否有 @JwtToken 注解 决定是否需要登录
.addPathPatterns("/**");
}
/**
* jwt拦截器
*
* @return
*/
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}
JwtController
package com.demo.controller;
import com.alibaba.fastjson.JSONObject;
import com.demo.annotation.JwtToken;
import com.demo.util.JwtUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/jwt")
public class JwtController {
/**
* 登录并获取token
*
* @param userName
* @param passWord
* @return
*/
@PostMapping("/login")
public Object login(String userName, String passWord) {
JSONObject jsonObject = new JSONObject();
// 检验用户是否存在(为了简单,这里假设用户存在,并制造一个uuid假设为用户id)
String userId = UUID.randomUUID().toString();
// 生成签名
String token = JwtUtil.sign(userId);
Map<String, String> userInfo = new HashMap<>();
userInfo.put("userId", userId);
userInfo.put("userName", userName);
userInfo.put("passWord", passWord);
jsonObject.put("token", token);
jsonObject.put("user", userInfo);
return jsonObject;
}
/**
* 该接口需要带签名才能访问
*
* @return
*/
@JwtToken
@GetMapping("/getMessage")
public String getMessage() {
return "你已通过验证";
}
}
GlobalExceptionHandler
package com.demo.common;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@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("code", 500);
jsonObject.put("message", msg);
return jsonObject;
}
}
SpringbootJwtApplication
package com.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(value = "com.demo.mapper")
public class SpringbootJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootJwtApplication.class, args);
}
}
application.yml
#端口,项目上下文
server:
port: 8080
servlet:
context-path: /springboot-jwt
spring:
datasource:
username: leo825
password: 1WSX@357wj
# 如果需要数据本地化,则改成 file 方式
# jdbc:h2:file:D:/program/sqlite3/db/testDB;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1
url: jdbc:h2:mem:testDB;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
# 初始化表
schema: classpath:schema.sql
# 初始化数据
data: classpath:data.sql
initialization-mode: always
continue-on-error: true
# 开启这个配置就可以通过 web 页面访问了,例如:http://localhost:8080/springboot-h2/h2-console
h2:
console:
enabled: true
settings:
# 开启h2 console 跟踪 方便调试 默认 false
trace: true
# 允许console 远程访问 默认false
web-allow-others: true
# h2 访问路径上下文
path: /h2-console
# mybatis-plus 配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.dmo.entity
global-config:
#数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
field-strategy: NOT_NULL
#驼峰下划线转换
column-underline: true
logic-delete-value: -1
logic-not-delete-value: 0
banner: false
#原生配置
configuration:
# 打印sql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
cache-enabled: false
call-setters-on-nulls: true
jdbc-type-for-null: 'null'
# 日志输出配置
logging:
level:
root: INFO
org:
springframework:
security: WARN
web: ERROR
file:
path: ./logs
name: './logs/springboot-jwt.log'
pattern:
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'
测试
1、登录
2、验证jwt
3、验证jwt(无token失败)
转载:https://blog.csdn.net/u011047968/article/details/127813137