JWT(JSON Web Token)也就是通过 JSON 形式作为 Web 应用中的令牌,用于在各方之间安全地将信息作为 JSON 对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。常用于判断用户是否已经登录。
# 添加依赖
在 pom.xml
中添加 JWT 相关依赖。
<dependency> | |
<groupId>io.jsonwebtoken</groupId> | |
<artifactId>jjwt</artifactId> | |
<version>0.7.0</version> | |
</dependency> | |
<dependency> | |
<groupId>com.auth0</groupId> | |
<artifactId>java-jwt</artifactId> | |
<version>3.4.0</version> | |
</dependency> |
排雷:如果 ds 或者 gpt 让你加什么 Spring Security 依赖,不要加!会出现很多莫名其妙的问题!
# 关键类
共有三个类:Utils
包下的 JwtUtils
工具类Interceptor
包下的 JwtInterceptor
拦截类Config
包下的 InterceptConfig
配置类
# JwtUtils
以及前端处理 token 方式
用于生成 token、校验 token 以及获取 token 内信息。
话不多说直接丢代码:
package com.example.demo_mysql.Utils; | |
import com.auth0.jwt.JWT; | |
import com.auth0.jwt.JWTCreator; | |
import com.auth0.jwt.algorithms.Algorithm; | |
import com.auth0.jwt.interfaces.Claim; | |
import com.auth0.jwt.interfaces.DecodedJWT; | |
import java.util.Calendar; | |
import java.util.Map; | |
public class JwtUtils { | |
/** | |
* 密钥要自己保管好 | |
*/ | |
private static String SECRET = "!Q@W#E$R"; | |
/** | |
* 传入 payload 信息获取 token | |
* * @param map payload | |
* @return token | |
* */ | |
public static String getToken(Map<String, String> map) { | |
JWTCreator.Builder builder = JWT.create(); | |
//payload | |
map.forEach((k, v) -> { | |
builder.withClaim(k, v); | |
}); | |
Calendar instance = Calendar.getInstance(); | |
instance.add(Calendar.DATE, 3); // 默认 3 天过期 | |
builder.withExpiresAt(instance.getTime());// 指定令牌的过期时间 | |
return builder.sign(Algorithm.HMAC256(SECRET)); | |
} | |
/** | |
* 验证 token 合法性 | |
*/ | |
public static DecodedJWT verify(String token) { | |
// 如果有任何验证异常,此处都会抛出异常 | |
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token); | |
} | |
/** | |
* 获取 token 信息方法 | |
*/ | |
public static Map<String, Claim> getTokenInfo(String token) { | |
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token).getClaims(); | |
} | |
} |
对于生成 token 的 getToken 函数,传入的 map 数据用于写入 token,不推荐写入如 password 的敏感信息。
Controller 使用用例:
@GetMapping("/login") | |
public Map<String, Object> login(User user){ | |
log.info(user.getUsername()); | |
log.info(user.getPassword()); | |
Map<String, Object> map = new HashMap<>(); | |
try{ | |
User login = userService.login(user); | |
Map<String, String> payload = new HashMap<>(); | |
payload.put("id", String.valueOf(login.getId())); | |
payload.put("password", login.getPassword()); | |
// 生成 token | |
String token = JwtUtils.getToken(payload); | |
map.put("status", true); | |
map.put("msg", "登录成功"); | |
map.put("token", token); // 响应 token,存储在客户端 | |
}catch (Exception e) { | |
log.info("登录失败..."); | |
map.put("status", false); | |
map.put("msg", "登录失败"); | |
} | |
return map; | |
} |
# 前端处理 token
前端需要将获得的 token 存储在 localStorage
的 token
中:
localStorage.setItem('token', response.data.token); |
此外,前端在每次发送请求的时候都需要将 token 加在请求头中,这里我们加在标准的 Authorization
头中,此外还需要以 Bearer
为开头。我们以 axios 为例,创建 axiosInstance
的 axios 实例:
import axios from 'axios'; | |
// 创建 axios 实例 | |
const axiosInstance = axios.create({ | |
baseURL: 'http://127.0.0.1:8139/' // 本地调试 URL | |
}); | |
// 设置请求拦截器 | |
axiosInstance.interceptors.request.use( | |
config => { | |
// 获取 JWT token | |
const token = localStorage.getItem('token'); | |
if (token) { | |
// 将 token 添加到请求头部 | |
config.headers.Authorization = `Bearer ${token}`; | |
} | |
return config; | |
}, | |
error => { | |
return Promise.reject(error); | |
} | |
); | |
// 设置响应拦截器 | |
axiosInstance.interceptors.response.use( | |
response => { | |
return response; | |
}, | |
error => { | |
if (error.response && error.response.status === 400 || error.response.status === 401) { | |
// 如果响应状态码为 400 或 401,则跳转到登录页面 | |
localStorage.removeItem('token'); | |
window.location.href = `/login`; | |
alert('您还未登录。') | |
} | |
} | |
) | |
export default axiosInstance; |
# JwtInterceptor
JwtInterceptor
用于校验 token 是否合法,从而拦截不合法的请求。
package com.example.demo_mysql.Interceptor; | |
import com.auth0.jwt.interfaces.DecodedJWT; | |
import com.example.demo_mysql.Utils.JwtUtils; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import jakarta.servlet.http.HttpServletRequest; | |
import jakarta.servlet.http.HttpServletResponse; | |
import org.springframework.web.servlet.HandlerInterceptor; | |
import java.util.HashMap; | |
public class JwtInterceptor implements HandlerInterceptor { | |
@Override | |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | |
HashMap<String, String> map = new HashMap<>(); | |
// 从请求头中提取 Authorization 值 | |
String authHeader = request.getHeader("Authorization"); | |
// 检查 Authorization 头是否存在且格式合法 | |
if (authHeader == null || !authHeader.startsWith("Bearer ")) { | |
// 失败返回 401 错误码 | |
System.out.println("验证失败,头不存在"); | |
map.put("msg", "验证失败,头不存在"); | |
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 | |
sendJsonResponse(response, map); | |
return false; | |
} | |
// 提取 token(去除 "Bearer" 前缀) | |
String token = authHeader.substring(7); | |
try { | |
// 如果验证成功放行请求 | |
DecodedJWT verify = JwtUtils.verify(token); | |
return true; | |
} catch (Exception exception) { | |
// 失败返回 401 错误码 | |
System.out.println("验证失败:" + exception); | |
map.put("msg", "验证失败:" + exception); | |
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | |
sendJsonResponse(response, map); | |
return false; | |
} | |
} | |
// 封装发送 JSON 响应的逻辑 | |
private void sendJsonResponse( | |
HttpServletResponse response, | |
HashMap<String, String> map | |
) throws Exception { | |
response.setContentType("application/json;charset=UTF-8"); // 修正 Content-Type | |
String json = new ObjectMapper().writeValueAsString(map); | |
response.getWriter().write(json); // 写入 JSON 数据 | |
response.getWriter().flush(); // 确保数据发送到客户端 | |
} | |
} |
# InterceptConfig
这个类用于注册拦截器,并规定哪些路径需要放行或拦截。
package com.example.demo_mysql.Config; | |
import com.example.demo_mysql.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 InterceptConfig implements WebMvcConfigurer { | |
@Override | |
public void addInterceptors(InterceptorRegistry registry) { | |
// 添加拦截器 | |
registry.addInterceptor(new JwtInterceptor()) | |
// 拦截的路径即需要进行 token 验证的路径。这里配置全部拦截 | |
.addPathPatterns("/**") | |
// 放行的路径 | |
.excludePathPatterns("/login", "/user_create"); | |
} | |
} |
路径拦截与放行遵循「白名单优先拦截」规则,即如果一个路径既不在放行也不在拦截里,那么默认按放行处理。所以这里我们先将所有路径全部拦截,再单独放行登录与注册两条路径。
运行测试,发现能够顺利运行和拦截。