JWT简介
JSON Web Token(JWT)是一个非常轻巧的规范,这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。一个 JWT 实际上就是一个字符串,它由三部分组成,头部、载荷与签名。前两部分需要经过 Base64 编码,后一部分通过前两部分 Base64 编码后再加密而成。
JWT组成:Header + Payload + Signature
Header:头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等,如{"type":"JWT","alg":"HS256"},Base64 加密header后的字符串为(JWT官网JSON Web Tokens - jwt.io 可以验证):eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload:一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。如:{"id":"1","name":"张三","sex":"male"},Base64 加密Payload后的字符串为(JWT官网 可以验证):eyJpZCI6IjEiLCJuYW1lIjoi5byg5LiJIiwic2V4IjoibWFsZSJ9
Signature:这个部分需要 Base64 加密后的 header 和 Base64 加密后的 payload 使用 “.” 连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 salt组合加密,然后就构成了 jwt 的第三部分。如:salt设置为abc,Signature字符串为:mZKsezNd5e5Q0Gi4vdeyEH3-ilxG_qEHkZp0gn7ayr0
综上,公式如下:
Token = Base64(Header).Base64(Payload).Base64(Signature)
Signature = Header指定的签名算法(Base64(header).Base64(payload),秘钥)
生成后的token信息为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoi5byg5LiJIiwic2V4IjoibWFsZSJ9.mZKsezNd5e5Q0Gi4vdeyEH3-ilxG_qEHkZp0gn7ayr0
JWT应用
在分布式环境下,有状态(含session)可以采用分布式session解决方案,参考:分布式session解决方案-CSDN博客,无状态token+redis也可以实现分布式token验证校验,spring-session和token+redis 都借助了redis,如果实际项目不采用redis中间件,可以用JWT实现上述需求,架构示意图如下:
项目中使用JWT步骤如下:
1、添加项目依赖:java-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>
<groupId>com.gingko</groupId>
<artifactId>jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwt</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.5.0</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.gingko.jwt.JwtApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2、编写配置
# 应用服务 WEB 访问端口
server:
port: 8080
#jwt key
jwt:
key: gingko
3、代码实现加密token,解密token。登录成功后,将加密后的token返回到前台,前台放入本地缓存,请求后台时,将token放入request header中,后台通过拦截器实现token的校验进而验证用户是否登录,代码如下:
后台请求UserController:
package com.gingko.jwt.controller;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.gingko.jwt.common.GenericWebResult;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
@RestController
@RequestMapping("/user")
public class UserController {
@Value("${jwt.key}")
private String jwtKey;
@RequestMapping("/login")
public GenericWebResult login(@RequestParam String userName,
@RequestParam String password) {
//到数据库校验通过
Algorithm algorithm = Algorithm.HMAC256(jwtKey);
String token = JWT.create()
.withClaim("userName",userName)
.withClaim("userId","001")
//token半小时过期
.withExpiresAt(new Date(System.currentTimeMillis() + 1800000))
.sign(algorithm);
return GenericWebResult.ok("登录成功",token);
}
@RequestMapping("/getInfo")
public GenericWebResult getInfo(@RequestAttribute String userId) {
return GenericWebResult.ok("获取信息成功 ",userId);
}
}
登录拦截器LoginInterceptor:
package com.gingko.jwt.interceptor;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.gingko.jwt.common.GenericWebResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登录拦截器
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Value("${jwt.key}")
private String jwtKey;
//登录之前拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
log.info("登录校验开始,token:{}", token);
if (token == null || token.isEmpty()) {
log.info("token为空,请求被拦截");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
GenericWebResult genericWebResult = GenericWebResult.error("token为空,请求被拦截");
String resultStr = JSON.toJSONString(genericWebResult);
response.getWriter().write(resultStr);
return false;
}
try {
Algorithm algorithm = Algorithm.HMAC256(jwtKey);
JWTVerifier verifier = JWT.require(algorithm)
.build();
//验证token
DecodedJWT decodedJWT = verifier.verify(token);
//将用户id设置到request中
request.setAttribute("userId",decodedJWT.getClaim("userId").asString());
} catch (JWTVerificationException exception) {
log.warn("token无效,请求被拦截");
GenericWebResult genericWebResult = GenericWebResult.error("token无效,请求被拦截");
String resultStr = JSON.toJSONString(genericWebResult);
response.getWriter().write(resultStr);
return false;
}
return true;
}
}
配置类WebMvcConfig及统一返回值GenericWebResult
package com.gingko.jwt.config;
import com.gingko.jwt.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* web配置类,配置拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/user/*")
.excludePathPatterns("/user/login");//登录不用拦截
}
}
package com.gingko.jwt.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* 向前台返回的统一格式的结果
*/
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class GenericWebResult {
/**
* 是否成功
*/
private boolean successFlag;
/**
* 成功或失败返回的信息
*/
private String msg;
/**
* /**
* 返回列表或树形数据,用户可以自由添加
*/
private Object property;
/**
* 数据总条数,分页情况下使用
*/
private Long total;
public static GenericWebResult ok() {
return new GenericWebResult(true, null, null, null);
}
public static GenericWebResult ok(String msg) {
return new GenericWebResult(true, msg, null, null);
}
public static GenericWebResult ok(Object property) {
return new GenericWebResult(true, null, property, null);
}
public static GenericWebResult ok(String msg, Object property) {
return new GenericWebResult(true, msg, property, null);
}
public static GenericWebResult ok(Object property, Long total) {
return new GenericWebResult(true, null, property, total);
}
public static GenericWebResult ok(String msg, Object property, Long total) {
return new GenericWebResult(true, msg, property, total);
}
public static GenericWebResult error(String msg) {
return new GenericWebResult(false, msg, null, null);
}
public static GenericWebResult error(String msg, Object data) {
return new GenericWebResult(false, msg, data, null);
}
}
4、后台启动2个服务,端口分别是8080和8082,通过8080服务登录,8082服务验证token的有效性,篡改token后验证token无效,符合预期。