0赞
赏
赞赏
更多好文
核心价值:
本文不止讲理论!提供 3套独立可运行代码 + 融合架构设计图 + 安全避坑清单,覆盖企业级认证全场景。
所有代码经 Spring Boot 3.3 + JDK 21 验证,开箱即用,拒绝“伪代码”。
🌐 一、为什么你需要这三把“钥匙”?
| 场景 | 痛点 | 解决方案 | 本文价值 |
|---|---|---|---|
| 移动端/API | Token过期频繁弹登录框 | 双Token无感刷新 | ✅ 附防刷/吊销完整实现 |
| 企业多系统 | 每个系统重复登录 | SSO单点登录 | ✅ CAS协议+Spring Security深度集成 |
| 开放平台 | 用户不愿注册新账号 | 第三方授权登录 | ✅ GitHub/微信双示例+权限映射 |
💡 关键认知:
三者非互斥!现代系统常 “双Token做主干 + SSO打通内网 + OAuth2开放生态”
本文最后揭秘 “三位一体”融合架构(附流程图)
🔑 二、双Token认证:告别“登录框恐惧症”(附防刷策略)
🌟 为什么双Token > 单Token?
sequenceDiagram
participant C as 客户端
participant S as 服务端
C->>S: 登录(账号密码)
S-->>C: Access Token(15min) + Refresh Token(7天)
loop 正常访问
C->>S: 带Access Token请求
S-->>C: 返回业务数据
end
C->>S: Access Token过期
S-->>C: 401 Unauthorized
C->>S: 用Refresh Token换新Token
S-->>C: 新Access Token(无需用户操作)
💻 核心代码(Spring Boot 3.3 + JWT + Redis)
// 1. Token生成器(含双Token策略)
@Component
public class TokenProvider {
private final JwtEncoder jwtEncoder;
private final RedisTemplate<String, String> redisTemplate; // 存储Refresh Token
// 生成双Token
public TokenPair generateTokens(UserDetails user) {
// Access Token (短时效)
String accessToken = jwtEncoder.encode(JwtClaimsSet.builder()
.subject(user.getUsername())
.claim("type", "access")
.expiresAt(Instant.now().plus(15, ChronoUnit.MINUTES))
.build()).getTokenValue();
// Refresh Token (长时效+唯一标识)
String refreshTokenId = UUID.randomUUID().toString();
String refreshToken = jwtEncoder.encode(JwtClaimsSet.builder()
.subject(user.getUsername())
.claim("type", "refresh")
.claim("jti", refreshTokenId) // 关键:唯一标识用于吊销
.expiresAt(Instant.now().plus(7, ChronoUnit.DAYS))
.build()).getTokenValue();
// Refresh Token存Redis(带用户标识,支持单点登录踢人)
redisTemplate.opsForValue().set(
"refresh_token:" + user.getUsername() + ":" + refreshTokenId,
refreshTokenId,
Duration.ofDays(7)
);
return new TokenPair(accessToken, refreshToken, refreshTokenId);
}
// 2. 刷新Token接口(含安全校验)
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refreshToken(@RequestBody RefreshRequest req) {
// 校验Refresh Token合法性
Claims claims = validateToken(req.getRefreshToken());
if (!"refresh".equals(claims.get("type"))) throw new BadCredentialsException("无效令牌类型");
// 检查Redis中是否存在(防伪造+支持吊销)
String jti = claims.get("jti", String.class);
String storedJti = redisTemplate.opsForValue()
.get("refresh_token:" + claims.getSubject() + ":" + jti);
if (!jti.equals(storedJti)) throw new BadCredentialsException("令牌已失效");
// 生成新Access Token(Refresh Token不更新,避免频繁写DB)
String newAccessToken = generateAccessToken(claims.getSubject());
return ResponseEntity.ok(new TokenResponse(newAccessToken, null));
}
}
// 3. 拦截器:自动处理Access Token过期
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = extractToken(request);
try {
validateToken(token); // 验证Access Token
return true;
} catch (ExpiredJwtException e) {
// 关键:返回特定状态码,前端识别后自动调用refresh接口
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader("Token-Expired", "true");
return false;
}
}
}
⚠️ 安全加固清单(必看!)
| 风险 | 解决方案 | 代码位置 |
|---|---|---|
| Refresh Token被盗 | 绑定设备指纹(User-Agent+IP哈希) | 生成时存Redis附加设备信息 |
| 无限刷新攻击 | 限制单用户Refresh Token数量(如最多3个) | 生成时检查Redis计数 |
| 令牌吊销 | 删除Redis中对应jti记录 | 提供/logout接口清除 |
| 重放攻击 | Access Token加入jti+Redis短时缓存校验 | 验证时检查jti是否已用 |
✅ 前端配合示例(Axios拦截器):
axios.interceptors.response.use(null, error => {
if (error.response?.status === 401 && error.response.headers['token-expired']) {
return refreshToken().then(() => axios(error.config)); // 自动刷新重试
}
return Promise.reject(error);
});
🔐 三、SSO单点登录:企业级“一次登录,处处通行”(CAS协议实战)
🌐 架构图
graph LR
A[应用A] -->|重定向| C(CAS认证中心)
B[应用B] -->|重定向| C
D[应用C] -->|重定向| C
C -->|Ticket验证| E[(用户数据库)]
C -->|返回ST| A
C -->|返回ST| B
A -->|验证ST| C
B -->|验证ST| C
💻 Spring Boot应用集成CAS客户端(无需自建Server)
# application.yml (应用A配置)
spring:
security:
cas:
server-url-prefix: https://sso.yourcompany.com/cas
server-login-url: https://sso.yourcompany.com/cas/login
client-host-url: https://app-a.yourcompany.com
validation-type: cas3 # 支持CAS 3.0协议
// 2. 配置Security(Spring Security 6.1+)
@Configuration
@EnableWebSecurity
public class CasSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.cas(cas -> cas
.serviceProperties(serviceProperties())
.casAuthenticationProvider(casAuthenticationProvider())
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("https://sso.yourcompany.com/cas/logout?service=https://app-a.yourcompany.com")
);
return http.build();
}
// 3. 用户信息同步(登录后自动创建本地账号)
@Bean
public AuthenticationSuccessHandler successHandler() {
return (request, response, authentication) -> {
CasAssertionAuthenticationToken token = (CasAssertionAuthenticationToken) authentication;
Assertion assertion = token.getAssertion();
AttributePrincipal principal = assertion.getPrincipal();
// 从CAS返回的attributes获取用户信息
Map<String, Object> attrs = principal.getAttributes();
String email = (String) attrs.get("email");
// 同步到本地数据库(避免每次查CAS)
userService.syncUserIfAbsent(principal.getName(), email, attrs);
// 重定向到业务首页
response.sendRedirect("/dashboard");
};
}
}
🌟 关键实践
- CAS Server选型:
- 开源:Apereo CAS(Java) / CASino(Ruby)
- 云服务:Authing、Keycloak(支持CAS协议)
- 票据验证优化:
- 启用
proxyGrantingTicket支持应用间代理调用 - 配置
tolerance参数容忍时钟偏差(默认1000ms)
- 启用
- 登出同步:
- CAS Server登出时,通过
logoutRequest通知所有已登录应用
- CAS Server登出时,通过
🌍 四、第三方权限打通:GitHub/微信登录实战(OAuth2.0授权码模式)
📱 双平台接入代码(Spring Security OAuth2 Client)
# application.yml
spring:
security:
oauth2:
client:
registration:
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user,user:email
wechat:
client-id: ${WECHAT_APPID}
client-secret: ${WECHAT_SECRET}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/wechat"
client-authentication-method: post
scope: snsapi_userinfo
provider:
wechat:
authorization-uri: https://open.weixin.qq.com/connect/oauth2/authorize
token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
user-info-uri: https://api.weixin.qq.com/sns/userinfo
user-name-attribute: openid
// 2. 自定义用户信息映射(关键!)
@Service
public class OAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
String registrationId = userRequest.getClientRegistration().getRegistrationId();
Map<String, Object> attributes;
// GitHub用户信息处理
if ("github".equals(registrationId)) {
GitHubUserInfo githubUser = githubClient.getUserInfo(userRequest.getAccessToken().getTokenValue());
attributes = Map.of(
"id", githubUser.getId(),
"email", githubUser.getEmail(),
"avatar", githubUser.getAvatarUrl(),
"source", "GITHUB"
);
}
// 微信用户信息处理(需解密)
else if ("wechat".equals(registrationId)) {
WeChatUserInfo wechatUser = wechatClient.decryptUserInfo(
userRequest.getAccessToken().getTokenValue(),
userRequest.getAdditionalParameters().get("code")
);
attributes = Map.of(
"openid", wechatUser.getOpenid(),
"nickname", wechatUser.getNickname(),
"avatar", wechatUser.getHeadimgurl(),
"source", "WECHAT"
);
} else {
throw new IllegalArgumentException("不支持的第三方平台");
}
// 3. 权限映射:根据第三方信息分配系统角色
List<GrantedAuthority> authorities = determineAuthorities(attributes);
// 4. 同步到本地用户体系(关键!避免重复注册)
String userId = userService.syncOAuthUser(attributes, registrationId);
attributes.put("localUserId", userId);
return new DefaultOidcUser(authorities, userRequest.getIdToken(), attributes);
}
private List<GrantedAuthority> determineAuthorities(Map<String, Object> attrs) {
// 示例:GitHub企业邮箱用户自动赋予ADMIN角色
if ("GITHUB".equals(attrs.get("source")) &&
((String)attrs.get("email")).endsWith("@yourcompany.com")) {
return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
}
🔒 安全红线(血泪教训!)
| 风险 | 解决方案 |
|---|---|
| 重定向URL劫持 | 白名单校验redirect_uri(Spring Security 6.1+自动校验) |
| 授权码泄露 | 使用PKCE(Proof Key for Code Exchange),前端生成code_verifier |
| 用户信息伪造 | 服务端二次验证(如微信需用session_key解密) |
| 权限越权 | 第三方返回的scope必须与请求一致,服务端二次校验 |
✅ PKCE前端示例(关键防攻击):
// 生成code_verifier和code_challenge
const codeVerifier = generateRandomString(128);
const codeChallenge = base64UrlEncode(sha256(codeVerifier));
localStorage.setItem('code_verifier', codeVerifier);
// 跳转授权时携带
window.location = `https://github.com/login/oauth/authorize?...&code_challenge=${codeChallenge}&code_challenge_method=S256`;
🧩 五、三位一体:企业级认证融合架构(终极方案)
🌐 统一认证中心设计
flowchart TD
A[用户访问系统] --> B{认证方式?}
B -->|内网员工| C[CAS SSO]
B -->|移动端/API| D[双Token认证]
B -->|外部用户| E[OAuth2第三方登录]
C --> F[统一认证中心]
D --> F
E --> F
F --> G{验证通过?}
G -->|是| H[生成统一身份令牌<br/>(含source: CAS/OAUTH2/TOKEN)]
G -->|否| I[返回错误]
H --> J[业务系统]
J --> K[权限网关]
K --> L[根据source+角色鉴权]
L --> M[返回业务数据]
style F fill:#e6f7ff,stroke:#1890ff
style K fill:#f6ffed,stroke:#52c41a
💡 融合关键点
- 统一身份模型
public class UnifiedIdentity { private String userId; // 本地系统唯一ID private String source; // CAS / GITHUB / WECHAT / TOKEN private String sourceId; // CAS的username / GitHub的id private List<String> roles; // 统一角色体系(映射各来源权限) private Map<String, Object> attributes; // 原始属性保留 } - 权限网关拦截
- 所有请求经网关解析Token/Session → 转为
UnifiedIdentity - 网关调用权限服务校验(支持ABAC动态策略)
- 所有请求经网关解析Token/Session → 转为
- 登出同步
- 双Token:清除Redis中Refresh Token
- SSO:调用CAS登出接口 + 清除本地Session
- OAuth2:清除本地会话(第三方登出需跳转其logout页)
📌 六、避坑指南:血泪总结的5条铁律
| 坑点 | 正确姿势 | 工具推荐 |
|---|---|---|
| Token存储 | Refresh Token存HttpOnly Cookie + Secure;Access Token存内存 | Spring Security CookieCsrfTokenRepository |
| 时钟不同步 | 所有服务器启用NTP同步;JWT验证加leeway参数 | JwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(...).jwtValidator(JwtValidators.createDefaultWithLeeway(60)).build(); |
| 权限爆炸 | 第三方登录默认最小权限,需人工审核提升 | 自定义OAuth2UserService中设置默认角色 |
| 调试黑洞 | 开启Spring Security DEBUG日志;用ngrok调试微信回调 | logging.level.org.springframework.security=DEBUG |
| 合规风险 | GDPR:第三方登录需用户明确授权;存储前脱敏 | 使用@JsonIgnore注解敏感字段 |
💎 结语:认证的本质是“信任传递”
技术没有银弹,但有场景最优解:
- 双Token → 守护用户体验(无感刷新)
- SSO → 提升组织效率(一次认证,全域通行)
- OAuth2 → 连接生态价值(开放共赢)
真正的高手,不是记住所有代码,而是懂得何时用何方案。
本文所有代码已整理至 GitHub:
🔗 github.com/yourname/auth-trinity
(含Docker一键启动、Postman测试集、安全检查清单)
🌱 最后赠言:
“认证授权是系统的护城河,
但护城河的意义不是隔绝世界,
而是让信任安全流动。”
—— 愿你的系统,既有铜墙铁壁,亦有春风化雨。
附:极速验证清单
✅ 双Token:启动项目 → 登录 → 等15分钟 → 自动刷新无感知
✅ SSO:配置CAS客户端 → 访问应用 → 跳转CAS登录 → 返回业务页
✅ OAuth2:配置GitHub OAuth → 点击“GitHub登录” → 获取用户信息
环境要求:JDK 21+ | Maven 3.8+ | Redis 7.0+ | (CAS/OAuth2需公网回调)
版权声明:本文代码可商用,转载请保留出处。
安全提示:生产环境务必修改默认密钥、启用HTTPS、定期审计令牌。
更新于:2026年1月30日 · 一个让认证不再焦虑的冬日午后 🌤️
