一. JWT简介
Json Web Token,将用户数据通过json格式在client与server之间传输,并在传输过程中可加密、签名。
类似session保持登录状态。
原理:
用户使用"用户名/密码"请求服务器—>服务器验证"用户名/密码"—>服务器通过验证后,生成jwt(本质上就是json字符串)的header、payload、signature,并将jwt发送给用户—>客户端接收服务器返回的jwt,可存储在 Cookie 或 localStorage —>客户端每次与服务器通信,都要带上这个jwt—>服务端验证JWT的有效性后,返回数据。
应用场景:用户认证,尤其是单点登录(SSO,Single Sign On,在多个应用中,用户只需要登录一次,就可以访问所有相互信任的应用)。
二. JWT的结构
-
Header
:{ "alg": "HS256" "typ": "JWT" }
alg
:为签名的算法,如RSA
或HS256
。typ
:令牌的类型,如JWT
。
-
payload
:要传输的数据,如用户数据,不建议包含敏感信息。
{ "id": "1", "name": "tom" }
-
signature
(签名):使用 header 和 payload 的base64编码,外加一个指定的 secret ,再利用 header 中指定的加密算法生成的字符串。
作用:用于校验后端接收到的jwt是否有效。
三. 传统Session与JWT的对比
传统Session:
- 优点:易于实现。
- 缺点:
- 在服务器中保存用户信息,当访问量多时,服务器内存压力大。
- 不适合单点登录。
JWT:
-
优点:
- 以json加密形式保存在客户端,不占用服务端内存。
- 适合移动端应用,如微信小程序等,没有 cookie ,但有 localStorage 。
- 适合单点登录。
- jwt的 payload 中可以存储一些常用信息,降低服务器查询数据库的次数。
-
缺点:
-
不利于用户修改密码:
假设号被盗了,用户修改密码之后,盗号者在原 jwt 有效期内仍可以继续访问系统。
解决:
- 减少 jwt 的有效时间。
- 将secret设计成和用户相关的属性,而不是所有用户公用的统一值,在用户注销或修改密码后,若 jwt 没有变化,由于 secret 不存在或改变,则无法完成校验。
-
无法满足token续签的场景:
传统的 cookie 续签方案一般都是开发框架自带的,如 session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。但是 jwt 本身的 payload 中也有一个
exp
过期时间参数,一旦 exp 修改,整个 jwt 串就变了( signature 是通过 payload + header + secret 生成的),所以jwt 不支持续签。解决:
- 在服务端生成新 jwt ,再写回客户端。
-
四. 实现
1. 添加依赖
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
2. 创建JWTUtils
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtils {
private static String secret = "hello";
// 生成token
public static String getToken(Map<String, String> map) {
JWTCreator.Builder builder = JWT.create();
// payload
map.forEach(builder::withClaim);
// Calendar instance = Calendar.getInstance();
// instance.add(Calendar.DATE, 7);
// // 指定令牌的过期时间,默认为7天
// builder.withExpiresAt(instance.getTime());
// signature,返回 jwt/token
return builder.sign(Algorithm.HMAC256(secret));
}
// 验证jwt/token,若数据被修改过或过期都会抛出异常
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}
}
3. JWT校验
package org.example.interceptor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.util.JWTUtils;
import org.example.vo.ResponseResult;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的令牌
String token = request.getHeader("token");
System.out.println("拦截器:token:" + token);
ResponseResult rs = new ResponseResult();
try {
JWTUtils.verify(token);
return true;
} catch (SignatureVerificationException e) {
rs.setMsg("签名不一致");
} catch (TokenExpiredException e) {
rs.setMsg("令牌过期");
} catch (AlgorithmMismatchException e) {
rs.setMsg("算法不匹配");
} catch (InvalidClaimException e) {
rs.setMsg("失效的payload");
} catch (Exception e) {
rs.setMsg("token无效");
}
rs.setCode(401);
// 响应到前端
String json = new ObjectMapper().writeValueAsString(rs);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
4. 应用
代码:
jwt/pom.xml:
<?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.5.2</version>
<relativePath/>
</parent>
<groupId>org.example</groupId>
<artifactId>jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<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>3.10.3</version>
</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>
jwt/src/main/java/org/example/util/JWTUtils.java:
package org.example.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtils {
private static String secret = "hello";
// 生成token
public static String getToken(Map<String, String> map) {
JWTCreator.Builder builder = JWT.create();
// payload
map.forEach(builder::withClaim);
// Calendar instance = Calendar.getInstance();
// instance.add(Calendar.DATE, 7);
// // 指定令牌的过期时间,默认为7天
// builder.withExpiresAt(instance.getTime());
// signature,返回 jwt/token
return builder.sign(Algorithm.HMAC256(secret));
}
// 验证jwt/token,若数据被修改过或过期都会抛出异常
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}
}
jwt/src/main/java/org/example/interceptor/JWTInterceptor.java:
package org.example.interceptor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.util.JWTUtils;
import org.example.vo.ResponseResult;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的令牌
String token = request.getHeader("token");
System.out.println("拦截器:token:" + token);
ResponseResult rs = new ResponseResult();
try {
JWTUtils.verify(token);
return true;
} catch (SignatureVerificationException e) {
rs.setMsg("签名不一致");
} catch (TokenExpiredException e) {
rs.setMsg("令牌过期");
} catch (AlgorithmMismatchException e) {
rs.setMsg("算法不匹配");
} catch (InvalidClaimException e) {
rs.setMsg("失效的payload");
} catch (Exception e) {
rs.setMsg("token无效");
}
rs.setCode(401);
// 响应到前端
String json = new ObjectMapper().writeValueAsString(rs);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
jwt/src/main/java/org/example/config/CustomMvcConfig.java:
package org.example.config;
import org.example.interceptor.JWTInterceptor;
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 CustomMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test")
.excludePathPatterns("/user/login");
}
}
jwt/src/main/java/org/example/pojo/User.java:
package org.example.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
private Integer id;
private String username;
private String password;
}
jwt/src/main/java/org/example/controller/UserController.java:
package org.example.controller;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.example.pojo.User;
import org.example.service.UserService;
import org.example.util.JWTUtils;
import org.example.vo.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user) {
try {
User userDB = userService.login(user);
Map<String, String> payload = new HashMap<>();
payload.put("id", userDB.getId().toString());
payload.put("username", userDB.getUsername());
return ResponseResult.success(JWTUtils.getToken(payload));
} catch (Exception e) {
e.printStackTrace();
}
return ResponseResult.fail(401, "jwt校验失败");
}
@PostMapping("/user/test")
public ResponseResult test(HttpServletRequest request) {
String token = request.getHeader("token");
System.out.println(token);
DecodedJWT verify = JWTUtils.verify(token);
String id = verify.getClaim("id").asString();
String username = verify.getClaim("username").asString();
log.info("用户id:{}", id);
log.info("用户名:{}", username);
return ResponseResult.success();
}
}
jwt/src/main/java/org/example/service/UserService.java:
package org.example.service;
import org.example.pojo.User;
public interface UserService {
User login(User user);
}
jwt/src/main/java/org/example/service/impl/UserServiceImpl.java:
package org.example.service.impl;
import org.example.pojo.User;
import org.example.service.UserService;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Override
public User login(User user) {
// 模拟数据库查询
List<User> users = Arrays.asList(
new User(1, "a1", "123"),
new User(2, "a2", "456")
);
for (User u : users) {
if (u.getUsername().equals(user.getUsername())
&& u.getPassword().equals(user.getPassword())
) {
return u;
}
}
throw new RuntimeException("认证失败");
}
}
jwt/src/main/java/org/example/vo/ResponseResult.java:
package org.example.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult {
private int code;
private String msg;
private Object rs;
private String token; // jwt
public static ResponseResult success() {
return new ResponseResult(
200,
"success",
null,
null
);
}
public static ResponseResult success(String token) {
return new ResponseResult(
200,
"success",
null,
token
);
}
public static ResponseResult success(Object rs, String token) {
return new ResponseResult(
200,
"success",
rs,
token
);
}
public static ResponseResult fail(Integer code, String msg) {
return new ResponseResult(
code,
msg,
null,
null
);
}
}
jwt/src/main/resources/public/login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="jquery.min.js"></script>
<script>
let login = () => {
$.ajax({
url: '/user/login',
type: 'post',
data: JSON.stringify({
username: $('input[name=username]').val(),
password: $('input[name=password]').val()
}),
dataType: 'json',
contentType: 'application/json;charset=UTF-8',
headers: {
token: localStorage.getItem('token')
},
success(res) {
localStorage.setItem('token', res.token)
location.href = "success.html"
}
})
}
</script>
</head>
<body>
<input name="username" placeholder="用户名"/> <br/>
<input name="password" placeholder="密码"/> <br/>
<input type="button" onclick="login()" value="登录"/>
</body>
</html>
jwt/src/main/resources/public/success.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="jquery.min.js"></script>
<script>
let f = () => {
$.ajax({
url: '/user/test',
type: 'post',
dataType: "json",
headers: {
token: localStorage.getItem('token')
},
success(res) {
alert(res.msg)
}
})
}
</script>
</head>
<body>
<input type="button" onclick="f()" value="测试"/>
</body>
</html>
jwt执行结果:
自行测试。