Spring Boot实现单点登录案例

wen java案例 3

本文目录导读:

Spring Boot实现单点登录案例

  1. 项目结构
  2. 公共模块 (common)
  3. SSO认证服务器 (sso-server)
  4. 客户端应用 (sso-client1)
  5. 客户端2 (sso-client2)
  6. 运行测试
  7. 安全注意事项
  8. 扩展功能建议

我来为您提供一个完整的Spring Boot单点登录(SSO)实现案例,这里将使用JWT(JSON Web Token)+ Redis的方案来实现。

项目结构

sso-demo
├── sso-server/          # SSO认证服务器
├── sso-client1/         # 客户端应用1
├── sso-client2/         # 客户端应用2
└── common/              # 公共模块

公共模块 (common)

1 Maven依赖 (common/pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>sso-common</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <properties>
        <java.version>1.8</java.version>
        <jjwt.version>0.11.5</jjwt.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

2 实体类 (common/src/main/java/com/example/sso/common/entity/User.java)

package com.example.sso.common.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String role;
    // 用于JWT中的用户信息
    public UserInfo toUserInfo() {
        return new UserInfo(this.id, this.username, this.email, this.role);
    }
}

3 JWT工具类 (common/src/main/java/com/example/sso/common/utils/JwtUtil.java)

package com.example.sso.common.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.refresh-expiration}")
    private Long refreshExpiration;
    private Key key;
    @PostConstruct
    public void init() {
        this.key = Keys.hmacShaKeyFor(secret.getBytes());
    }
    /**
     * 生成访问令牌
     */
    public String generateAccessToken(String username, String userId, String role) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("role", role);
        claims.put("tokenType", "access");
        return generateToken(claims, username, expiration);
    }
    /**
     * 生成刷新令牌
     */
    public String generateRefreshToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("tokenType", "refresh");
        return generateToken(claims, username, refreshExpiration);
    }
    /**
     * 生成令牌
     */
    private String generateToken(Map<String, Object> claims, String subject, Long expiration) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }
    /**
     * 解析令牌
     */
    public Claims parseToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    /**
     * 获取用户ID
     */
    public String getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return (String) claims.get("userId");
    }
    /**
     * 获取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }
    /**
     * 获取角色
     */
    public String getRoleFromToken(String token) {
        Claims claims = parseToken(token);
        return (String) claims.get("role");
    }
    /**
     * 验证令牌是否有效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    /**
     * 检查是否为刷新令牌
     */
    public boolean isRefreshToken(String token) {
        Claims claims = parseToken(token);
        return "refresh".equals(claims.get("tokenType"));
    }
}

4 安全工具类 (common/src/main/java/com/example/sso/common/utils/SecurityUtil.java)

package com.example.sso.common.utils;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SecurityUtil {
    /**
     * 从请求中获取令牌
     */
    public static String getTokenFromRequest(HttpServletRequest request) {
        // 1. 从Header获取
        String token = request.getHeader("Authorization");
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            return token.substring(7);
        }
        // 2. 从Cookie获取
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("SSO_TOKEN".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        // 3. 从参数获取
        token = request.getParameter("token");
        if (StringUtils.hasText(token)) {
            return token;
        }
        return null;
    }
    /**
     * 添加令牌到Cookie
     */
    public static void addTokenToCookie(HttpServletResponse response, String token, int maxAge) {
        Cookie cookie = new Cookie("SSO_TOKEN", token);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }
    /**
     * 清除Cookie
     */
    public static void clearCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("SSO_TOKEN", null);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }
}

SSO认证服务器 (sso-server)

1 Maven配置 (sso-server/pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>sso-server</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>sso-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

2 配置文件 (sso-server/src/main/resources/application.yml)

server:
  port: 8080
  servlet:
    context-path: /sso
spring:
  application:
    name: sso-server
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 5000
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
jwt:
  secret: your-256-bit-secret-key-for-sso-demo-application-change-in-production
  expiration: 3600000        # 1小时
  refresh-expiration: 86400000  # 24小时
# SSO配置
sso:
  server:
    url: http://localhost:8080/sso
  clients:
    - clientId: client1
      clientSecret: client1-secret
      redirectUri: http://localhost:8081/client1/callback
      homeUrl: http://localhost:8081/client1/home
    - clientId: client2
      clientSecret: client2-secret
      redirectUri: http://localhost:8082/client2/callback
      homeUrl: http://localhost:8082/client2/home

3 SSO认证服务器主类 (sso-server/src/main/java/com/example/sso/server/SsoServerApplication.java)

package com.example.sso.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SsoServerApplication.class, args);
        System.out.println("SSO Server started on port: 8080");
    }
}

4 认证控制器 (sso-server/src/main/java/com/example/sso/server/controller/AuthController.java)

package com.example.sso.server.controller;
import com.example.sso.common.entity.User;
import com.example.sso.common.utils.JwtUtil;
import com.example.sso.common.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private RedisUtil redisUtil;
    @Value("${sso.server.url}")
    private String ssoServerUrl;
    // 模拟用户数据库(实际应使用数据库)
    private Map<String, User> userDatabase = new ConcurrentHashMap<>();
    // 模拟客户端配置
    private Map<String, Map<String, String>> clientConfigs = new ConcurrentHashMap<>();
    @PostConstruct
    public void init() {
        // 初始化测试用户
        userDatabase.put("admin", new User(1L, "admin", "123456", "admin@example.com", "ADMIN"));
        userDatabase.put("user", new User(2L, "user", "123456", "user@example.com", "USER"));
        // 初始化客户端配置
        Map<String, String> client1 = new HashMap<>();
        client1.put("clientSecret", "client1-secret");
        client1.put("redirectUri", "http://localhost:8081/client1/callback");
        client1.put("homeUrl", "http://localhost:8081/client1/home");
        clientConfigs.put("client1", client1);
        Map<String, String> client2 = new HashMap<>();
        client2.put("clientSecret", "client2-secret");
        client2.put("redirectUri", "http://localhost:8082/client2/callback");
        client2.put("homeUrl", "http://localhost:8082/client2/home");
        clientConfigs.put("client2", client2);
    }
    /**
     * 登录接口
     */
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody Map<String, String> loginRequest,
                                   @RequestParam(required = false) String clientId,
                                   @RequestParam(required = false) String redirectUri,
                                   HttpServletResponse response) {
        String username = loginRequest.get("username");
        String password = loginRequest.get("password");
        // 验证用户
        User user = userDatabase.get(username);
        if (user == null || !user.getPassword().equals(password)) {
            return ResponseEntity.badRequest().body(Map.of("error", "用户名或密码错误"));
        }
        // 生成令牌
        String accessToken = jwtUtil.generateAccessToken(username, String.valueOf(user.getId()), user.getRole());
        String refreshToken = jwtUtil.generateRefreshToken(username);
        // 保存令牌到Redis
        redisUtil.saveToken("sso:" + username, accessToken, 3600);
        // 设置Cookie
        com.example.sso.common.utils.SecurityUtil.addTokenToCookie(response, accessToken, 3600);
        // 如果指定了客户端,返回授权码
        if (clientId != null && clientConfigs.containsKey(clientId)) {
            String authCode = generateAuthCode(username, clientId);
            Map<String, Object> result = new HashMap<>();
            result.put("accessToken", accessToken);
            result.put("refreshToken", refreshToken);
            result.put("authCode", authCode);
            result.put("redirectUri", clientConfigs.get(clientId).get("redirectUri"));
            return ResponseEntity.ok(result);
        }
        // 返回用户信息和令牌
        Map<String, Object> result = new HashMap<>();
        result.put("accessToken", accessToken);
        result.put("refreshToken", refreshToken);
        result.put("user", user.toUserInfo());
        result.put("ssoServerUrl", ssoServerUrl);
        return ResponseEntity.ok(result);
    }
    /**
     * 获取用户信息
     */
    @GetMapping("/user/info")
    public ResponseEntity<?> getUserInfo(@RequestHeader(value = "Authorization", required = false) String token,
                                         @RequestParam(required = false) String clientId) {
        if (token == null || !token.startsWith("Bearer ")) {
            return ResponseEntity.status(401).body(Map.of("error", "未认证"));
        }
        String accessToken = token.substring(7);
        if (!jwtUtil.validateToken(accessToken)) {
            return ResponseEntity.status(401).body(Map.of("error", "令牌无效"));
        }
        String username = jwtUtil.getUsernameFromToken(accessToken);
        String tokenKey = "sso:" + username;
        String storedToken = redisUtil.getToken(tokenKey);
        if (storedToken == null || !storedToken.equals(accessToken)) {
            return ResponseEntity.status(401).body(Map.of("error", "令牌已过期"));
        }
        User user = userDatabase.get(username);
        Map<String, Object> result = new HashMap<>();
        result.put("user", user.toUserInfo());
        return ResponseEntity.ok(result);
    }
    /**
     * 令牌验证接口(供客户端调用)
     */
    @PostMapping("/token/validate")
    public ResponseEntity<?> validateToken(@RequestBody Map<String, String> request) {
        String accessToken = request.get("token");
        if (!jwtUtil.validateToken(accessToken)) {
            return ResponseEntity.status(401).body(Map.of("error", "令牌无效"));
        }
        String username = jwtUtil.getUsernameFromToken(accessToken);
        String tokenKey = "sso:" + username;
        String storedToken = redisUtil.getToken(tokenKey);
        if (storedToken == null || !storedToken.equals(accessToken)) {
            return ResponseEntity.status(401).body(Map.of("error", "令牌已过期"));
        }
        User user = userDatabase.get(username);
        return ResponseEntity.ok(user.toUserInfo());
    }
    /**
     * 刷新令牌
     */
    @PostMapping("/token/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody Map<String, String> request) {
        String refreshToken = request.get("refreshToken");
        if (!jwtUtil.validateToken(refreshToken) || !jwtUtil.isRefreshToken(refreshToken)) {
            return ResponseEntity.status(401).body(Map.of("error", "刷新令牌无效"));
        }
        String username = jwtUtil.getUsernameFromToken(refreshToken);
        User user = userDatabase.get(username);
        String newAccessToken = jwtUtil.generateAccessToken(username, String.valueOf(user.getId()), user.getRole());
        String newRefreshToken = jwtUtil.generateRefreshToken(username);
        // 更新Redis中的令牌
        redisUtil.saveToken("sso:" + username, newAccessToken, 3600);
        Map<String, Object> result = new HashMap<>();
        result.put("accessToken", newAccessToken);
        result.put("refreshToken", newRefreshToken);
        return ResponseEntity.ok(result);
    }
    /**
     * 登出
     */
    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader(value = "Authorization") String token,
                                    HttpServletResponse response) {
        if (token != null && token.startsWith("Bearer ")) {
            String accessToken = token.substring(7);
            if (jwtUtil.validateToken(accessToken)) {
                String username = jwtUtil.getUsernameFromToken(accessToken);
                redisUtil.deleteToken("sso:" + username);
            }
        }
        // 清除Cookie
        com.example.sso.common.utils.SecurityUtil.clearCookie(response);
        return ResponseEntity.ok(Map.of("message", "登出成功"));
    }
    /**
     * 生成授权码
     */
    private String generateAuthCode(String username, String clientId) {
        String authCode = java.util.UUID.randomUUID().toString().replace("-", "");
        String key = "auth_code:" + authCode;
        String value = username + ":" + clientId;
        redisUtil.setValue(key, value, 300); // 5分钟有效
        return authCode;
    }
}

5 Redis工具类 (common/src/main/java/com/example/sso/common/utils/RedisUtil.java)

package com.example.sso.common.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 保存令牌
     */
    public void saveToken(String key, String token, long timeout) {
        stringRedisTemplate.opsForValue().set(key, token, timeout, TimeUnit.SECONDS);
    }
    /**
     * 获取令牌
     */
    public String getToken(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }
    /**
     * 删除令牌
     */
    public void deleteToken(String key) {
        stringRedisTemplate.delete(key);
    }
    /**
     * 设置值
     */
    public void setValue(String key, String value, long timeout) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }
    /**
     * 获取值
     */
    public String getValue(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }
    /**
     * 检查键是否存在
     */
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key));
    }
}

客户端应用 (sso-client1)

1 Maven配置 (sso-client1/pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>sso-client1</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>sso-common</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!-- Thymeleaf模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
</project>

2 配置文件 (sso-client1/src/main/resources/application.yml)

server:
  port: 8081
  servlet:
    context-path: /client1
spring:
  application:
    name: sso-client1
# SSO客户端配置
sso:
  client:
    clientId: client1
    clientSecret: client1-secret
    ssoServerUrl: http://localhost:8080/sso
    redirectUri: http://localhost:8081/client1/callback
    loginPage: http://localhost:8080/sso/login
    homeUrl: http://localhost:8081/client1/home

3 客户端主类 (sso-client1/src/main/java/com/example/sso/client1/SsoClient1Application.java)

package com.example.sso.client1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoClient1Application {
    public static void main(String[] args) {
        SpringApplication.run(SsoClient1Application.class, args);
        System.out.println("SSO Client1 started on port: 8081");
    }
}

4 SSO配置类 (sso-client1/src/main/java/com/example/sso/client1/config/SsoProperties.java)

package com.example.sso.client1.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "sso.client")
public class SsoProperties {
    private String clientId;
    private String clientSecret;
    private String ssoServerUrl;
    private String redirectUri;
    private String loginPage;
    private String homeUrl;
}

5 认证拦截器 (sso-client1/src/main/java/com/example/sso/client1/interceptor/AuthInterceptor.java)

package com.example.sso.client1.interceptor;
import com.example.sso.client1.config.SsoProperties;
import com.example.sso.common.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Autowired
    private SsoProperties ssoProperties;
    @Autowired
    private JwtUtil jwtUtil;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 排除登录和回调接口
        String uri = request.getRequestURI();
        if (uri.contains("/callback") || uri.contains("/login") || uri.contains("/logout")) {
            return true;
        }
        // 获取令牌
        String token = com.example.sso.common.utils.SecurityUtil.getTokenFromRequest(request);
        if (token == null) {
            redirectToLogin(request, response);
            return false;
        }
        // 验证令牌
        if (!jwtUtil.validateToken(token)) {
            redirectToLogin(request, response);
            return false;
        }
        // 将用户信息放入request
        String username = jwtUtil.getUsernameFromToken(token);
        request.setAttribute("username", username);
        request.setAttribute("role", jwtUtil.getRoleFromToken(token));
        return true;
    }
    private void redirectToLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String redirectUrl = ssoProperties.getSsoServerUrl() + "/login?clientId=" + 
                             ssoProperties.getClientId() + "&redirectUri=" + 
                             ssoProperties.getRedirectUri();
        // 如果是AJAX请求,返回401状态码
        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\": \"未认证\", \"redirectUrl\": \"" + redirectUrl + "\"}");
        } else {
            response.sendRedirect(redirectUrl);
        }
    }
}

6 Web配置类 (sso-client1/src/main/java/com/example/sso/client1/config/WebConfig.java)

package com.example.sso.client1.config;
import com.example.sso.client1.interceptor.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private AuthInterceptor authInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/callback", "/css/*", "/js/*");
    }
}

7 客户端控制器 (sso-client1/src/main/java/com/example/sso/client1/controller/ClientController.java)

package com.example.sso.client1.controller;
import com.example.sso.client1.config.SsoProperties;
import com.example.sso.common.entity.User;
import com.example.sso.common.utils.SecurityUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
public class ClientController {
    @Autowired
    private SsoProperties ssoProperties;
    @Autowired
    private RestTemplate restTemplate;
    /**
     * 客户端首页
     */
    @GetMapping("/home")
    public String home(HttpServletRequest request, Model model) {
        String username = (String) request.getAttribute("username");
        String role = (String) request.getAttribute("role");
        model.addAttribute("username", username);
        model.addAttribute("role", role);
        model.addAttribute("clientName", "Client1");
        return "home";
    }
    /**
     * SSO回调接口
     */
    @GetMapping("/callback")
    public String callback(@RequestParam String token,
                          @RequestParam(required = false) String authCode,
                          HttpServletResponse response) {
        // 设置Cookie
        SecurityUtil.addTokenToCookie(response, token, 3600);
        return "redirect:/client1/home";
    }
    /**
     * 调用SSO服务器获取用户信息
     */
    @GetMapping("/user/info")
    public ResponseEntity<?> getUserInfo(HttpServletRequest request) {
        String token = SecurityUtil.getTokenFromRequest(request);
        // 调用SSO服务器验证令牌
        String validateUrl = ssoProperties.getSsoServerUrl() + "/api/auth/token/validate";
        Map<String, String> requestBody = new HashMap<>();
        requestBody.put("token", token);
        ResponseEntity<User> response = restTemplate.postForEntity(validateUrl, requestBody, User.class);
        return response;
    }
    /**
     * 登出
     */
    @GetMapping("/logout")
    public String logout(HttpServletResponse response) {
        // 清除Cookie
        SecurityUtil.clearCookie(response);
        // 重定向到SSO服务器的登出接口
        return "redirect:" + ssoProperties.getSsoServerUrl() + "/logout";
    }
    /**
     * 获取用户角色
     */
    @GetMapping("/role")
    public ResponseEntity<?> getRole(HttpServletRequest request) {
        String role = (String) request.getAttribute("role");
        String username = (String) request.getAttribute("username");
        Map<String, String> result = new HashMap<>();
        result.put("username", username);
        result.put("role", role);
        result.put("clientName", "Client1");
        return ResponseEntity.ok(result);
    }
}

8 前端页面 (sso-client1/src/main/resources/templates/home.html)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">SSO Client1 - 首页</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background-color: white;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            margin-top: 20px;
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
        }
        .user-info {
            margin-bottom: 20px;
            padding: 15px;
            background-color: #e8f5e9;
            border-radius: 4px;
        }
        .btn {
            background-color: #1976d2;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            text-decoration: none;
            display: inline-block;
            cursor: pointer;
        }
        .btn:hover {
            background-color: #1565c0;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>SSO客户端1 首页</h1>
            <span style="color: #1976d2; font-weight: bold;">
                [<span th:text="${clientName}"></span>]
            </span>
        </div>
        <div class="user-info">
            <h3>用户信息</h3>
            <p>用户名:<span th:text="${username}"></span></p>
            <p>角色:<span th:text="${role}"></span></p>
        </div>
        <div>
            <a href="/client2/home" class="btn" style="background-color: #388e3c; margin-right: 10px;">
                访问客户端2
            </a>
            <a href="/client1/logout" class="btn" style="background-color: #d32f2f;">
                退出登录
            </a>
        </div>
    </div>
</body>
</html>

客户端2 (sso-client2)

客户端2的代码与客户端1基本相同,只需修改以下配置文件:

1 配置文件 (sso-client2/src/main/resources/application.yml)

server:
  port: 8082
  servlet:
    context-path: /client2
spring:
  application:
    name: sso-client2
# SSO客户端配置
sso:
  client:
    clientId: client2
    clientSecret: client2-secret
    ssoServerUrl: http://localhost:8080/sso
    redirectUri: http://localhost:8082/client2/callback
    loginPage: http://localhost:8080/sso/login
    homeUrl: http://localhost:8082/client2/home

2 客户端2的主类和其他类

类似于客户端1,只需将类名和包名中的client1改为client2。

运行测试

1 数据库配置(MySQL)

-- 创建用户表
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(100) NOT NULL,
  `email` varchar(100) DEFAULT NULL,
  `role` varchar(20) DEFAULT 'USER',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO `user` (`id`, `username`, `password`, `email`, `role`) VALUES
(1, 'admin', '123456', 'admin@example.com', 'ADMIN'),
(2, 'user', '123456', 'user@example.com', 'USER');

2 启动服务

  1. 启动Redis服务
  2. 启动SSO服务器(端口8080)
  3. 启动客户端1(端口8081)
  4. 启动客户端2(端口8082)

3 测试流程

  1. 直接访问 http://localhost:8081/client1/home
  2. 系统会重定向到SSO服务器登录页面
  3. 输入用户名和密码登录
  4. 登录成功后,会重定向回客户端1
  5. 自动跳转到SSO服务器
  6. 重定向到客户端1的首页
  7. 点击"访问客户端2"链接,无需再次登录
  8. 点击"退出登录"即可退出所有系统的登录状态

安全注意事项

  1. HTTPS:生产环境必须使用HTTPS协议
  2. 密钥管理:JWT密钥要使用环境变量或配置中心管理
  3. 令牌时效:合理设置令牌有效期
  4. CSRF防护:在表单提交时添加CSRF令牌
  5. XSS防护:对输用户输入进行转义
  6. 异常处理:完善的身份认证异常处理

扩展功能建议

  1. 多因素认证:添加短信/邮箱验证
  2. **第三方

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