一、token简介
1、什么是token
在 Spring Boot 的接口中,Token 通常指的是用于接口访问的 身份验证和授权凭证。
它是一段字符串,客户端(前端或其他系统)在调用后端 API 时将这个 Token 携带过去,后端用它来判断:
-
你是谁(身份认证)
-
你能做什么(权限控制)
-
是否过期(安全性)
2、为什么要使用token
在 Token 出现之前,很多系统使用的是传统的 会话管理方式(Session + Cookie),模式大致是这样:
- 用户登录时,后端验证用户名/密码,在服务器上存一个 Session(会话信息) 。
- 服务器把一个 Session ID 通过 Cookie 发给客户端。
- 客户端访问时把这个 Session ID 带上,服务器查自己的 Session 存储,看这个用户是谁。
这种方式的缺点:
- 要在服务器保存会话信息(状态),占用内存或数据库空间,清理和管理也比较复杂。
- 不方便分布式部署:如果后端有多台服务器,需要共享 Session 数据,不然会出现“登录了但访问另一台服务器还要重新登录”的情况。
- 主要依赖 Cookie,对于移动端、跨域调用会比较麻烦(移动端没有浏览器帮你“自动存 Cookie 再自动发送”;同源策略下,浏览器只会自动发送同源域名的 Cookie,跨域是不会带上Cookie的)。
使用token之后,可以解决上述问题,带来的优点如下:
1. 无状态
- 服务器不需要保存每个用户的会话数据,直接通过 验证 Token 是否有效 来判断身份。
- 因为不依赖服务器状态,很容易实现 多台服务器的负载均衡。
2. 跨平台 & 跨域方便
- Token 可以放在请求头(Authorization),不依赖 Cookie,不依赖浏览器。
- 无论是网页、手机 APP、桌面软件甚至其他后端系统,都能统一用 Token 来认证。
3. 安全可控
-
Token 通常是加密签名的,不能随意伪造。
-
可以设置过期时间,防止长期有效带来的风险。
-
有些 Token 还能携带用户角色、权限等信息,方便后端直接判断。
3、token由谁生产
通常情况下,由后端服务器生产
- 用户在客户端(前端、APP等)输入账号密码登录
- 客户端把用户名+密码发给后端(走 HTTPS)
- 后端验证正确后,用自己掌握的 密钥(secret key) 把用户信息和过期时间等内容加密/签名,生成 Token
- 把 Token 返回给客户端
- 客户端此后每次请求接口,就把这个 Token 带上(一般放在 HTTP Header 的 Authorization 中)
- 验证 Token 时,后端用 相同的密钥 去解密/重新计算签名,验证是否一致
-
-
解析 Token
后端拿到 Token,先按规则拆成 Header、Payload、Signature 三段。 -
重新计算签名
用同样的算法,把解析出来的 Header & Payload 再用密钥加密生成一个新的签名。 -
对比签名
- 如果新签名和 Token 里带的 Signature 完全一致 → Token 没被篡改(Payload没改)
- 如果不一致 → Token内容被修改过(比如 userId 或 exp 被人为改成别的),直接拒绝。
-
-
-
检查过期时间
从 Payload 里读exp(过期时间),对比当前时间,若已过期 → 拒绝。
-
二、token方案
1、传递方式
目前token的三大传递方式就是 Header vs Query vs Cookie,基于前文的讨论已经看到了cookie的不适配性,故此处不再赘述。
关于 Query 传递,其实就是 URL 参数传递,把 Token 放在 URL 里,缺点显而易见,就是
-
- URL 长度限制问题
- 安全性差(容易被日志、浏览器历史记录、Referer 泄露)
故目前最流行的传递方案还是使用 http header 的 Authorization头。
2、token格式
| 方案 | 类型/特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| JWT(JSON Web Token) | 无状态、可携带声明 | 解析快;跨语言支持好 | 体积大;可暴露数据;撤销困难 | Web API、跨域授权 |
| Opaque Token | 纯随机、不含信息 | 安全、易控制失效 | 验证需查数据库 | OAuth 2.0、企业内部API |
| Paseto | JWT安全替代 | 安全默认值、无算法坑 | 社区较小 | 高安全要求系统 |
| MAC Token | 消息签名验证 | 性能好、简单 | 只能验证完整性、不能携带信息 | 内部服务调用 |
| Bearer+Refresh | 双 Token 机制 | 降低泄露风险 | 多带一次刷新逻辑 | 移动应用、长时会话 |
3、协议
| 协议 | 类型/特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| OAuth 2.0 / 2.1 | 业界最主流授权协议,支持多种授权模式,颁发 Access/Refresh Token | 标准化、生态成熟、跨平台;支持多级授权和细粒度权限 | 在简单场景显得复杂;需要额外的授权服务器实现 | 对外 API、第三方接入、移动端/PC授权、SaaS |
| OpenID Connect (OIDC) | OAuth 2.0 扩展,提供身份认证功能 | 不仅授权,还能返回用户身份信息(Profile、Email 等);兼容 OAuth 生态 | 增加实现复杂度 | 需要“登录+授权”一体的系统,如单点登录、用户中心 |
| SAML 2.0 | 基于 XML 的联邦身份认证协议 | 成熟稳定,适合跨组织/企业的单点登录 | XML冗长、解析复杂;前后端分离适配麻烦 | 传统企业与合作方的 SSO(比如企业门户对接) |
| 自定义 Token 校验 | 不依赖协议,登录直接颁发 Token(JWT/Opaque),后续校验签名或存储 | 实现简单、性能好,适合内网 | 不够标准化,扩展对外授权需要重构 | 内网系统、低安全要求接口 |
| Kerberos | 基于对称加密的网络身份认证协议 | 双向认证、防重放攻击、广泛用于Windows域环境 | 依赖时间同步;部署较复杂 | 企业内 Windows 域、Hadoop 集群安全认证 |
4、方案选择
本系统位于公司内网,仅供内部用户与内部系统调用,主要安全威胁来自误调用或非授权的内部脚本,而非外部攻击。鉴于此,我们选择基于 JWT 格式的 Token,并结合自定义的轻量级认证协议实现访问控制。
该方案在 Spring Boot 环境中开发与集成非常简单,无需引入完整的 OAuth2 流程即可满足当前安全合规要求。同时,设计上参考了标准协议的核心思路(Access Token 与过期机制),为后续迁移到更复杂或对外开放的认证框架留出了扩展空间。
三、代码开发
1、自定义 JWT 工具类,实现自定义认证协议
public class JwtParserUtil {
private static final byte[] SECRET_KEY = "ai-data".getBytes(StandardCharsets.UTF_8);
private static final Logger log = LoggerFactory.getLogger(JwtParserUtil.class);
private static final Cache<String, String> tokenCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(30, TimeUnit.MINUTES).build();
public static boolean verifyToken(String token) {
JWTValidator jwtValidator;
try {
jwtValidator = JWTValidator.of(token);
} catch (JSONException e) {
throw new ServiceException("token格式异常");
}
try {
//1,验证算法类型和签名
jwtValidator.validateAlgorithm(JWTSignerUtil.hs256(SECRET_KEY));
} catch (JWTException | ValidateException e) {
log.error("jwt-签名验证异常:{}", e.getMessage());
throw new ServiceException("token认证失败");
}
try {
// 2. 使用 UTC 时间进行验证
jwtValidator.validateDate(DateTime.now());
} catch (ValidateException e) {
log.error("jwt-时间验证异常:{}", e.getMessage());
throw new ServiceException("token失效");
}
return false;
}
public static String parseUsername(String token) {
try {
// 基础格式校验
if (org.apache.commons.lang.StringUtils.isBlank(token) || token.split("\.").length != 3) {
throw new ServiceException("Token格式异常");
}
// 解析前先验证签名有效性
if (verifyToken(token)) {
throw new ServiceException("Token签名验证失败");
}
// 详细解析Token内容
JWT jwt = JWT.of(token);
Map<String, Object> payloads = jwt.getPayloads();
// 获取用户名字段
Object usernameObj = payloads.get("username");
if (usernameObj == null) {
throw new ServiceException("Token缺少用户信息");
}
// 类型安全校验
if (!(usernameObj instanceof String)) {
throw new ServiceException("用户名字段类型错误");
}
return (String) usernameObj;
} catch (JWTException e) {
log.error("JWT解析异常: {}", e.getMessage());
throw new ServiceException("Token解析失败");
}
}
public static Map<String, Object> parseUser(String token) {
// 基础格式校验
if (StringUtils.isBlank(token) || token.split("\.").length != 3) {
throw new ServiceException("Token格式异常");
}
// 解析前先验证签名有效性
if (verifyToken(token)) {
throw new ServiceException("Token签名验证失败");
}
// 详细解析Token内容
JWT jwt = JWT.of(token);
return jwt.getPayloads();
}
public static UserVo convertUser(String token) {
Map<String, Object> userMap = parseUser(token);
UserVo user = new UserVo();
user.setUserId(Convert.toInt(userMap.get("userId")));
user.setUserName(Convert.toStr(userMap.get("username")));
return user;
}
public static String parseUsernameWithCache(String token) {
try {
return tokenCache.get(token, () -> parseUsername(token));
} catch (ExecutionException e) {
throw new ServiceException("用户信息获取失败");
}
}
}
2、编写拦截器
@Slf4j
@Component
public class WebInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
String token = request.getHeader("token");
String requestURI = request.getRequestURI();
if (StringUtils.isEmpty(token)) {
log.warn("用户访问token不能为空,当前访问路径:{}", requestURI);
return false;
}
return !JwtParserUtil.verifyToken(token);
}
}
3、注册拦截器,实现黑/白名单过滤
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册loginInterceptor拦截器
InterceptorRegistration registration = registry.addInterceptor(new WebInterceptor());
//添加拦截路径
registration.addPathPatterns("/logs/**", "/metrics/**");
}
@Bean
public TimeoutCallableProcessingInterceptor timeoutInterceptor() {
return new TimeoutCallableProcessingInterceptor();
}
/**
* 添加跨域问题
*
* @param registry 拦截器注册表
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//设置允许跨域请求的域名
//是否允许证书(cookies)
.allowCredentials(true)
.allowedOriginPatterns("*")
.allowedHeaders("*")
//设置请求的方式
.allowedMethods("*")
.maxAge(3600);
}
}