Java案例如何实现单点登录?

wen python案例 4

本文目录导读:

Java案例如何实现单点登录?

  1. 核心原理(必知)
  2. 方案一:基于 Cookie + 共享 Session(最简单的同域名 SSO)
  3. 方案二:基于 JWT + 中央认证服务(最流行的跨域名 SSO)
  4. 方案三:基于 OAuth2 / CAS(企业级最强)
  5. 总结:如何选型?
  6. 关键安全提示

实现单点登录(SSO)在 Java 中是一个经典的企业级分布式系统问题,核心思想是一次登录,多系统通行

由于“如何实现”这个问题范围较大,下面按照从简单到复杂、从原理到实践的顺序,给出几种主流的 Java 实现方案,并附上核心代码逻辑和架构图思路。

核心原理(必知)

所有 SSO 的实现都围绕着一个独立的认证中心(SSO Server)

  1. 用户访问子系统 A,未登录 -> 重定向到 SSO Server。
  2. 用户输入账号密码,SSO Server 验证成功,创建全局会话(Global Session)并生成一个 Ticket
  3. 浏览器携带 Ticket 跳回子系统 A,A 拿着 Ticket 去 SSO Server 验证。
  4. SSO Server 验证 Ticket 有效,通知 A 登录成功,A 创建自己的局部会话。
  5. 用户访问子系统 B,B 发现未登录 -> 重定向到 SSO Server。
  6. SSO Server 发现用户已有全局会话(通过 Cookie/Session),直接生成新 Ticket 并跳回 B,B 验证后登录。

基于 Cookie + 共享 Session(最简单的同域名 SSO)

适用场景: 多个子系统属于同一个顶级域名(如 a.example.comb.example.com)。

实现思路:

  • 将 Session 存储到 Redis 等中间件中(Session 共享)。
  • 将 Cookie 的 Domain 属性设置为 .example.com
  • 所有子系统读取同一个 Redis Session 数据。

Java 核心步骤(以 Spring Boot + Redis 为例):

  1. 引入依赖: spring-session-data-redis
  2. 配置文件(application.yml):
    spring:
      session:
        store-type: redis
      redis:
        host: 你的redis地址
    server:
      servlet:
        session:
          cookie:
            domain: .example.com  # 关键:顶级域名共享
  3. 所有子系统配置一样即可。
    • 优点: 实现极简单,无需额外开发认证中心。
    • 缺点: 无法跨顶级域名(如 a.comb.com),Cookie 不安全(易被劫持,需配合 HTTPS)。

基于 JWT + 中央认证服务(最流行的跨域名 SSO)

适用场景: 跨不同域名、前后端分离、移动端。

核心组件:

  1. SSO Server(认证中心): 登录页面、Token 发放、JWT 生成与校验。
  2. Client 子系统(服务端): 拦截未登录请求,跳转 SSO Server。
  3. JWT 令牌: 携带用户身份信息,时效短,无状态。

Java 实现流程(伪代码 + 关键类):

SSO Server 登录接口

@RestController
@RequestMapping("/sso")
public class SSOAuthController {
    @PostMapping("/login")
    public Result login(String username, String password, HttpServletResponse response) {
        // 1. 校验账号密码
        User user = userService.authenticate(username, password);
        if (user == null) {
            return Result.error("登录失败");
        }
        // 2. 生成 JWT Token(使用 JJWT 或 io.jsonwebtoken 库)
        String token = JwtUtils.createToken(user.getId(), user.getName(), 3600 * 1000); // 1小时过期
        // 3. 将 token 写入 Cookie(HttpOnly, SameSite=Lax)或直接返回给前端
        Cookie cookie = new Cookie("SSO_TOKEN", token);
        cookie.setPath("/");
        cookie.setDomain("sso.com"); // 仅认证中心的域名
        cookie.setHttpOnly(true);
        cookie.setSecure(true); // 强制 HTTPS
        response.addCookie(cookie);
        // 4. 重定向回子系统:携带临时票据 code
        String code = UUID.randomUUID().toString();
        // 将 code -> token 存入 Redis(短期有效,如 5 分钟)
        redisTemplate.opsForValue().set("SSO_CODE:" + code, token, 5, TimeUnit.MINUTES);
        return Result.success("登录成功", code); // 前端用这个 code 去回调子系统
    }
}

Client 子系统的 Filter 拦截

public class SSOFilter extends OncePerRequestFilter {
    @Autowired
    private RestTemplate restTemplate; // 用于与 SSO Server 通信
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        // 1. 检查本地会话(业务系统的 Session)
        HttpSession session = request.getSession();
        if (session.getAttribute("LOGIN_USER") != null) {
            chain.doFilter(request, response);
            return;
        }
        // 2. 检查是否有 callback 参数(SSO Server 跳转回来时携带的 ticket)
        String ticket = request.getParameter("ticket");
        if (StringUtils.hasText(ticket)) {
            // 2.1 拿着 ticket 去 SSO Server 验证并获取用户信息
            String verifyUrl = "http://sso.com/sso/verify?ticket=" + ticket;
            ResponseEntity<UserInfo> resp = restTemplate.getForEntity(verifyUrl, UserInfo.class);
            if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) {
                // 2.2 验证成功,创建本地会话
                session.setAttribute("LOGIN_USER", resp.getBody());
                // 2.3 跳回本系统首页(去除 URL 中的 ticket 参数)
                response.sendRedirect("/index");
                return;
            }
        }
        // 3. 未登录,也没有 ticket,重定向到 SSO Server
        String redirectUrl = URLEncoder.encode("http://www.business.com/current/path", "UTF-8");
        String ssoLoginUrl = "http://sso.com/login?redirect=" + redirectUrl;
        response.sendRedirect(ssoLoginUrl);
    }
}

SSO Server 验证 Ticket 接口

@GetMapping("/sso/verify")
public Result verifyTicket(@RequestParam("ticket") String ticket) {
    String token = redisTemplate.opsForValue().get("SSO_CODE:" + ticket);
    if (token == null) {
        return Result.error("无效ticket");
    }
    // 解析 JWT,获取用户信息
    UserInfo user = JwtUtils.parseToken(token);
    if (user == null) {
        return Result.error("token已过期");
    }
    // 删除已使用的 ticket(一次性)
    redisTemplate.delete("SSO_CODE:" + ticket);
    return Result.success(user);
}
  • 优点: 无状态、跨域、适合分布式。
  • 缺点: 未登录时重定向流程较复杂,需要维护 JWT 刷新机制。

基于 OAuth2 / CAS(企业级最强)

适用场景: 大型企业、需要对接第三方应用、要求标准协议。

推荐框架:

  • CAS Server(Java 经典): Jedis 社区版,功能完善,官方提供了 cas-server-webapp,直接打包部署即可。
  • Spring Security + OAuth2 Client: Spring Boot 官方支持,配置即可集成 SSO。

Spring Security + OAuth2 Client 配置示例(作为 Client 接入):

spring:
  security:
    oauth2:
      client:
        registration:
          my-sso:
            client-id: your-client-id
            client-secret: your-client-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: openid, profile
        provider:
          my-sso:
            issuer-uri: https://your-sso-server.com/auth/realms/your-realm
  • 优点: 标准化(OAuth2 / SAML2),支持社交登录、身份联合。
  • 缺点: 配置复杂,需要维护认证中心(如 Keycloak、CAS Server)。

如何选型?

需求场景 推荐方案 复杂度 安全性
同域名下多个子系统 Cookie + 共享Session 中(需HTTPS)
跨域名、前端分离、小型项目 JWT + 中央认证 ⭐⭐
企业级、多协议、对接第三方 OAuth2 / CAS / Keycloak ⭐⭐⭐⭐ 极高

关键安全提示

  1. Cookie 必须设置 HttpOnly(防止 JS 盗取)。
  2. 生产环境强制 HTTPS(防止 Token 被中间人截获)。
  3. Ticket 必须一次性(防止 Replay Attack)。
  4. JWT 过期时间不宜过长(建议 15-30 分钟,配合 Refresh Token 使用)。
  5. SSO Server 与 Client 之间应使用内网或签名(防止伪造请求)。

如果你需要一个完整的可运行 Demo(Spring Boot + JWT + Redis),可以告诉我,我可以帮你写出更详细的代码结构(包括 JJWT 工具类、登录页面跳转逻辑等)。

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