谈谈项目中单点登录的实现原理

2025-12-04 0 520

什么是单点登录

想象一下这样的场景:早晨来到公司,你登录了OA系统;接着需要查看项目进度,又得登录项目管理平台;下午要申请报销,还得再次登录财务系统… 这样的重复登录体验是不是很糟糕?

单点登录(SSO)  就是为了解决这个问题而生的:用户只需登录一次,就可以访问所有相互信任的应用系统。

核心实现原理

1. 基于Cookie的共享Session方案

在早期项目中,我们采用基于Cookie的共享Session方案:

实现思路

  • 所有子系统使用同一个顶级域名
  • 登录成功后,认证中心设置一个全局Session Cookie
  • 其他子系统通过读取这个Cookie来验证用户身份

代码示例

@Service
public class TraditionalSSOService {
    
    public void login(HttpServletResponse response, String username) {
        // 创建全局Session
        String globalSessionId = UUID.randomUUID().toString();
        
        // 存储Session信息
        redisTemplate.opsForValue().set(
            \"global_session:\" + globalSessionId, 
            username, 
            30, TimeUnit.MINUTES
        );
        
        // 设置全局Cookie,所有子域名都可以访问
        Cookie sessionCookie = new Cookie(\"GLOBAL_SESSION_ID\", globalSessionId);
        sessionCookie.setDomain(\".company.com\");  // 设置顶级域名
        sessionCookie.setPath(\"/\");
        sessionCookie.setMaxAge(30 * 60);  // 30分钟
        response.addCookie(sessionCookie);
    }
}

局限性

  • 域名必须相同或具有父子关系
  • 安全性较低,容易受到CSRF攻击
  • 不适合跨域场景

2. 基于Token的现代SSO方案(主流)

现在我们普遍采用基于Token的SSO方案,核心流程如下:

2.1 登录时序图

用户访问业务系统A 
    → 重定向到认证中心 
    → 用户输入账号密码 
    → 认证中心验证身份并生成Token 
    → 重定向回业务系统A(携带Token)
    → 业务系统A向认证中心验证Token
    → 登录成功

2.2 认证中心实现

@RestController
@RequestMapping(\"/auth\")
public class AuthCenterController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    /**
     * 登录接口
     */
    @PostMapping(\"/login\")
    public ResponseEntity login(@RequestBody LoginRequest request) {
        // 1. 验证用户凭证
        User user = userService.authenticate(request.getUsername(), request.getPassword());
        
        // 2. 生成JWT Token
        String token = tokenProvider.generateToken(user);
        
        // 3. 记录登录状态
        redisTemplate.opsForValue().set(
            \"sso_token:\" + user.getId(), 
            token, 
            tokenProvider.getTokenValidity(), 
            TimeUnit.SECONDS
        );
        
        return ResponseEntity.ok(new LoginResult(token, user));
    }
    
    /**
     * 验证Token接口
     */
    @PostMapping(\"/verify\")
    public ResponseEntity verifyToken(@RequestParam String token) {
        // 1. 验证Token有效性
        if (!tokenProvider.validateToken(token)) {
            return ResponseEntity.status(401).build();
        }
        
        // 2. 从Token中提取用户信息
        String userId = tokenProvider.getUserIdFromToken(token);
        
        // 3. 检查Token是否在服务端有记录(支持登出功能)
        String serverToken = redisTemplate.opsForValue().get(\"sso_token:\" + userId);
        if (!token.equals(serverToken)) {
            return ResponseEntity.status(401).build();
        }
        
        User user = userService.findById(userId);
        return ResponseEntity.ok(user);
    }
    
    /**
     * 登出接口
     */
    @PostMapping(\"/logout\")
    public ResponseEntity logout(@RequestHeader(\"Authorization\") String token) {
        String userId = tokenProvider.getUserIdFromToken(token.replace(\"Bearer \", \"\"));
        redisTemplate.delete(\"sso_token:\" + userId);
        return ResponseEntity.ok().build();
    }
}

2.3 JWT Token工具类

@Component
public class JwtTokenProvider {
    
    @Value(\"${jwt.secret:defaultSecretKey}\")
    private String secretKey;
    
    @Value(\"${jwt.validity:3600}\")
    private long tokenValidityInSeconds;
    
    /**
     * 生成JWT Token
     */
    public String generateToken(User user) {
        Map claims = new HashMap();
        claims.put(\"userId\", user.getId());
        claims.put(\"username\", user.getUsername());
        claims.put(\"roles\", user.getRoles());
        
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + tokenValidityInSeconds * 1000))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
    
    /**
     * 验证Token有效性
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            log.warn(\"Token已过期: {}\", e.getMessage());
        } catch (Exception e) {
            log.warn(\"Token验证失败: {}\", e.getMessage());
        }
        return false;
    }
    
    /**
     * 从Token中提取用户ID
     */
    public String getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();
        return claims.get(\"userId\", String.class);
    }
}

2.4 业务系统拦截器

@Component
public class SSOInterceptor implements HandlerInterceptor {
    
    @Autowired
    private AuthClient authClient;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        // 排除登录接口本身
        if (request.getRequestURI().contains(\"/login\")) {
            return true;
        }
        
        // 1. 获取Token
        String token = extractToken(request);
        
        if (token == null) {
            // 重定向到认证中心登录页
            redirectToLoginPage(request, response);
            return false;
        }
        
        // 2. 验证Token
        User user = authClient.verifyToken(token);
        if (user == null) {
            // Token无效,重新登录
            redirectToLoginPage(request, response);
            return false;
        }
        
        // 3. 将用户信息存入请求上下文
        UserContext.setCurrentUser(user);
        return true;
    }
    
    private String extractToken(HttpServletRequest request) {
        // 从Header中获取
        String authHeader = request.getHeader(\"Authorization\");
        if (authHeader != null && authHeader.startsWith(\"Bearer \")) {
            return authHeader.substring(7);
        }
        
        // 从URL参数中获取
        String tokenParam = request.getParameter(\"token\");
        if (tokenParam != null) {
            return tokenParam;
        }
        
        // 从Cookie中获取
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (\"SSO_TOKEN\".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        
        return null;
    }
    
    private void redirectToLoginPage(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
        String currentUrl = request.getRequestURL().toString();
        String queryString = request.getQueryString();
        if (queryString != null) {
            currentUrl += \"?\" + queryString;
        }
        
        String loginUrl = authClient.getAuthCenterUrl() + 
                         \"/auth/login?redirect_url=\" + 
                         URLEncoder.encode(currentUrl, \"UTF-8\");
        
        response.sendRedirect(loginUrl);
    }
}

安全考虑与最佳实践

1. Token安全策略

@Component
public class TokenSecurityService {
    
    /**
     * 生成安全的随机Token
     */
    public String generateSecureToken() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[32];
        random.nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }
    
    /**
     * 设置安全Cookie
     */
    public void setSecureCookie(HttpServletResponse response, 
                               String name, String value, int maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setHttpOnly(true);  // 防止XSS攻击
        cookie.setSecure(true);    // 仅HTTPS传输
        cookie.setPath(\"/\");
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }
}

2. 防止重放攻击

@Service
public class ReplayAttackProtection {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 检查并记录Token使用
     */
    public boolean checkAndRecordTokenUsage(String token, String requestId) {
        String key = \"token_usage:\" + token + \":\" + requestId;
        
        // 如果这个请求ID已经存在,说明是重放攻击
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, \"1\", 5, TimeUnit.MINUTES);
        return Boolean.TRUE.equals(result);
    }
}

2. 数据库优化

-- 创建用户会话表
CREATE TABLE user_sessions (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id VARCHAR(64) NOT NULL,
    token VARCHAR(512) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    expires_at DATETIME NOT NULL,
    client_ip VARCHAR(45),
    user_agent TEXT,
    INDEX idx_user_id (user_id),
    INDEX idx_expires_at (expires_at),
    INDEX idx_token (token(64))
);

实际部署架构

高可用架构设计

客户端 → 负载均衡器 → [认证中心实例1, 认证中心实例2, ...]
                    ↓
                [Redis集群]
                    ↓
                [数据库主从]

监控与告警

@Component
public class SSOMonitor {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    private final Counter loginCounter;
    private final Counter tokenVerifyCounter;
    
    public SSOMonitor() {
        loginCounter = Counter.builder(\"sso.login.requests\")
                .description(\"登录请求次数\")
                .register(meterRegistry);
                
        tokenVerifyCounter = Counter.builder(\"sso.token.verify\")
                .description(\"Token验证次数\")
                .register(meterRegistry);
    }
    
    public void recordLogin(boolean success) {
        loginCounter.increment();
        if (!success) {
            // 记录登录失败指标
        }
    }
}

总结

单点登录的实现需要综合考虑多个方面:

  1. 用户体验:无缝的登录跳转,减少用户操作
  2. 安全性:Token安全、防重放攻击、安全传输
  3. 性能:缓存策略、数据库优化、高并发处理
  4. 可扩展性:支持多系统、跨域场景
  5. 可维护性:清晰的架构、完善的监控

通过合理的架构设计和技术选型,单点登录能够显著提升用户体验,同时保证系统的安全性和稳定性。

互动思考:在你的项目中,是如何处理移动端和Web端统一的单点登录需求的?欢迎在评论区分享你的实践经验!

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 谈谈项目中单点登录的实现原理 https://www.zuozi.net/3308.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务