本文目录导读:

我来为您提供一个完整的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 启动服务
- 启动Redis服务
- 启动SSO服务器(端口8080)
- 启动客户端1(端口8081)
- 启动客户端2(端口8082)
3 测试流程
- 直接访问
http://localhost:8081/client1/home - 系统会重定向到SSO服务器登录页面
- 输入用户名和密码登录
- 登录成功后,会重定向回客户端1
- 自动跳转到SSO服务器
- 重定向到客户端1的首页
- 点击"访问客户端2"链接,无需再次登录
- 点击"退出登录"即可退出所有系统的登录状态
安全注意事项
- HTTPS:生产环境必须使用HTTPS协议
- 密钥管理:JWT密钥要使用环境变量或配置中心管理
- 令牌时效:合理设置令牌有效期
- CSRF防护:在表单提交时添加CSRF令牌
- XSS防护:对输用户输入进行转义
- 异常处理:完善的身份认证异常处理
扩展功能建议
- 多因素认证:添加短信/邮箱验证
- **第三方