1、JWT 简介
JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以进行验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名
2、应用场景
- 授权:这是使用JWT最常见的方案。当用户登录后,每个后续请求将会在header带上JWT,允许用户访问允许使用该令牌的路由、服务和资源。单点登录是当今广泛使用JWT的一个特性,因为它具有较小的开销和易于跨不同域使用的能力。
- 信息交换:JWT是保证各方之间安全地传输信息的一种好方法。因为JWT可以被签名,例如使用公钥/私钥对,可以确保发件人是他们所说的人。另外,由于使用header和playload计算签名,还可以验证内容是否被篡改。
3、Jwt结构
JSON Web令牌以紧凑的形式由三部分组成,这些部分由点(.
)分隔,分别是:
- 标头
- 有效载荷
- 签名 因此,JWT通常如下所示:
xxxxx.yyyyy.zzzzz
4、Jwt工作流程
- 用户使用账号登录发出post请求;
- 服务器使用私钥创建一个jwt;
- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串放在请求头中,向服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器。
5、SpringBoot 集成 Jwt
- 工作目录介绍
- pom配置文件
<?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.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>loginintercept</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>loginintercept</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- JwtUtils 工具类
package com.example.loginintercept.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import org.springframework.stereotype.Component;
/**
* token 工具类
*/
@Component
public class JwtUtils {
// 过期时间
private static long expire = 604800;
// 秘钥
private static String secret = "HSyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
/**
* 创建一个token
*
* @param userId
* @return
*/
public String generateToken(String userId) {
Date now = new Date();
Date expireDate = new Date(now.getTime() expire);
return Jwts.builder().setHeaderParam("type", "JWT").setSubject(userId).setIssuedAt(now)
.setExpiration(expireDate).signWith(
SignatureAlgorithm.HS512, secret).compact();
}
/**
* 解析token
*/
public Claims getClaimsByToken(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
System.out.println("validate is token error");
return null;
}
}
/**
* 判断 token 是否过期
*/
public boolean isTokenExpired(Date expiration){
return expiration.before(new Date());
}
}
- ResultT 统一返回格式
package com.example.loginintercept.config;
import java.util.HashMap;
import java.util.Map;
public class ResultT extends HashMap<String, Object> {
public ResultT() {
put("code", 0);
put("msg", "success");
}
public static ResultT ok() {
ResultT t = new ResultT();
t.put("msg", "操作成功");
return t;
}
public static ResultT ok(String msg) {
ResultT t = new ResultT();
t.put("msg", msg);
return t;
}
public static ResultT ok(Map<String, Object> map) {
ResultT t = new ResultT();
t.putAll(map);
return t;
}
public static ResultT error(int code, String msg) {
ResultT t = new ResultT();
t.put("code", code);
t.put("msg", msg);
return t;
}
public static ResultT error() {
return error(500, "未知异常");
}
public ResultT put(String key, Object value){
super.put(key, value);
return this;
}
}
- TokenInterceptor 创建一个token拦截器
package com.example.loginintercept.config;
import com.example.loginintercept.exception.TokenRuntimeException;
import io.jsonwebtoken.Claims;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* 创建一个 token 拦截器.
* 需要继承 HandlerInterceptorAdapter,并且声明为spring的组件
*/
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
// 注入jwt工具类
@Autowired
private JwtUtils jwtUtils;
// 重写 前置拦截方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 1、从请求头中获取token
String token = request.getHeader("token");
// 2、判断 token 是否存在
if (token == null ||"".equals(token)) {
System.out.println("未登录");
// 这里可以自定义 抛出 token 异常
throw new TokenRuntimeException("未登录");
}
// 3、解析token
Claims claim = jwtUtils.getClaimsByToken(token);
if (null == claim) {
System.out.println("token 解析错误");
// 这里可以自定义 抛出 token 异常
throw new TokenRuntimeException("token 解析错误");
}
// 4、判断 token 是否过期
Date expiration = claim.getExpiration();
boolean tokenExpired = jwtUtils.isTokenExpired(expiration);
if (tokenExpired) {
System.out.println("token已过期,请重新登录");
// 这里可以自定义 抛出 token 异常
throw new TokenRuntimeException("token已过期,请重新登录");
}
// 5、 从 token 中获取员工信息
String subject = claim.getSubject();
// 6、去数据库中匹配 id 是否存在 (这里直接写死了)
if (null == subject) {
System.out.println("员工不存在");
// 这里可以自定义 抛出 token 异常
throw new TokenRuntimeException("员工不存在");
}
// 7、成功后 设置想设置的属性,比如员工姓名
request.setAttribute("userId", subject);
request.setAttribute("userName", "张三");
return true;
}
}
- WebMvcConfig 设置拦截器
package com.example.loginintercept.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 设置拦截器.
* 打上configuration 注解,标注为配置项
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// 注入 token 拦截器
@Autowired
private TokenInterceptor interceptor;
/**
* 重写添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加自定义拦截器,并拦截对应 url
registry.addInterceptor(interceptor).addPathPatterns("/gateway/**");
}
}
- GatewayController 模仿需要登录后才能访问的资源
package com.example.loginintercept.controller;
import com.example.loginintercept.config.ResultT;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* controller
*/
@RestController
@RequestMapping("/gateway")
public class GatewayController {
@RequestMapping(value = "/find",method = RequestMethod.POST)
public ResultT find(){
return ResultT.ok("find one success");
}
}
- LoginController 登录
package com.example.loginintercept.controller;
import com.example.loginintercept.config.JwtUtils;
import com.example.loginintercept.config.ResultT;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录
*/
@RestController
public class LoginController {
// 注入jwt工具类
@Autowired
private JwtUtils jwtUtils;
@RequestMapping(value = "/login", method = RequestMethod.POST)
public ResultT login(String name, String psw) {
String userId = "132";
String token = jwtUtils.generateToken(userId);
return ResultT.ok().put("token", token);
}
}
- SysRuntimeExceptionHandler 全局异常处理
package com.example.loginintercept.exception;
import com.example.loginintercept.config.ResultT;
import com.example.loginintercept.exception.TokenRuntimeException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*/
@RestControllerAdvice
public class SysRuntimeExceptionHandler {
@ExceptionHandler(TokenRuntimeException.class)
public ResultT tokenRuntimeException(TokenRuntimeException e) {
e.printStackTrace();
return ResultT.error(e.getCode(), e.getMsg());
}
@ExceptionHandler(Exception.class)
public ResultT handlerException(Exception e){
e.printStackTrace();
return ResultT.error();
}
}
- TokenRuntimeException 自定义 token 异常
package com.example.loginintercept.exception;
import lombok.Data;
/**
* 自定义 token 异常
*/
@Data
public class TokenRuntimeException extends RuntimeException{
private Integer code = 401;
private String msg;
public TokenRuntimeException(String msg) {
this.msg = msg;
}
}
6、运行测试
- 首先访问需要 token 的资源
- 登录 获取 token
- 请求头中存放 token 再次访问需要 token 的资源
当然,一般在实际开发中请求头会叫Authorization
而 token
内容的前面通常会拼接上 `’Bearer ‘ 这里为了方便,就不这样做了
好,完全!