前言

单点登录(SSO,Single Sign-On)是一种用户认证和授权的解决方案,允许用户使用一组凭据(如用户名和密码)登录多个相互关联的应用程序和网站,而不需要在每个应用程序和网站中重新输入凭据。
SSO 通过在用户访问第一个应用程序或网站时进行身份验证,然后将用户的身份信息存储在一个中央位置中,以便于其他应用程序和网站进行访问。这种方式可以简化用户的登录过程,减少用户需要记住的凭据数量,提高用户体验和工作效率

四种实现方案

基于Cookie-Session的传统SSO方案

原理:
这是最基础的SSO实现方式,其核心是将用户认证状态存储在服务端Session中,并通过Cookie在客户端保存Session标识符。
当用户登录SSO服务器后,服务器创建Session存储用户信息,并将SessionID通过Cookie设置在顶级域名下,使所有子域应用都能访问该Cookie并验证同一个Session。

优点:
• 实现相对简单,遵循传统Web开发模式
• 服务端完全控制会话状态和生命周期
• 客户端无需存储和管理复杂状态
• 支持即时会话失效和撤销

缺点:
• 受同源策略限制,仅适用于同一顶级域名下的应用
• 依赖Cookie机制,在某些环境可能受限(如移动应用)
• 存在CSRF风险

基于JWT的无状态SSO方案

原理:
JWT(JSON Web Token)是一种紧凑的、自包含的令牌格式,可以在不同应用间安全地传递信息。
使用JWT实现SSO时,认证服务器在用户登录后生成JWT令牌,其中包含用户相关信息和签名。
由于JWT可以独立验证而无需查询中央服务器,它非常适合构建无状态的SSO系统。

优点:
• 完全无状态,服务器不需要存储会话信息
• 跨域支持,适用于分布式系统和微服务
• 可扩展性好,JWT可包含丰富的用户信息
• 不依赖Cookie,避免CSRF问题
• 适用于各种客户端(Web、移动应用、API)

缺点:
• 无法主动失效已颁发的令牌(除非使用黑名单机制)
• JWT可能较大,增加网络传输负担
• 令牌管理需要客户端介入
• 刷新令牌机制较复杂
• 存在令牌被盗用的风险

基于OAuth 2.0/OpenID Connect的SSO方案

原理:
OAuth 2.0是一个授权框架,而OpenID Connect(OIDC)是建立在OAuth 2.0之上的身份认证层。
这是目前最标准化和完善的SSO解决方案,特别适合企业级应用和需要第三方集成的场景。它提供了丰富的授权流程选项和安全特性。

优点:
• 成熟的安全协议,广泛采用的行业标准
• 支持多种认证流程(授权码、隐式、密码等)
• 令牌撤销机制完善
• 可扩展性极好,适合企业级应用
• 明确分离认证与授权职责

缺点:
• 实现复杂度高,小型应用可能不合适
• 配置和理解学习曲线陡峭

基于Spring Session的共享会话SSO方案

原理:
Spring Session提供了一个将会话数据存储在共享外部存储(如Redis)中的框架,使得不同的应用之间能够共享会话信息。
这种方式特别适合基于Spring的同构系统,可以在保持简单实现的同时解决分布式会话共享问题。

优点:
• 实现简单,易于理解
• 与Spring生态无缝集成
• 会话可包含丰富的信息

缺点:
• 依赖中央存储(如Redis)
• 会话数据需要序列化/反序列化
• 依赖Cookie,不适合非Web应用

基于JWT具体实现方案

首先添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

实现JWT工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Component
public class JwtTokenUtil {

@Value("${jwt.secret}")
private String secret;

@Value("${jwt.expiration}")
private Long expiration;

private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}

// 生成Token
public String generateToken(String username, Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSecretKey(), SignatureAlgorithm.HS256)
.compact();
}

// 验证Token
public Boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}

// 获取用户名
public String getUsernameFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(getSecretKey()).build()
.parseClaimsJws(token).getBody().getSubject();
}

// 获取Claims
public Claims getClaimsFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(getSecretKey()).build()
.parseClaimsJws(token).getBody();
}
}

实现认证控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RestController
@RequestMapping("/auth")
public class AuthController {

@Autowired
private JwtTokenUtil jwtTokenUtil;

@PostMapping("/login")
public ResponseEntity<?> login(@RequestParam String username, @RequestParam String password) {
// 1. 验证用户名密码
// 这里我直接使用静态数据来示例了,实际需要查询数据库
if (!"admin".equals(username) || !"123456".equals(password)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误");
}

// 2. 创建JWT
Map<String, Object> claims = new HashMap<>();
// 可自定义添加用户的角色,权限等到claims
claims.put("role", "ADMIN");
String token = jwtTokenUtil.generateToken(username, claims);

// 3. 返回Token
// 实际项目中需要返回给前端存储在localStorage/cookie中
Map<String, String> response = new HashMap<>();
response.put("token", token);
return ResponseEntity.ok(response);
}
}

JWT过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenUtil jwtTokenUtil;

public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 从请求头中获取Token
String token = getTokenFromRequest(request);

if (token != null && jwtTokenUtil.validateToken(token)) {
// 解析Token获取用户名
String username = jwtTokenUtil.getUsernameFromToken(token);

// 创建认证信息
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username, null, new ArrayList<>());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

// 设置认证信息到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("认证失败: " + e.getMessage());
}

filterChain.doFilter(request, response);
}

private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

安全配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenUtil), UsernamePasswordAuthenticationFilter.class);
}
}

方案选择与最佳实践

方案类型 推荐场景 不适合场景
Cookie-Session 同域名下的小型应用,简单的认证需求 跨域应用,移动应用集成,高安全性需求
JWT 分布式微服务,前后端分离应用 需要即时撤销令牌的场景,极高安全性要求
OAuth 2.0/OIDC 企业级应用,需要第三方集成,多租户系统 小型应用,资源受限环境,急速开发需求
Spring Session Spring技术栈的应用,中型企业应用 异构技术栈,非Web应用集成