认证授权三重奏:双Token、SSO、第三方权限打通实战指南(附完整可运行代码)

avatar
莫雨IP属地:上海
02026-01-30:15:31:31字数 12509阅读 2

核心价值
本文不止讲理论!提供 3套独立可运行代码 + 融合架构设计图 + 安全避坑清单,覆盖企业级认证全场景。
所有代码经 Spring Boot 3.3 + JDK 21 验证,开箱即用,拒绝“伪代码”。


🌐 一、为什么你需要这三把“钥匙”?

场景痛点解决方案本文价值
移动端/APIToken过期频繁弹登录框双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");
        };
    }
}

🌟 关键实践

  1. CAS Server选型
    • 开源:Apereo CAS(Java) / CASino(Ruby)
    • 云服务:Authing、Keycloak(支持CAS协议)
  2. 票据验证优化
    • 启用proxyGrantingTicket支持应用间代理调用
    • 配置tolerance参数容忍时钟偏差(默认1000ms)
  3. 登出同步
    • CAS Server登出时,通过logoutRequest通知所有已登录应用

🌍 四、第三方权限打通: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

💡 融合关键点

  1. 统一身份模型
    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; // 原始属性保留
    }
    
  2. 权限网关拦截
    • 所有请求经网关解析Token/Session → 转为UnifiedIdentity
    • 网关调用权限服务校验(支持ABAC动态策略)
  3. 登出同步
    • 双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日 · 一个让认证不再焦虑的冬日午后 🌤️

总资产 0
暂无其他文章

热门文章

暂无热门文章