Java单点登录案例

wen java案例 2

Java单点登录(SSO)案例深度拆解与最佳实践

📚 目录导读

  1. 什么是单点登录(SSO)?—— 概念与核心价值
  2. Java SSO 主流实现方案对比(CAS / OAuth2 / JWT)
  3. 实战案例:基于Spring Boot + JWT + Redis 构建轻量级SSO
    • 1 系统架构与登录流程时序
    • 2 核心代码实现(认证中心 + 业务系统拦截器)
    • 3 会话共享与安全细节(Redis存储、密钥管理)
  4. 常见问题与经典问答(FAQ)
  5. 性能优化与安全加固建议
  6. 总结与展望

什么是单点登录(SSO)?—— 概念与核心价值

单点登录(Single Sign-On,简称SSO) 是指用户只需登录一次,即可访问多个相互信任的应用系统,在企业级分布式架构或微服务体系中,SSO是提升用户体验、统一身份认证的基石。

Java单点登录案例

核心价值:

  • 用户体验提升:消除多系统重复输入账号密码的痛点。
  • 安全统一管控:集中认证、集中审计,便于设置强密码策略和二次认证(MFA)。
  • 降低开发成本:各业务系统无需独立维护认证逻辑,只需对接统一认证中心。

Java SSO 主流实现方案对比(CAS / OAuth2 / JWT)

方案 核心协议 适用场景 优点 缺点
CAS(Central Authentication Service) 重定向 + Ticket 传统企业门户、内外网系统集成 协议成熟,部署广泛,支持跨域 配置较重,前端需要跳转,体验略显陈旧
OAuth2 / OIDC Token(Access Token + Refresh Token) 开放平台、第三方授权登录、移动端 灵活、标准开放,适合与第三方对接 Token生命周期管理复杂,需额外校验状态
JWT(JSON Web Token) 无状态签名Token 微服务、前后端分离、RESTful API 无状态、极易水平扩展、跨语言 无法主动注销(服务端无状态)、密钥泄露风险高

选择建议:对于中小型Java项目或微服务集群,JWT + Redis黑名单/白名单 是最轻量高效的组合,也是当前业界最流行的方案,下文将基于此展开实战案例。


实战案例:基于Spring Boot + JWT + Redis 构建轻量级SSO

1 系统架构与登录流程时序

架构角色:统一认证中心(Auth-Server) + 业务系统A(App-A) + 业务系统B(App-B)。

登录时序(核心简版)

  1. 用户首次访问业务系统A的受保护资源 → A检测无有效令牌 → 重定向至认证中心登录页。
  2. 用户输入账号密码 → 认证中心校验成功 → 生成JWT令牌(包含用户ID、角色、过期时间) → 将令牌写入Redis(key: sso:token:{jti},value: 用户信息,设置TTL与JWT过期时间一致)。
  3. 认证中心通过重定向将JWT携带到回调URL,业务系统A解析JWT,去Redis校验是否存在且未过期(可选校验签名),通过后创建本地会话(或直接信任JWT)。
  4. 用户随后访问业务系统B → B携带JWT(前端存储在localStorage/Cookie) → 同样的解析+校验流程 → 无需再次登录。

2 核心代码实现(认证中心 + 业务系统拦截器)

(1)认证中心:生成令牌与“单点登录”

@Service
public class AuthService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    // 密钥,实际应放在配置中心或环境变量
    private final SecretKey key = Keys.hmacShaKeyFor("my-secret-key-1234567890-abcdef".getBytes());
    public String login(String username, String password) {
        // 1. 校验用户(省略DB查询逻辑)
        User user = userMapper.findByUsername(username);
        if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
            throw new RuntimeException("用户名或密码错误");
        }
        // 2. 生成JWT
        String jti = UUID.randomUUID().toString().replace("-", "");
        Instant now = Instant.now();
        Date expiry = Date.from(now.plus(30, ChronoUnit.MINUTES));
        String jwt = Jwts.builder()
                .setSubject(user.getId().toString())
                .claim("username", user.getUsername())
                .claim("jti", jti)  // 用于Redis关联
                .setIssuedAt(Date.from(now))
                .setExpiration(expiry)
                .signWith(key)
                .compact();
        // 3. 写入Redis,设置过期时间(单位秒)
        String redisKey = "sso:token:" + jti;
        redisTemplate.opsForValue().set(redisKey, user.getId().toString(), 30, TimeUnit.MINUTES);
        return jwt;
    }
}

(2)业务系统拦截器:统一校验(实现“单点”登录的核心)

public class SsoInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate redisTemplate;
    // 与认证中心相同的密钥(可从配置中心拉取)
    private final SecretKey key = Keys.hmacShaKeyFor("my-secret-key-1234567890-abcdef".getBytes());
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从Authorization头或Cookie取JWT
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
        } else {
            // 尝试Cookie
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie c : cookies) {
                    if ("SSO_TOKEN".equals(c.getName())) {
                        token = c.getValue();
                    }
                }
            }
        }
        if (token == null || token.isEmpty()) {
            response.sendRedirect("http://auth-server.com/login?redirect=" + request.getRequestURL());
            return false;
        }
        // 2. 解析JWT(校验签名与过期时间)
        Claims claims;
        try {
            claims = Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException e) {
            // 签名错误或过期,跳转登录
            response.sendRedirect("http://auth-server.com/login?redirect=" + request.getRequestURL());
            return false;
        }
        // 3. 关键:去Redis检查该jti是否仍然有效(实现“注销/废除”)
        String jti = claims.get("jti", String.class);
        String redisKey = "sso:token:" + jti;
        Boolean exists = redisTemplate.hasKey(redisKey);
        if (Boolean.TRUE.equals(exists)) {
            // 将用户信息放入ThreadLocal,供Controller使用
            request.setAttribute("userId", claims.getSubject());
            return true;
        } else {
            // 已注销或Redis过期(JWT可能还没过期,但主动踢掉了)
            response.sendRedirect("http://auth-server.com/login?redirect=" + request.getRequestURL());
            return false;
        }
    }
}

(3)全局退出(注销)—— 让“一次退出,全网生效”

@PostMapping("/logout")
public String logout(@RequestHeader("Authorization") String header) {
    String token = header.replace("Bearer ", "");
    Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    String jti = claims.get("jti", String.class);
    // 删除Redis中的key → 实现强制下线
    redisTemplate.delete("sso:token:" + jti);
    return "redirect:http://auth-server.com/login";
}

3 会话共享与安全细节(Redis存储、密钥管理)

  • Redis的作用:让JWT从“完全无状态”变为“可主动失效”,每次校验时去Redis查询,若key不存在(已被删除或过期),则登录失效,这也是解决JWT“无法注销”痛点的经典方案。
  • 密钥管理:生产环境严禁将密钥硬编码在代码中,应使用Vault、Nacos配置中心或环境变量,且JWT签名算法推荐使用RS256(非对称加密),公钥下发到业务系统,私钥保留在认证中心,避免密钥泄露导致全线失守。
  • 跨域Cookie vs Header:前后端分离项目优先使用Header(Authorization: Bearer)方式传递Token,前端拦截器统一附加,并配合Access-Control-Allow-Headers解决跨域。
  • 滑动续期:在拦截器内可判断JWT剩余有效期小于10分钟,则通过特定接口刷新,返回新的JWT(同时更新Redis TTL)。

常见问题与经典问答(FAQ)

Q1:JWT无状态,为何还要Redis?

纯JWT无法在用户修改密码或管理员踢人时立即失效,引入Redis作为“会话白名单”(或黑名单),将JWT的jti(唯一标识)存储,校验时同时检查签名有效性和Redis中的存在性,实现“可控的无状态”。

Q2:多业务系统部署在不同域名下,Cookie无法跨域传递,怎么办?

方案A:将JWT放在URL参数(不推荐,有泄露风险),方案B:使用认证中心作为跳板(CAS模式),通过重定向传递短时效的临时票据(Ticket),再由业务系统兑换JWT,方案C(推荐):采用前端微服务网关(Gateway)统一鉴权,由网关解析JWT后向下游转发用户ID,系统间不再传递原始Token。

Q3:SSO认证中心挂了,业务系统还能用吗?

视架构而定,若业务系统本地也持有JWT公钥且本地缓存了Redis中的校验结果,则短期可用(需配置降级策略)。高可用设计:Redis必须集群部署,认证中心无状态化,可多实例负载均衡。

Q4:如何防止JWT被窃取后重用(重放攻击)?

务必全站使用HTTPS(TLS)加密传输,避免在HTTP明文下传输,可以绑定终端指纹(User-Agent、设备ID)放入JWT的claim中,校验时对比,降低窃取后的利用价值。


性能优化与安全加固建议

  1. 缓存校验结果:在业务系统拦截器内对(jti, userId)加本地Caffeine缓存(例如5秒),减少对Redis的QPS冲击。
  2. Token过期时间分级:短期Token(30分钟)用于正常访问,长期Refresh Token(7天)用于主动刷新,Refresh Token仅存于认证中心,且可单独撤销。
  3. 审计日志:在认证中心记录每次登录、退出的日志,关键操作(如修改密码)后强制清除该用户所有站点凭证。
  4. 密钥轮换:定期更换JWT签名密钥(旧密钥保留宽限期,支持新旧验签)。
  5. 防暴力破解:登录接口加入图形验证码、滑动验证码或限流(如IP每分钟失败5次锁定)。

总结与展望

通过上述Java单点登录案例,我们清晰地看到:纯JWT方案 + Redis白名单能在简化架构的同时提供较高的安全性和可控性,适用于大多数中小型微服务系统,无论选择CAS、OAuth2还是JWT,核心思路都是“集中认证、分散鉴权、会话共享”

在未来,随着Spring Authorization Server(SAS)的成熟,Java生态中自定义SSO的成本将进一步降低,但理解底层机制(Ticket、Token、会话状态)依然是每个Java架构师必备的技能。

行动建议:对于新项目,优先采用Spring Authorization Server或Keycloak(开源IAM)进行二次封装,避免重复造轮子;而对于遗留系统,使用上述JWT + Redis的轻量方案做平滑改造,是性价比最高的路径。

抱歉,评论功能暂时关闭!