深入解析:SSO 单点登录 解决方案

SSO 单点登录 解决方案


1. 概述

1.1. 共享会话 Session

  • 前提条件: 统一域名, 或统一父域名, 再手动设置 Cookie 域名为父级域名
  • Cookie 自动绑定当前域名, 而不绑定端口, 如需绑定父级域名, 显示指定
  • Cookie 共享以后, 还想做到后端共享 Session , 也就自动实现了 SSO 单点登录

实现技术: Spring Session + Redis
缺点: 无法跨顶级域名, 需要共享会话(Redis 或 JDBC)

1.3. CAS 中心化认证服务

CAS 简化版 sso 登录流程:

  1. 客户端 service1 登录时, 重定向到 cas-server 进行登录
  2. cas-server 认证成功后, 保存 Session, 创建 Cookie=TGC, 创建 ST, 再携带 ST 重定向回到 service1
  3. service1 拿到 ST, 向 cas-server 的 /serviceValidate 端点验证 ST 是否有效
  4. service1 验证成功后, 保存 Session
  5. 当 service2 登录时, 重定向到 cas-server, 由于 TGC 已经存在, 直接创建 ST, service2 实现自动登录

要点: 各个 service, 以及 cas-server 都有自己的独立的会话 Session 和 Cookie
实现技术: Apereo CAS (同时支持 CAS, OAuth2, ODIC 等多种协议)

1.3. JWT 无会话令牌

  • JWT 即 Json Web Token, 其内容包括: 基本的用户信息 , 证明用户的签名
  • JWT 已经可以证明用户身份, 每次请求只需验证 JWT, 所以无需保存会话 Session,
  • JWT 特别适合微服务架构 + 前后端分离, 不用考虑 Session 共享问题
  • JWT 采用 RSA 签名, 认证服务用私钥签发 JWT, 其他业务服务用公钥验证 JWT

参考: 非对称加密 原理: RSA 加密与签名

JWT 实现登录登出:

  1. 登录后, 创建 2 个 JWT: Access Token 用于访问, Refresh Token 用于刷新 Access Token
  2. Access Token 15 分钟有效, 存储: 内存变量(Vue Pinia, 不用 localStorage)
  3. Refresh Token 7 天有效, 存储: Cookie(HttpOnly; Secure; SameSite=Strict)
  4. 登出时: 后端删除Redis 中的 Refresh Token, 前端删除 Cookie

JWT 解决了登录, 要想实现单点登录, 还需考虑前端如何共享 Refresh Token?

  1. 通过父域名共享 Cookie, 共享 Refresh Token, 也就可以实现自动登录
  2. 通过中心化认证服务, 重定向的方式获得 Refresh Token, 也可以实现自动登录

可能会问: 既然共享 Cookie 了, 或使用心化认证服务, 还需要用 JWT 吗, 用前面的2种方式不好吗?
答: 使用 JWT 与前面的 共享会话 和 CAS 本质区别在于: JWT 无会话, 适合微服务架构

实现技术: jjwt, nimbus-jose


2 OAuth2 OIDC

OAuth2 OIDC 实际上就是 JWT 的中心认证服务的具体实现
OIDC 不是独立的协议, 而是对 OAuth2 的补充, 最初 OAuth2 只支持授权, 增加 OIDC 则支持认证

2.1 OAuth2 中的角色

  • Authorization Server 授权服务器 : 负责用户认证、颁发 Token
  • Resource Server 资源服务 : 微服务,需要 Access Token 才能访问的 API
  • Client 客户端 : 访问资源服务器的客户端, 可以是后端服务, 也可以是单页面应用 SPA
  • User 用户 : 资源的所有者, 授权服务器所管理的用户

2.2 OAuth2 授权码授权流程

  • 授权请求 : 客户端重定向到授权服务器, 用户登录, 并同意授权, 并返回授权码
  • 换取令牌 : 客户端使用授权码换取令牌: Access Token, ID Token, Refresh Token
  • 客户端 访问资源服务的 API, 需要携带 Access Token
  • 资源服务 收到 Access Token 向授权服务器校验是否有效

Access Token 可以是 JWT 或者是 无意义的字符串, ID Token 规定必须为 JWT
JWT 的验证, 向授权服务器获取公钥, 然后使用公钥验证 Access Token 或 ID Token

2.3 换取令牌验证密匙 (PKCE):

PKCE 是 OAuth 2.0 的一项安全扩展, 从 OAuth 2.1 开始,强制实施 PKCE
PKCE 的工作流程:

  1. 客户端生成一个验证码: code_verifier
  2. 客户端创建一个挑战码: code_challenge = BASE64URL-ENCODE(SHA256(code_verifier))
  3. 授权请求: 包含 code_challenge
GET /oauth2/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=REDIRECT_URI&
code_challenge=XXXXXX&
code_challenge_method=S256
  1. 返回授权码:
GET /login/oauth2/code/gateway?code=XXX

授权码 通过 URL 参数传递, 泄露风险高

  1. 换取令牌请求: 包含 code_verifier
POST /token HTTP/1.1
Host: your-authorization-server.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
  &code=YOUR_AUTHORIZATION_CODE
  &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
  &client_id=YOUR_CLIENT_ID
  &code_verifier=YOUR_CODE_VERIFIER
  • 公共客户端(单页应用 SPA) 只需包含: code_verifier
  • 机密客户端(Web 后端应用) 需包含: code_verifier + client_secret
  • 授权服务收到 code_verifier 与 code_challenge 进行校验, 即使授权码泄露, 也无法换取令牌
  • POST + HTTPS 参数传递, 泄露风险极低

2.4 使用 OAuth2 架构设计与适用场景

  • Authorization Code + PKCE(SPA 作为 OAuth 客户端,前端直接走 OIDC)

    • 优点:符合现代 SPA 安全实践,不需要客户端密钥,跨域可行(每个前端注册自己的 redirect URI)。
    • 缺点:要为每个前端注册 redirect URI(可自动化或批量管理)。
    • 适用:大量独立前端、希望前端直接完成登录(推荐 SPA 场景)。
  • BFF(为每个前端提供独立的后端)/ 后端代理模式

    • 做法:前端只与自己同源的后端通信,后端作为 OAuth 客户端握有密钥并管理会话(cookie 只在同源后端域名下)。
    • 优点:后端负责令牌管理,前端无需处理敏感令牌,单点控制会话与安全策略。
    • 适用:需要更高安全性、控制会话(尤其要对接资源服务器/后端服务)。

2.5 主流身份提供商(Identity Provider, IdP):

IdP名称类型协议支持部署方式开源/商业适用场景特点
Keycloak全功能OAuth 2.0
OIDC, SAML
自托管开源(Red Hat 支持)中大型企业、微服务架构功能丰富、UI 友好、支持多租户、可扩展性强
Auth0云原生OAuth 2.0
OIDC, SAML
SaaS商业(有免费层)快速上线、初创公司、SaaS 应用易集成、开发者友好、支持社交登录、规则引擎
Spring
Authorization Server
轻量级OAuth 2.1
OIDC(有限)
自托管开源(Spring 官方)Spring 生态内建、定制化需求高代码可控、轻量、适合微服务内部认证

如果你在 Spring 生态中,且需要轻量、可控的 OAuth2 授权服务器 → 选择 Spring Authorization Server
如果你需要完整的身份管理(用户注册、MFA、社交登录、审计) → 选择 Keycloak(开源) 或 Auth0/Okta(商业)
实践建议:很多团队采用 混合模式 —— 用 Keycloak/Auth0 做用户身份管理(IdP),用 Spring Authorization Server 做内部服务间 OAuth2 授权(如 Machine-to-Machine),两者通过 OIDC 联邦集成。


3 使用 Spring Authorization Server + oauth2-client 实现 SSO

  • auth-server 为授权服务器, 地址: http://auth-server.net:9000
  • app1-vue 为前端, 地址: http://app1.taj.com:3000
  • gateway 为统一登录 OAuth2 客户端, 地址: http://gateway:8080
  • service-a 为一个微服务, 地址: http://service-a:8081
  • app1 添加代理 /api 转到 gateway 再转给 service-a
  • app1 添加代理 /oauth2/authorization, /login/oauth2/code 转到 gateway 处理 OIDC
  • app1 检测 未登录 导航到 /api/to-app-login, 认证拦截 触发 OIDC, 最后回到 app1 首页

本地域名映射 host

# ------ sso ------
127.0.0.1 auth-server.net
127.0.0.1 service-a
127.0.0.1 gateway
127.0.0.1 taj.com
127.0.0.1 app.taj.com
127.0.0.1 app1.taj.com
127.0.0.1 app2.taj.com

3.1 搭建授权服务器: auth-server

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>4.0.0</version>
  <!--<version>3.5.8</version>-->   <!--OAuth2授权服务器 1.5.3 版本-->
  <!--<version>3.3.13</version>-->  <!--OAuth2授权服务器 1.3.7 版本-->
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
<groupId>com.taj</groupId>
<artifactId>auth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth-server</name>
<description>auth-server</description>
  <properties>
  <java.version>21</java.version>
  </properties>
  <dependencies>
    <!--web-->
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <!--oauth2-authorization-server-->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
        </dependency>
        <!--security-->
          <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
          </dependency>
          <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
          </dependency>
        </dependencies>
        <build>
          <plugins>
            <!--跳过测试-->
              <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                <skipTests>true</skipTests>
                </configuration>
              </plugin>
              <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
              </plugin>
            </plugins>
          </build>
        </project>
application.yaml
server:
  port: 9000
spring:
  application:
    name: auth-server
---
# 日志
logging:
  level:
    org.springframework.security: DEBUG
---
SecurityConfig.java
package com.taj.resservera.security;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
imp
posted @ 2026-01-26 10:15  gccbuaa  阅读(0)  评论(0)    收藏  举报