Java实现SSO单点登录方案

一、系统架构设计
1. 架构图与组件关系
1.请求访问
2.路由转发
3.集成SDK
4.本地验证
5.远程验证
6.存储状态
7.配置同步
8.用户数据
User
API Gateway
业务系统集群
SSO Client SDK
JWT校验
SSO认证中心
Redis集群
配置中心
用户数据库
2. 核心组件说明
组件技术栈功能描述
API GatewaySpring Cloud Gateway统一入口、路由分发、跨域处理、请求过滤
SSO ServerSpring Boot + JWT用户认证、Token签发、全局会话管理
SSO Client SDKSpring Boot Starter自动配置、Token解析、权限验证、与SSO Server通信
配置中心Nacos统一管理JWT密钥、Token有效期等配置
缓存层Redis存储Token黑名单、用户会话状态

二、核心流程时序图
1. 统一登录流程
User BizSystem SSOClient SSOServer Redis MySQL 访问/protected 检查Cookie/Header中的Token 返回用户上下文 返回资源 重定向到SSO登录页(302) GET /login/page 返回登录表单 POST 提交账号密码 验证用户凭证 返回验证结果 生成JWT Token 记录会话状态(HSET user:${username}) 重定向回原系统(携带Token) 携带Token访问 验证Token有效性 可选远程验证 验证结果 用户上下文 返回请求资源 alt [验证成功] alt [Token有效] [Token无效/过期] User BizSystem SSOClient SSOServer Redis MySQL
2. Token验证流程
BizSystem SSOClient SSOServer Redis 传递Token 本地校验: 1. Base64解码 2. 验证签名 3. 检查有效期 检查本地黑名单(token_id) 是否存在 返回用户信息 返回401未授权 alt [未在黑名单] [在黑名单] 发起远程验证请求 检查全局黑名单 验证Token有效性 返回验证结果 返回验证结果 alt [本地校验通过] [本地校验失败] BizSystem SSOClient SSOServer Redis

三、代码实现详解
1. SSO Server核心模块
Token签发服务(含自动刷新)
/**
 * JWT Token服务(支持自动续期)
 */
@Service
public class TokenService {
    // RSA密钥对(2048位)
    @Value("${jwt.private-key}")
    private String privateKey;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 生成访问令牌(Access Token)
     */
    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)) // 1小时
            .signWith(rsaPrivateKey, SignatureAlgorithm.RS256)
            .compact();
    }

    /**
     * 验证Token并解析声明
     */
    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);
        }
    }

    /**
     * 刷新Token(双Token方案)
     */
    public TokenPair refreshToken(String refreshToken) {
        Claims claims = validateToken(refreshToken);
        if (!"REFRESH".equals(claims.get("type"))) {
            throw new InvalidTokenException("非法的Refresh Token");
        }
        
        // 生成新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) {
        // 1. 身份认证
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getUsername(),
                request.getPassword()
            )
        );
        
        // 2. 生成双Token
        UserDetails user = (UserDetails) authentication.getPrincipal();
        String accessToken = tokenService.generateAccessToken(user);
        String refreshToken = tokenService.generateRefreshToken(user);
        
        // 3. 记录登录状态
        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. 前端统一封装
// axios实例配置
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000,
  withCredentials: true // 跨域携带cookie
})

// 请求拦截器
service.interceptors.request.use(config => {
  if (store.getters.token) {
    config.headers['Authorization'] = 'Bearer ' + store.getters.token
  }
  return config
})

// 响应拦截器(处理401)
service.interceptors.response.use(
  response => response,
  error => {
    if (error.response.status === 401) {
      // 触发Token刷新流程
      return refreshToken().then(() => {
        return service(error.config)
      })
    }
    return Promise.reject(error)
  }
)

五、会话生命周期管理
1. Token自动续期流程
Client Gateway BizSystem SSOServer Redis 请求携带过期Token 返回401 Unauthorized 使用Refresh Token请求新Token 验证Refresh Token有效性 返回新Token对 重试请求携带新Token 返回正常响应 返回401触发重新登录 alt [验证通过] [验证失败] Client Gateway BizSystem SSOServer Redis
2. 全局登出实现

MQ+双黑名单实现登出机制

@PostMapping("/logout")
public ResponseEntity<Void> logout(@RequestHeader("Authorization") String token) {
    // 解析Token获取关键信息
    Claims claims = tokenService.validateToken(token.replace("Bearer ", ""));
    String username = claims.getSubject();
    String tokenId = (String) claims.get("jti");
    
    // 记录黑名单(有效期比Token稍长)
    redisTemplate.opsForValue().set(
        "token_blacklist:" + tokenId, 
        "revoked", 
        7200, TimeUnit.SECONDS
    );
    
    // 清除用户会话记录
    redisTemplate.opsForHash().delete("active_sessions", username);
    
    // 通知所有子系统(WebSocket广播)
    messagingTemplate.convertAndSend("/topic/logout", tokenId);
    
    return ResponseEntity.noContent().build();
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值