本文目录导读:

我来为您提供一个完整的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防护:对输用户输入进行转义
- 异常处理:完善的身份认证异常处理
扩展功能建议
- 多因素认证:添加短信/邮箱验证
- **第三方
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播