一、系统架构设计
1. 架构图与组件关系
2. 核心组件说明
| 组件 | 技术栈 | 功能描述 |
|---|
| API Gateway | Spring Cloud Gateway | 统一入口、路由分发、跨域处理、请求过滤 |
| SSO Server | Spring Boot + JWT | 用户认证、Token签发、全局会话管理 |
| SSO Client SDK | Spring Boot Starter | 自动配置、Token解析、权限验证、与SSO Server通信 |
| 配置中心 | Nacos | 统一管理JWT密钥、Token有效期等配置 |
| 缓存层 | Redis | 存储Token黑名单、用户会话状态 |
二、核心流程时序图
1. 统一登录流程
2. Token验证流程
三、代码实现详解
1. SSO Server核心模块
Token签发服务(含自动刷新)
@Service
public class TokenService {
@Value("${jwt.private-key}")
private String privateKey;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public String generateAccessToken(UserDetails user) {
RSAPrivateKey rsaPrivateKey = loadPrivateKey(privateKey);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setIssuer("SSO-CENTER")
.setSubject(user.getUsername())
.claim("authorities", user.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600_000))
.signWith(rsaPrivateKey, SignatureAlgorithm.RS256)
.compact();
}
public Claims validateToken(String token) {
try {
RSAPublicKey publicKey = loadPublicKey();
return Jwts.parserBuilder()
.setSigningKey(publicKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
throw new InvalidTokenException("Token验证失败", e);
}
}
public TokenPair refreshToken(String refreshToken) {
Claims claims = validateToken(refreshToken);
if (!"REFRESH".equals(claims.get("type"))) {
throw new InvalidTokenException("非法的Refresh Token");
}
UserDetails user = userService.loadUser(claims.getSubject());
return new TokenPair(
generateAccessToken(user),
generateRefreshToken(user)
);
}
}
登录控制器
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private TokenService tokenService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
UserDetails user = (UserDetails) authentication.getPrincipal();
String accessToken = tokenService.generateAccessToken(user);
String refreshToken = tokenService.generateRefreshToken(user);
redisTemplate.opsForHash().put(
"active_sessions",
user.getUsername(),
accessToken
);
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, accessToken)
.body(new AuthResponse(accessToken, refreshToken));
}
}
2. SSO Client SDK实现
自动配置类
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableConfigurationProperties(SsoClientProperties.class)
public class SsoAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TokenValidator tokenValidator(SsoClientProperties properties) {
return new DefaultTokenValidator(
resolvePublicKey(properties.getPublicKey()),
properties.getSsoServerUrl()
);
}
@Bean
public FilterRegistrationBean<SsoFilter> ssoFilter() {
FilterRegistrationBean<SsoFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SsoFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return registration;
}
}
Token验证过滤器
public class SsoFilter extends OncePerRequestFilter {
private static final String AUTH_HEADER = "Authorization";
@Autowired
private TokenValidator tokenValidator;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String token = resolveToken(request);
if (StringUtils.hasText(token)) {
try {
Authentication authentication = tokenValidator.validate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (InvalidTokenException e) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "无效的访问凭证");
return;
}
}
chain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTH_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
四、跨域解决方案
1. 后端全局配置(Gateway)
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "https://*.company.com"
allowed-methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowed-headers: "*"
allow-credentials: true
exposed-headers:
- Authorization
- X-Refresh-Token
max-age: 7200
2. 前端统一封装
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000,
withCredentials: true
})
service.interceptors.request.use(config => {
if (store.getters.token) {
config.headers['Authorization'] = 'Bearer ' + store.getters.token
}
return config
})
service.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
return refreshToken().then(() => {
return service(error.config)
})
}
return Promise.reject(error)
}
)
五、会话生命周期管理
1. Token自动续期流程
2. 全局登出实现
MQ+双黑名单实现登出机制
@PostMapping("/logout")
public ResponseEntity<Void> logout(@RequestHeader("Authorization") String token) {
Claims claims = tokenService.validateToken(token.replace("Bearer ", ""));
String username = claims.getSubject();
String tokenId = (String) claims.get("jti");
redisTemplate.opsForValue().set(
"token_blacklist:" + tokenId,
"revoked",
7200, TimeUnit.SECONDS
);
redisTemplate.opsForHash().delete("active_sessions", username);
messagingTemplate.convertAndSend("/topic/logout", tokenId);
return ResponseEntity.noContent().build();
}