【java面试100问】02 什么是单点登录,以及单点登录的实现流程?

【java面试100问】02 什么是单点登录,以及单点登录的实现流程?

单点登录(SSO)全面解析与Java实现

1. 单点登录概述

1.1 什么是单点登录

单点登录(Single Sign-On,SSO)是一种身份认证方案,允许用户使用一组凭证(如用户名和密码)登录多个相互信任的应用系统。用户只需在认证中心进行一次登录,即可访问所有授权的应用,无需重复输入凭证。

1.2 SSO的优势

  1. 用户体验提升:减少密码记忆负担,提高操作效率
  2. 安全性增强:聚焦管理认证,降低密码泄露风险
  3. 管理成本降低:统一用户管理,简化权限控制
  4. 系统集成便捷:便于微服务架构下的应用整合

1.3 SSO的应用场景

  • 企业内部系统集成
  • 跨域应用统一认证
  • 第三方应用授权登录
  • 微服务架构下的统一认证

2. 单点登录的核心概念

2.1 主要参与方

2.2 关键术语

  • 认证中心(Authentication Center):负责用户身份验证的核心系统
  • 服务提供方(Service Provider, SP):需要认证的业务应用
  • 令牌(Token):认证凭证的载体
  • 会话(Session):用户登录状态的保持机制

3. 单点登录的实现流程

3.1 基本流程时序图

3.2 详细步骤解析

步骤1:用户访问业务应用

用户尝试访问受保护的业务应用资源。

步骤2:检查本地会话

业务应用检查用户是否存在有效的本地会话。

步骤3:重定向到认证中心

如果无有效会话,业务应用将用户重定向到认证中心。

步骤4:认证中心检查全局会话

认证中心检查用户是否存在有效的全局会话。

步骤5:用户认证

如果无全局会话,要求用户进行身份认证。

步骤6:创建全局会话

认证成功后在认证中心创建全局会话。

步骤7:返回业务应用

认证中心生成令牌并重定向回业务应用。

步骤8:验证令牌

业务应用向认证中心验证令牌有效性。

步骤9:创建本地会话

验证成功后创建本地会话。

步骤10:返回请求资源

业务应用向用户返回请求的资源。

4. 基于Token的SSO实现方案

4.1 系统架构设计

java

// 系统架构核心组件
public class SSOArchitecture {
    /**
     * 认证中心组件
     */
    public class AuthCenter {
        private UserService userService;
        private TokenService tokenService;
        private SessionManager sessionManager;
    }
    
    /**
     * 业务应用组件
     */
    public class BusinessApp {
        private AuthClient authClient;
        private LocalSessionManager sessionManager;
    }
    
    /**
     * 通用工具组件
     */
    public class CommonUtils {
        private CryptoUtil cryptoUtil;
        private HttpUtil httpUtil;
        private JsonUtil jsonUtil;
    }
}

4.2 核心数据结构设计

java

/**
 * SSO令牌实体类
 */
public class SSOToken implements Serializable {
    private static final long serialVersionUID = 1L;
    
    // 令牌ID
    private String tokenId;
    // 用户ID
    private String userId;
    // 用户名
    private String username;
    // 颁发时间
    private Long issueTime;
    // 过期时间
    private Long expireTime;
    // 客户端IP
    private String clientIp;
    // 签名
    private String signature;
    // 自定义数据
    private Map<String, Object> claims;
    
    // 构造方法、getter、setter等
    public SSOToken() {}
    
    public SSOToken(String tokenId, String userId, String username, 
                   Long expireTime, String clientIp) {
        this.tokenId = tokenId;
        this.userId = userId;
        this.username = username;
        this.issueTime = System.currentTimeMillis();
        this.expireTime = expireTime;
        this.clientIp = clientIp;
        this.claims = new HashMap<>();
    }
    
    // 省略getter和setter方法
}

/**
 * 用户信息实体
 */
public class UserInfo implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String userId;
    private String username;
    private String email;
    private String phone;
    private List<String> roles;
    private Map<String, Object> attributes;
    
    // 构造方法、getter、setter
}

/**
 * 认证响应结果
 */
public class AuthResult {
    private boolean success;
    private String message;
    private SSOToken token;
    private UserInfo userInfo;
    private String redirectUrl;
    
    // 构造方法
    public AuthResult(boolean success, String message) {
        this.success = success;
        this.message = message;
    }
    
    // 省略其他构造方法和getter/setter
}

5. Java实现单点登录系统

5.1 认证中心实现

5.1.1 认证控制器

java

/**
 * 认证中心控制器
 */
@RestController
@RequestMapping("/sso/auth")
public class AuthCenterController {
    
    private static final Logger logger = LoggerFactory.getLogger(AuthCenterController.class);
    
    @Autowired
    private AuthService authService;
    
    @Autowired
    private TokenService tokenService;
    
    /**
     * 单点登录入口
     */
    @GetMapping("/login")
    public ResponseEntity<?> login(
            @RequestParam String service,
            @RequestParam(required = false) String redirectUrl,
            HttpServletRequest request) {
        
        try {
            // 检查是否已登录
            String globalToken = getGlobalToken(request);
            if (globalToken != null && tokenService.validateToken(globalToken)) {
                // 已登录,直接生成服务令牌并重定向
                String serviceToken = tokenService.generateServiceToken(globalToken, service);
                String targetUrl = buildRedirectUrl(redirectUrl, serviceToken);
                return ResponseEntity.status(HttpStatus.FOUND)
                        .header("Location", targetUrl)
                        .build();
            }
            
            // 未登录,返回登录页面
            LoginPageVO pageVO = new LoginPageVO();
            pageVO.setService(service);
            pageVO.setRedirectUrl(redirectUrl);
            pageVO.setLoginUrl("/sso/auth/doLogin");
            
            return ResponseEntity.ok(pageVO);
            
        } catch (Exception e) {
            logger.error("登录入口处理失败", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(AuthResult.error("系统错误"));
        }
    }
    
    /**
     * 执行登录认证
     */
    @PostMapping("/doLogin")
    public ResponseEntity<AuthResult> doLogin(
            @RequestBody LoginRequest loginRequest,
            HttpServletRequest request) {
        
        try {
            // 验证登录凭证
            AuthResult authResult = authService.authenticate(
                    loginRequest.getUsername(), 
                    loginRequest.getPassword(),
                    getClientIp(request));
            
            if (!authResult.isSuccess()) {
                return ResponseEntity.badRequest().body(authResult);
            }
            
            // 创建全局会话
            SSOToken globalToken = tokenService.createGlobalToken(authResult.getUserInfo());
            authResult.setToken(globalToken);
            
            // 生成服务令牌
            String serviceToken = tokenService.generateServiceToken(
                    globalToken.getTokenId(), 
                    loginRequest.getService());
            
            // 构建重定向URL
            String redirectUrl = buildRedirectUrl(loginRequest.getRedirectUrl(), serviceToken);
            authResult.setRedirectUrl(redirectUrl);
            
            // 设置全局会话Cookie
            setGlobalTokenCookie(globalToken.getTokenId(), request);
            
            return ResponseEntity.ok(authResult);
            
        } catch (Exception e) {
            logger.error("登录认证失败", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(AuthResult.error("认证服务异常"));
        }
    }
    
    /**
     * 验证服务令牌
     */
    @PostMapping("/validate")
    public ResponseEntity<AuthResult> validateToken(@RequestBody TokenValidateRequest request) {
        try {
            AuthResult result = tokenService.validateServiceToken(
                    request.getToken(), 
                    request.getService());
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            logger.error("令牌验证失败", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(AuthResult.error("令牌验证异常"));
        }
    }
    
    /**
     * 退出登录
     */
    @PostMapping("/logout")
    public ResponseEntity<AuthResult> logout(
            @RequestParam String token,
            @RequestParam(required = false) String service) {
        
        try {
            AuthResult result = tokenService.invalidateToken(token, service);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            logger.error("退出登录失败", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(AuthResult.error("退出登录异常"));
        }
    }
    
    // 辅助方法
    private String getGlobalToken(HttpServletRequest request) {
        // 从Cookie或Header中获取全局令牌
        return CookieUtil.getCookieValue(request, "SSO_GLOBAL_TOKEN");
    }
    
    private void setGlobalTokenCookie(String token, HttpServletRequest request) {
        // 设置全局令牌Cookie
        // 实际实现中需要思考安全性和域设置
    }
    
    private String getClientIp(HttpServletRequest request) {
        // 获取客户端IP
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
    
    private String buildRedirectUrl(String baseUrl, String token) {
        if (baseUrl.contains("?")) {
            return baseUrl + "&sso_token=" + token;
        } else {
            return baseUrl + "?sso_token=" + token;
        }
    }
}

5.1.2 认证服务实现

java

/**
 * 认证服务实现
 */
@Service
public class AuthServiceImpl implements AuthService {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Autowired
    private LoginAttemptService loginAttemptService;
    
    @Override
    public AuthResult authenticate(String username, String password, String clientIp) {
        // 检查登录尝试次数
        if (loginAttemptService.isBlocked(username, clientIp)) {
            return AuthResult.error("登录失败次数过多,请稍后重试");
        }
        
        try {
            // 验证用户凭证
            UserInfo userInfo = userService.findByUsername(username);
            if (userInfo == null) {
                loginAttemptService.recordFailure(username, clientIp);
                return AuthResult.error("用户名或密码错误");
            }
            
            if (!passwordEncoder.matches(password, userInfo.getPassword())) {
                loginAttemptService.recordFailure(username, clientIp);
                return AuthResult.error("用户名或密码错误");
            }
            
            if (!userInfo.isEnabled()) {
                return AuthResult.error("账户已被禁用");
            }
            
            // 清除失败记录
            loginAttemptService.clearFailure(username, clientIp);
            
            return AuthResult.success("登录成功", userInfo);
            
        } catch (Exception e) {
            loginAttemptService.recordFailure(username, clientIp);
            throw new AuthException("认证过程发生错误", e);
        }
    }
    
    @Override
    public void changePassword(String username, String oldPassword, String newPassword) {
        // 密码修改逻辑
        UserInfo userInfo = userService.findByUsername(username);
        if (!passwordEncoder.matches(oldPassword, userInfo.getPassword())) {
            throw new AuthException("原密码错误");
        }
        
        userService.updatePassword(username, passwordEncoder.encode(newPassword));
    }
}

5.1.3 令牌服务实现

java

/**
 * 令牌服务实现
 */
@Service
public class TokenServiceImpl implements TokenService {
    
    @Autowired
    private TokenStorage tokenStorage;
    
    @Autowired
    private CryptoUtil cryptoUtil;
    
    @Value("${sso.token.global-timeout:1800}")
    private long globalTokenTimeout; // 全局令牌超时时间(秒)
    
    @Value("${sso.token.service-timeout:300}")
    private long serviceTokenTimeout; // 服务令牌超时时间(秒)
    
    @Override
    public SSOToken createGlobalToken(UserInfo userInfo) {
        String tokenId = generateTokenId();
        long expireTime = System.currentTimeMillis() + globalTokenTimeout * 1000;
        
        SSOToken token = new SSOToken(tokenId, userInfo.getUserId(), 
                                     userInfo.getUsername(), expireTime, userInfo.getClientIp());
        
        // 设置用户信息
        token.getClaims().put("userInfo", userInfo);
        
        // 生成签名
        String signature = generateSignature(token);
        token.setSignature(signature);
        
        // 存储令牌
        tokenStorage.storeGlobalToken(tokenId, token, globalTokenTimeout);
        
        return token;
    }
    
    @Override
    public String generateServiceToken(String globalTokenId, String service) {
        SSOToken globalToken = tokenStorage.getGlobalToken(globalTokenId);
        if (globalToken == null) {
            throw new TokenException("全局令牌不存在或已过期");
        }
        
        // 验证全局令牌有效性
        if (!validateToken(globalToken)) {
            throw new TokenException("全局令牌无效");
        }
        
        String serviceTokenId = generateTokenId();
        long expireTime = System.currentTimeMillis() + serviceTokenTimeout * 1000;
        
        SSOToken serviceToken = new SSOToken(serviceTokenId, globalToken.getUserId(),
                                           globalToken.getUsername(), expireTime, globalToken.getClientIp());
        
        // 设置关联的全局令牌和服务标识
        serviceToken.getClaims().put("globalTokenId", globalTokenId);
        serviceToken.getClaims().put("service", service);
        serviceToken.getClaims().put("userInfo", globalToken.getClaims().get("userInfo"));
        
        // 生成签名
        String signature = generateSignature(serviceToken);
        serviceToken.setSignature(signature);
        
        // 存储服务令牌
        tokenStorage.storeServiceToken(serviceTokenId, serviceToken, serviceTokenTimeout);
        
        return serviceTokenId;
    }
    
    @Override
    public AuthResult validateServiceToken(String tokenId, String service) {
        SSOToken token = tokenStorage.getServiceToken(tokenId);
        if (token == null) {
            return AuthResult.error("服务令牌不存在或已过期");
        }
        
        // 验证令牌签名
        if (!validateSignature(token)) {
            return AuthResult.error("令牌签名无效");
        }
        
        // 验证令牌是否过期
        if (System.currentTimeMillis() > token.getExpireTime()) {
            return AuthResult.error("服务令牌已过期");
        }
        
        // 验证服务标识
        String tokenService = (String) token.getClaims().get("service");
        if (!service.equals(tokenService)) {
            return AuthResult.error("服务标识不匹配");
        }
        
        // 验证关联的全局令牌
        String globalTokenId = (String) token.getClaims().get("globalTokenId");
        SSOToken globalToken = tokenStorage.getGlobalToken(globalTokenId);
        if (globalToken == null || !validateToken(globalToken)) {
            return AuthResult.error("关联的全局令牌无效");
        }
        
        UserInfo userInfo = (UserInfo) token.getClaims().get("userInfo");
        return AuthResult.success("令牌验证成功", userInfo);
    }
    
    @Override
    public boolean validateToken(SSOToken token) {
        if (token == null) return false;
        
        // 验证签名
        if (!validateSignature(token)) return false;
        
        // 验证过期时间
        if (System.currentTimeMillis() > token.getExpireTime()) return false;
        
        return true;
    }
    
    @Override
    public AuthResult invalidateToken(String tokenId, String service) {
        if (service != null) {
            // 注销服务令牌
            tokenStorage.removeServiceToken(tokenId);
        } else {
            // 注销全局令牌及所有关联的服务令牌
            tokenStorage.removeGlobalToken(tokenId);
        }
        
        return AuthResult.success("注销成功");
    }
    
    // 辅助方法
    private String generateTokenId() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    
    private String generateSignature(SSOToken token) {
        String data = token.getTokenId() + token.getUserId() + token.getIssueTime();
        return cryptoUtil.hmacSha256(data, getSecretKey());
    }
    
    private boolean validateSignature(SSOToken token) {
        String expectedSignature = generateSignature(token);
        return expectedSignature.equals(token.getSignature());
    }
    
    private String getSecretKey() {
        // 从配置或密钥管理服务获取
        return "your-secret-key"; // 实际应用中应该使用安全的密钥管理
    }
}

5.2 业务应用集成

5.2.1 SSO客户端过滤器

java

/**
 * SSO客户端过滤器
 */
@Component
public class SSOClientFilter implements Filter {
    
    @Autowired
    private SSOClient ssoClient;
    
    @Value("${sso.auth-center-url}")
    private String authCenterUrl;
    
    @Value("${sso.client.service-id}")
    private String serviceId;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 检查是否已登录
        if (isAuthenticated(httpRequest)) {
            chain.doFilter(request, response);
            return;
        }
        
        // 检查是否有SSO令牌
        String ssoToken = getSSOToken(httpRequest);
        if (ssoToken != null) {
            // 验证令牌
            AuthResult authResult = ssoClient.validateToken(ssoToken, serviceId);
            if (authResult.isSuccess()) {
                // 创建本地会话
                createLocalSession(httpRequest, authResult.getUserInfo());
                chain.doFilter(request, response);
                return;
            }
        }
        
        // 重定向到认证中心
        redirectToAuthCenter(httpRequest, httpResponse);
    }
    
    private boolean isAuthenticated(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        return session != null && session.getAttribute("userInfo") != null;
    }
    
    private String getSSOToken(HttpServletRequest request) {
        // 从请求参数中获取令牌
        String token = request.getParameter("sso_token");
        if (token != null) return token;
        
        // 从Header中获取
        token = request.getHeader("X-SSO-Token");
        if (token != null) return token;
        
        return null;
    }
    
    private void createLocalSession(HttpServletRequest request, UserInfo userInfo) {
        HttpSession session = request.getSession();
        session.setAttribute("userInfo", userInfo);
        session.setAttribute("loginTime", System.currentTimeMillis());
        
        // 设置会话超时时间
        session.setMaxInactiveInterval(30 * 60); // 30分钟
    }
    
    private void redirectToAuthCenter(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
        
        String currentUrl = getCurrentUrl(request);
        String loginUrl = authCenterUrl + "/login?service=" + serviceId + 
                         "&redirectUrl=" + URLEncoder.encode(currentUrl, "UTF-8");
        
        response.sendRedirect(loginUrl);
    }
    
    private String getCurrentUrl(HttpServletRequest request) {
        StringBuffer url = request.getRequestURL();
        String queryString = request.getQueryString();
        if (queryString != null) {
            url.append("?").append(queryString);
        }
        return url.toString();
    }
}

5.2.2 SSO客户端实现

java

/**
 * SSO客户端服务
 */
@Service
public class SSOClientImpl implements SSOClient {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @Value("${sso.auth-center-url}")
    private String authCenterUrl;
    
    @Override
    public AuthResult validateToken(String token, String service) {
        try {
            TokenValidateRequest request = new TokenValidateRequest(token, service);
            
            ResponseEntity<AuthResult> response = restTemplate.postForEntity(
                    authCenterUrl + "/validate", 
                    request, 
                    AuthResult.class);
            
            if (response.getStatusCode() == HttpStatus.OK) {
                return response.getBody();
            } else {
                return AuthResult.error("令牌验证请求失败");
            }
            
        } catch (Exception e) {
            // 记录日志
            return AuthResult.error("令牌验证服务不可用");
        }
    }
    
    @Override
    public void logout(String token) {
        try {
            restTemplate.postForEntity(
                    authCenterUrl + "/logout?token=" + token, 
                    null, 
                    AuthResult.class);
        } catch (Exception e) {
            // 记录日志,但不需要抛出异常
        }
    }
    
    @Override
    public String getLoginUrl(String redirectUrl) {
        return authCenterUrl + "/login?service=" + getServiceId() + 
               "&redirectUrl=" + URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8);
    }
    
    private String getServiceId() {
        // 从配置获取服务标识
        return "your-service-id";
    }
}

6. 安全思考与最佳实践

6.1 安全性设计

6.1.1 令牌安全

java

/**
 * 增强的令牌安全服务
 */
@Service
public class EnhancedTokenService extends TokenServiceImpl {
    
    @Autowired
    private BlacklistService blacklistService;
    
    @Autowired
    private AuditService auditService;
    
    @Override
    public AuthResult validateServiceToken(String tokenId, String service) {
        // 检查黑名单
        if (blacklistService.isTokenBlacklisted(tokenId)) {
            auditService.recordSecurityEvent("BLACKLISTED_TOKEN_ACCESS", 
                                           tokenId, service, getClientIp());
            return AuthResult.error("令牌已被列入黑名单");
        }
        
        AuthResult result = super.validateServiceToken(tokenId, service);
        
        if (!result.isSuccess()) {
            // 记录验证失败事件
            auditService.recordSecurityEvent("TOKEN_VALIDATION_FAILED", 
                                           tokenId, service, getClientIp());
            
            // 多次失败后加入黑名单
            if (shouldBlacklistToken(tokenId)) {
                blacklistService.blacklistToken(tokenId, 3600); // 黑名单1小时
            }
        }
        
        return result;
    }
    
    @Override
    public String generateServiceToken(String globalTokenId, String service) {
        // 添加设备指纹验证
        String deviceFingerprint = generateDeviceFingerprint();
        
        SSOToken token = super.generateServiceToken(globalTokenId, service);
        token.getClaims().put("deviceFingerprint", deviceFingerprint);
        
        return token;
    }
    
    private String generateDeviceFingerprint() {
        // 基于用户代理、IP等信息生成设备指纹
        return "device-fingerprint"; // 简化实现
    }
}

6.1.2 通信安全

java

/**
 * HTTPS安全配置
 */
@Configuration
public class SecurityConfig {
    
    @Bean
    public RestTemplate restTemplate() throws Exception {
        // 配置SSL安全的RestTemplate
        SSLContext sslContext = SSLContextBuilder
                .create()
                .loadTrustMaterial((chain, authType) -> true) // 实际生产环境需要严格验证
                .build();
        
        HttpClient client = HttpClients.custom()
                .setSSLContext(sslContext)
                .build();
        
        HttpComponentsClientHttpRequestFactory factory = 
                new HttpComponentsClientHttpRequestFactory(client);
        
        return new RestTemplate(factory);
    }
}

6.2 性能优化

6.2.1 令牌存储优化

java

/**
 * 基于Redis的令牌存储
 */
@Service
public class RedisTokenStorage implements TokenStorage {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String GLOBAL_TOKEN_PREFIX = "sso:global:";
    private static final String SERVICE_TOKEN_PREFIX = "sso:service:";
    private static final String USER_SESSION_PREFIX = "sso:user:";
    
    @Override
    public void storeGlobalToken(String tokenId, SSOToken token, long timeout) {
        String key = GLOBAL_TOKEN_PREFIX + tokenId;
        redisTemplate.opsForValue().set(key, token, timeout, TimeUnit.SECONDS);
        
        // 存储用户与令牌的映射
        String userKey = USER_SESSION_PREFIX + token.getUserId();
        redisTemplate.opsForSet().add(userKey, tokenId);
        redisTemplate.expire(userKey, timeout, TimeUnit.SECONDS);
    }
    
    @Override
    public SSOToken getGlobalToken(String tokenId) {
        String key = GLOBAL_TOKEN_PREFIX + tokenId;
        return (SSOToken) redisTemplate.opsForValue().get(key);
    }
}

7. 部署与配置

7.1 配置文件示例

yaml

# application.yml
sso:
  auth-center-url: https://auth.example.com
  token:
    global-timeout: 1800
    service-timeout: 300
    secret-key: ${SSO_SECRET_KEY:default-secret-key}
  redis:
    host: localhost
    port: 6379
    password: ${REDIS_PASSWORD:}
  security:
    require-https: true
    allowed-origins: 
      - https://app1.example.com
      - https://app2.example.com

7.2 数据库设计

sql

-- 用户表
CREATE TABLE sso_users (
    user_id VARCHAR(64) PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    phone VARCHAR(20),
    enabled BOOLEAN DEFAULT TRUE,
    created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 令牌黑名单表
CREATE TABLE sso_blacklist (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    token_id VARCHAR(64) NOT NULL,
    blacklist_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expire_time TIMESTAMP NOT NULL,
    reason VARCHAR(200)
);

-- 审计日志表
CREATE TABLE sso_audit_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_type VARCHAR(50) NOT NULL,
    user_id VARCHAR(64),
    token_id VARCHAR(64),
    service_id VARCHAR(50),
    client_ip VARCHAR(45),
    event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    details TEXT
);

8. 测试与验证

8.1 单元测试示例

java

/**
 * 认证服务测试
 */
@SpringBootTest
class AuthServiceTest {
    
    @Autowired
    private AuthService authService;
    
    @Test
    void testAuthenticateSuccess() {
        AuthResult result = authService.authenticate("testuser", "password", "127.0.0.1");
        assertTrue(result.isSuccess());
        assertEquals("登录成功", result.getMessage());
    }
    
    @Test
    void testAuthenticateFailure() {
        AuthResult result = authService.authenticate("wronguser", "wrongpass", "127.0.0.1");
        assertFalse(result.isSuccess());
        assertEquals("用户名或密码错误", result.getMessage());
    }
}

9. 总结

本文详细介绍了单点登录的概念、原理和实现流程,并提供了完整的Java实现方案。通过认证中心、令牌服务、客户端集成等核心组件的实现,构建了一个功能完备的SSO系统。

关键要点总结:

  1. 架构设计:采用中心化的认证架构,确保认证逻辑的统一性
  2. 令牌机制:使用双重令牌(全局令牌+服务令牌)保证安全性
  3. 安全思考:包含签名验证、黑名单、审计日志等安全措施
  4. 性能优化:通过Redis缓存和连接池等技术提升系统性能
  5. 可扩展性:设计良好的接口和配置,便于系统扩展

单点登录是现代分布式系统的重大组成部分,正确的实现可以显著提升用户体验和系统安全性。本文提供的实现方案可以作为实际项目的基础,根据具体需求进行定制和扩展。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
是阿茜子吖的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容