
一、先搞懂核心:SSO单点登录到底是什么?
SSO即单点登录,是一种身份认证与授权技术,核心目标是:用户在多个相互信任的系统(服务)中,只需完成一次身份验证,即可获得所有关联系统的访问权限,无需重复登录。
在电商场景中,SSO的价值体现在三个维度:
普通登录(如单一系统的账号密码登录)中,认证信息(如Session)存储在当前系统服务器,仅对当前系统有效;而SSO的核心是“统一认证中心”,所有关联系统(称为“依赖方”)的身份认证都委托给该中心处理,认证信息由中心统一管理。
举个通俗例子:普通登录像“每个小区都有独立门禁,需单独登记身份”;SSO像“城市一卡通,在所有合作小区门禁系统中只需验证一次,即可通行”。
一个完整的电商SSO架构,包含三个核心角色:
SSO的核心逻辑是“统一认证、凭证共享、信任传递”,不同实现方案(如Cookie+Session、JWT、OAuth2.0)的流程本质一致,只是凭证类型和传递方式不同。下面以电商最常用的“JWT令牌模式”为例,用流程图拆解完整流程,并解释关键环节的设计思路。

SSO的核心设计是“全局会话”与“局部会话”的分离:
令牌是SSO中传递身份信息的核心载体,电商场景中最常用的是JWT(JSON Web Token),相比传统的SessionID,JWT具有“无状态、可携带自定义信息、支持跨域传递”的优势,适合分布式电商架构。
JWT的核心作用:
在电商架构中,用户中心(SSO Server)的域名可能是user.jam-mall.com,商品系统是goods.jam-mall.com,订单系统是order.jam-mall.com——这些系统属于不同域名(跨域场景)。
根据浏览器的“同源策略”:不同源的页面之间,无法直接读取对方的Cookie、LocalStorage,也无法直接发起AJAX请求。而SSO的核心流程(如重定向携带令牌、依赖方验证令牌)都需要跨域交互,若不解决跨域问题,令牌无法正常传递,SSO流程会直接中断。
浏览器判断两个URL是否“同源”,需同时满足三个条件:
jam-mall.com,user.jam-mall.com与goods.jam-mall.com不同源);同源策略是浏览器的安全机制,核心限制包括:
电商SSO中,主要有两个核心跨域场景:
goods.jam-mall.com)重定向到SSO认证中心(user.jam-mall.com),登录后再重定向回商品系统,携带令牌;user.jam-mall.com/api/sso/verifyToken),验证令牌有效性。解决跨域的方案有很多,如JSONP、代理转发、CORS、iframe等。结合电商场景的高可用、高安全性要求,我们需要对比各方案的优缺点,选择最优解。
方案 | 核心原理 | 优点 | 缺点 | 电商场景适用性 |
|---|---|---|---|---|
JSONP | 利用script标签不受同源策略限制,加载远程脚本 | 兼容性好(支持旧浏览器) | 仅支持GET请求,安全性低(易受XSS攻击) | 不推荐 |
代理转发 | 后端服务器转发跨域请求(如Nginx/网关) | 前端无感知,安全性高 | 增加服务器转发压力,需配置网关 | 推荐(配合CORS) |
CORS | 服务器端设置响应头,允许跨域请求 | 支持所有HTTP方法,安全性高,配置简单 | 兼容性依赖浏览器(现代浏览器均支持) | 推荐(核心方案) |
iframe+Cookie | 利用iframe嵌套,共享Cookie | 无需修改前端逻辑 | 安全性低(易受CSRF攻击),Cookie配置复杂 | 不推荐 |
结合电商架构特点(微服务+网关),我们采用“CORS为主,网关代理为辅”的方案:
核心原因:
下面我们基于电商实际场景,搭建一套完整的SSO单点登录系统,包含“SSO认证中心(用户中心)”“商品系统(SSO Client)”两个核心服务,解决跨域问题,实现用户一次登录、多系统访问。
组件 | 版本 | 作用 |
|---|---|---|
JDK | 17 | 开发环境 |
Spring Boot | 3.2.5 | 微服务基础框架 |
Spring Security | 6.2.4 | 安全框架(辅助认证授权) |
MyBatis-Plus | 3.5.5 | 持久层框架(操作MySQL) |
JWT | JJWT 0.11.5 | 生成/验证令牌 |
MySQL | 8.0.36 | 存储用户信息、权限数据 |
Lombok | 1.18.30 | 简化Java代码(@Slf4j等) |
Fastjson2 | 2.0.46 | JSON序列化/反序列化 |
SpringDoc-OpenAPI | 2.3.0 | Swagger3,接口文档生成 |
Guava | 33.2.1-jre | 集合工具类(Lists/Maps) |

SSO认证中心是核心,负责用户登录、令牌生成、令牌验证,需解决跨域问题,提供标准化接口给各业务系统。
创建用户表(sys_user)和角色表(sys_role),采用RBAC权限模型(简化版,满足电商基础权限需求):
-- 用户表
CREATETABLE`sys_user` (
`id`bigintNOTNULL AUTO_INCREMENT COMMENT'用户ID',
`username`varchar(50) NOTNULLCOMMENT'用户名',
`password`varchar(100) NOTNULLCOMMENT'加密密码(BCrypt)',
`nickname`varchar(50) DEFAULTNULLCOMMENT'用户昵称',
`phone`varchar(20) DEFAULTNULLCOMMENT'手机号',
`status`tinyintNOTNULLDEFAULT1COMMENT'状态:1-正常,0-禁用',
`create_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
`update_time` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',
PRIMARY KEY (`id`),
UNIQUEKEY`uk_username` (`username`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='电商用户表';
-- 角色表
CREATETABLE`sys_role` (
`id`bigintNOTNULL AUTO_INCREMENT COMMENT'角色ID',
`role_name`varchar(50) NOTNULLCOMMENT'角色名称(如:ROLE_USER/ROLE_ADMIN)',
`role_desc`varchar(100) DEFAULTNULLCOMMENT'角色描述',
PRIMARY KEY (`id`),
UNIQUEKEY`uk_role_name` (`role_name`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='角色表';
-- 用户角色关联表
CREATETABLE`sys_user_role` (
`id`bigintNOTNULL AUTO_INCREMENT COMMENT'关联ID',
`user_id`bigintNOTNULLCOMMENT'用户ID',
`role_id`bigintNOTNULLCOMMENT'角色ID',
PRIMARY KEY (`id`),
UNIQUEKEY`uk_user_role` (`user_id`,`role_id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户角色关联表';
-- 初始化数据
INSERTINTO`sys_role` (`role_name`, `role_desc`) VALUES ('ROLE_USER', '普通用户'), ('ROLE_ADMIN', '管理员');
-- 密码:123456(BCrypt加密)
INSERTINTO`sys_user` (`username`, `password`, `nickname`, `phone`) VALUES ('jam_user', '$2a$10$EixZaYb4xU58Gpq1R0yWbeb00LU5qUaK6x8h8y08Qv10hP7w2u7aK', '果酱用户', '13800138000');
INSERTINTO`sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);
com.jam.demo.sso.server
├── config/ # 配置类(CORS、JWT、Security等)
├── controller/ # 接口层(登录、令牌验证等)
├── entity/ # 实体类(User、Role等)
├── mapper/ # Mapper接口(MyBatis-Plus)
├── service/ # 服务层(用户校验、权限查询等)
├── util/ # 工具类(JWT工具类等)
├── SsoServerApplication.java # 启动类
└── 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 https://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>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>sso-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sso-server</name>
<description>电商SSO认证中心</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<jjwt.version>0.11.5</jjwt.version>
<fastjson2.version>2.0.46</fastjson2.version>
<guava.version>33.2.1-jre</guava.version>
<springdoc.version>2.3.0</springdoc.version>
</properties>
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- JWT -->
<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>
<scope>runtime</scope>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
解决SSO认证中心的跨域问题,允许各业务系统(如商品系统、订单系统)的跨域请求:
package com.jam.demo.sso.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置类
* 允许所有合法来源的跨域请求,适配电商多系统场景
* @author ken
*/
@Configuration
publicclass CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许所有路径的跨域请求
registry.addMapping("/**")
// 允许的来源(电商各业务系统域名,实际生产环境需指定具体域名,如"https://goods.jam-mall.com")
.allowedOrigins("http://localhost:8081", "http://localhost:8082")
// 允许的请求方法
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 允许的请求头
.allowedHeaders("*")
// 允许携带Cookie(SSO需要传递认证信息)
.allowCredentials(true)
// 预检请求缓存时间(减少预检请求次数,提升性能)
.maxAge(3600);
}
/**
* 跨域过滤器(优先级高于addCorsMappings)
* @return CorsFilter
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:8081");
config.addAllowedOrigin("http://localhost:8082");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
returnnew CorsFilter(source);
}
}
配置JWT的密钥、过期时间等核心参数:
package com.jam.demo.sso.server.config;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import java.util.Base64;
/**
* JWT配置类
* @author ken
*/
@Configuration
publicclass JwtConfig {
/**
* JWT密钥(生产环境需放在配置中心,加密存储)
*/
@Value("${jwt.secret:jam-sso-secret-key-2025-encrypt-123456}")
private String secret;
/**
* JWT过期时间(单位:秒),电商场景建议2小时(7200秒)
*/
@Value("${jwt.expire:7200}")
privatelong expire;
/**
* 签发者(区分不同环境,如dev/prod)
*/
@Value("${jwt.issuer:jam-sso-server}")
private String issuer;
/**
* 获取Base64编码后的密钥(JJWT要求密钥为Base64编码)
* @return 编码后的密钥
*/
public String getSecretKey() {
return Base64.getEncoder().encodeToString(secret.getBytes());
}
public long getExpire() {
return expire;
}
public String getIssuer() {
return issuer;
}
/**
* 获取签名算法(采用HS256,对称加密,适合内部系统)
* @return SignatureAlgorithm
*/
public SignatureAlgorithm getSignatureAlgorithm() {
return SignatureAlgorithm.HS256;
}
}
关闭默认登录页面,放行SSO相关接口,适配自定义认证流程:
package com.jam.demo.sso.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
/**
* Spring Security配置类
* 关闭默认认证,放行SSO接口
* @author ken
*/
@Configuration
@EnableWebSecurity
publicclass SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 关闭CSRF(电商场景中,前端通过JWT令牌验证,无需CSRF防护)
.csrf(csrf -> csrf.disable())
// 关闭默认登录页面和注销功能
.formLogin(form -> form.disable())
.logout(logout -> logout.disable())
// 配置接口权限
.authorizeHttpRequests(auth -> auth
// 放行SSO相关接口(登录、令牌验证、页面跳转)
.requestMatchers("/api/sso/login", "/api/sso/verifyToken", "/sso/loginPage").permitAll()
// 放行Swagger3接口
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 其他接口需认证
.anyRequest().authenticated()
);
return http.build();
}
/**
* 密码加密器(BCrypt算法,电商场景标准加密方式)
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
returnnew BCryptPasswordEncoder();
}
}
负责JWT令牌的生成、验证、解析:
package com.jam.demo.sso.server.util;
import com.jam.demo.sso.server.config.JwtConfig;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.security.Key;
import java.util.Date;
import java.util.Map;
/**
* JWT工具类(生成、验证、解析令牌)
* @author ken
*/
@Slf4j
@Component
@RequiredArgsConstructor
publicclass JwtUtil {
privatefinal JwtConfig jwtConfig;
/**
* 生成JWT令牌
* @param userId 用户ID
* @param username 用户名
* @param claims 额外负载(如角色、权限)
* @return JWT令牌字符串
*/
public String generateToken(Long userId, String username, Map<String, Object> claims) {
// 计算过期时间
Date expireDate = new Date(System.currentTimeMillis() + jwtConfig.getExpire() * 1000);
// 构建JWT令牌
return Jwts.builder()
// 额外负载
.setClaims(claims)
// 主题(用户名)
.setSubject(username)
// 签发者
.setIssuer(jwtConfig.getIssuer())
// 签发时间
.setIssuedAt(new Date())
// 过期时间
.setExpiration(expireDate)
// 签名(密钥+算法)
.signWith(getSecretKey(), jwtConfig.getSignatureAlgorithm())
.compact();
}
/**
* 验证JWT令牌有效性
* @param token JWT令牌
* @return 有效返回true,无效返回false
*/
public boolean verifyToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token);
returntrue;
} catch (Exception e) {
log.error("JWT令牌验证失败,token:{},错误信息:{}", token, e.getMessage());
returnfalse;
}
}
/**
* 解析JWT令牌,获取负载信息
* @param token JWT令牌
* @return Claims 负载信息
*/
public Claims parseClaims(String token) {
if (ObjectUtils.isEmpty(token)) {
log.error("JWT令牌为空,无法解析");
thrownew IllegalArgumentException("JWT令牌不能为空");
}
try {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.error("JWT令牌解析失败,token:{},错误信息:{}", token, e.getMessage());
thrownew RuntimeException("JWT令牌解析失败", e);
}
}
/**
* 从令牌中获取用户名
* @param token JWT令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseClaims(token);
return claims.getSubject();
}
/**
* 从令牌中获取用户ID
* @param token JWT令牌
* @return 用户ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = parseClaims(token);
return claims.get("userId", Long.class);
}
/**
* 获取签名密钥
* @return Key
*/
private Key getSecretKey() {
return Keys.hmacShaKeyFor(jwtConfig.getSecretKey().getBytes());
}
}
package com.jam.demo.sso.server.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 系统用户实体类
* @author ken
*/
@Data
@TableName("sys_user")
publicclass SysUser {
/**
* 用户ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 加密密码
*/
private String password;
/**
* 用户昵称
*/
private String nickname;
/**
* 手机号
*/
private String phone;
/**
* 状态:1-正常,0-禁用
*/
private Integer status;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
package com.jam.demo.sso.server.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 角色实体类
* @author ken
*/
@Data
@TableName("sys_role")
publicclass SysRole {
/**
* 角色ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 角色名称(如:ROLE_USER/ROLE_ADMIN)
*/
private String roleName;
/**
* 角色描述
*/
private String roleDesc;
}
package com.jam.demo.sso.server.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.sso.server.entity.SysUser;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 用户Mapper接口
* @author ken
*/
@Repository
publicinterface SysUserMapper extends BaseMapper<SysUser> {
/**
* 根据用户名查询用户
* @param username 用户名
* @return SysUser
*/
SysUser selectByUsername(@Param("username") String username);
/**
* 根据用户ID查询角色列表
* @param userId 用户ID
* @return 角色名称列表
*/
List<String> selectRolesByUserId(@Param("userId") Long userId);
}
package com.jam.demo.sso.server.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.sso.server.entity.SysUser;
import com.jam.demo.sso.server.mapper.SysUserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.List;
/**
* 用户服务类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
publicclass SysUserService extends ServiceImpl<SysUserMapper, SysUser> {
privatefinal SysUserMapper sysUserMapper;
privatefinal PasswordEncoder passwordEncoder;
/**
* 根据用户名查询用户(含角色信息)
* @param username 用户名
* @return SysUser
*/
public SysUser getUserByUsername(String username) {
if (ObjectUtils.isEmpty(username)) {
log.error("查询用户失败:用户名为空");
thrownew IllegalArgumentException("用户名为空");
}
return sysUserMapper.selectByUsername(username);
}
/**
* 验证用户名和密码
* @param username 用户名
* @param password 原始密码
* @return 验证通过返回用户信息,失败返回null
*/
public SysUser verifyUsernameAndPassword(String username, String password) {
// 1. 查询用户
SysUser user = getUserByUsername(username);
if (ObjectUtils.isEmpty(user)) {
log.error("验证失败:用户不存在,username:{}", username);
returnnull;
}
// 2. 验证密码(BCrypt加密匹配)
if (!passwordEncoder.matches(password, user.getPassword())) {
log.error("验证失败:密码错误,username:{}", username);
returnnull;
}
// 3. 验证用户状态
if (user.getStatus() != 1) {
log.error("验证失败:用户已禁用,username:{}", username);
returnnull;
}
return user;
}
/**
* 根据用户ID查询角色列表
* @param userId 用户ID
* @return 角色名称列表
*/
public List<String> getRolesByUserId(Long userId) {
if (ObjectUtils.isEmpty(userId)) {
log.error("查询角色失败:用户ID为空");
thrownew IllegalArgumentException("用户ID为空");
}
return sysUserMapper.selectRolesByUserId(userId);
}
}
提供登录、令牌验证、登录页面跳转接口,供各业务系统调用:
package com.jam.demo.sso.server.controller;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Maps;
import com.jam.demo.sso.server.entity.SysUser;
import com.jam.demo.sso.server.service.SysUserService;
import com.jam.demo.sso.server.util.JwtUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* SSO认证中心接口层
* 提供登录、令牌验证等核心接口
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/sso")
@RequiredArgsConstructor
@Tag(name = "SSO认证接口", description = "电商平台单点登录核心接口")
publicclass SsoController {
privatefinal SysUserService sysUserService;
privatefinal JwtUtil jwtUtil;
/**
* 用户登录接口
* @param username 用户名
* @param password 密码
* @param redirectUrl 登录成功后重定向的地址(业务系统地址)
* @return 登录结果(含JWT令牌、重定向地址)
*/
@PostMapping("/login")
@Operation(
summary = "用户登录",
description = "用户输入账号密码,验证通过后生成JWT令牌,返回重定向地址",
parameters = {
@Parameter(name = "username", description = "用户名", required = true),
@Parameter(name = "password", description = "密码", required = true),
@Parameter(name = "redirectUrl", description = "登录成功重定向地址", required = true)
},
responses = @ApiResponse(
responseCode = "200",
description = "登录成功",
content = @Content(schema = @Schema(example = "{\"code\":200,\"msg\":\"登录成功\",\"data\":{\"token\":\"xxx\",\"redirectUrl\":\"http://localhost:8081/index\"}}"))
)
)
public JSONObject login(
@RequestParam String username,
@RequestParam String password,
@RequestParam String redirectUrl
) {
// 参数校验
StringUtils.hasText(username, "用户名不能为空");
StringUtils.hasText(password, "密码不能为空");
StringUtils.hasText(redirectUrl, "重定向地址不能为空");
// 验证用户名和密码
SysUser user = sysUserService.verifyUsernameAndPassword(username, password);
if (ObjectUtils.isEmpty(user)) {
log.error("登录失败:用户名或密码错误,username:{}", username);
return buildResult(401, "用户名或密码错误", null);
}
// 查询用户角色
List<String> roles = sysUserService.getRolesByUserId(user.getId());
// 构建JWT负载(含用户核心信息)
Map<String, Object> claims = Maps.newHashMap();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("roles", roles);
// 生成JWT令牌
String token = jwtUtil.generateToken(user.getId(), user.getUsername(), claims);
log.info("用户登录成功,生成JWT令牌,username:{},token:{}", username, token);
// 构建返回结果(含令牌和重定向地址)
Map<String, String> data = Maps.newHashMap();
data.put("token", token);
data.put("redirectUrl", redirectUrl);
return buildResult(200, "登录成功", data);
}
/**
* 验证JWT令牌有效性
* @param token JWT令牌
* @return 令牌验证结果(含用户信息)
*/
@GetMapping("/verifyToken")
@Operation(
summary = "验证JWT令牌",
description = "业务系统调用此接口验证令牌有效性,返回用户信息",
parameters = @Parameter(name = "token", description = "JWT令牌", required = true),
responses = @ApiResponse(
responseCode = "200",
description = "令牌有效",
content = @Content(schema = @Schema(example = "{\"code\":200,\"msg\":\"令牌有效\",\"data\":{\"userId\":1,\"username\":\"jam_user\",\"roles\":[\"ROLE_USER\"]}}"))
)
)
public JSONObject verifyToken(@RequestParam String token) {
// 参数校验
StringUtils.hasText(token, "令牌不能为空");
// 验证令牌
boolean valid = jwtUtil.verifyToken(token);
if (!valid) {
log.error("令牌无效,token:{}", token);
return buildResult(401, "令牌无效或已过期", null);
}
// 解析令牌,获取用户信息
Long userId = jwtUtil.getUserIdFromToken(token);
String username = jwtUtil.getUsernameFromToken(token);
List<String> roles = jwtUtil.parseClaims(token).get("roles", List.class);
// 构建返回结果
Map<String, Object> data = Maps.newHashMap();
data.put("userId", userId);
data.put("username", username);
data.put("roles", roles);
return buildResult(200, "令牌有效", data);
}
/**
* 跳转登录页面(供业务系统重定向)
* @param redirectUrl 登录成功后重定向的业务系统地址
* @return 登录页面(这里简化为返回HTML,实际项目中可集成Thymeleaf等模板引擎)
*/
@GetMapping("/loginPage")
@Operation(
summary = "跳转登录页面",
description = "业务系统未登录时,重定向到该接口,返回登录页面",
parameters = @Parameter(name = "redirectUrl", description = "登录成功重定向地址", required = true)
)
public String loginPage(@RequestParam String redirectUrl) {
// 简化实现:返回一个HTML登录页面,表单提交到/login接口
return"<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>电商平台-单点登录</title>\n" +
"</head>\n" +
"<body>\n" +
" <h3>电商平台单点登录</h3>\n" +
" <form action=\"/api/sso/login\" method=\"post\">\n" +
" <input type=\"hidden\" name=\"redirectUrl\" value=\"" + redirectUrl + "\">\n" +
" 用户名:<input type=\"text\" name=\"username\" required><br>\n" +
" 密码:<input type=\"password\" name=\"password\" required><br>\n" +
" <button type=\"submit\">登录</button>\n" +
" </form>\n" +
"</body>\n" +
"</html>";
}
/**
* 构建统一返回结果
* @param code 状态码
* @param msg 提示信息
* @param data 业务数据
* @return JSONObject
*/
private JSONObject buildResult(int code, String msg, Object data) {
JSONObject result = new JSONObject();
result.put("code", code);
result.put("msg", msg);
result.put("data", data);
return result;
}
}
package com.jam.demo.sso.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* SSO认证中心启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.sso.server.mapper")
publicclass SsoServerApplication {
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class, args);
}
}
server:
port:8080# SSO认证中心端口
servlet:
context-path:/
spring:
# 数据库配置
datasource:
url:jdbc:mysql://localhost:3306/jam_sso?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username:root
password:root
driver-class-name:com.mysql.cj.jdbc.Driver
# MyBatis-Plus配置
mybatis-plus:
mapper-locations:classpath:mapper/**/*.xml
type-aliases-package:com.jam.demo.sso.server.entity
configuration:
map-underscore-to-camel-case:true# 下划线转驼峰
log-impl:org.apache.ibatis.logging.stdout.StdOutImpl# 打印SQL日志
# JWT配置(可根据实际需求调整)
jwt:
secret:jam-sso-secret-key-2025-encrypt-123456# 生产环境需加密存储
expire:7200# 令牌过期时间(2小时)
issuer:jam-sso-server-dev# 开发环境标识
# SpringDoc-OpenAPI(Swagger3)配置
springdoc:
api-docs:
path:/v3/api-docs
swagger-ui:
path:/swagger-ui.html
operationsSorter:method# 按方法排序
商品系统作为SSO依赖方,需实现“未登录重定向到SSO认证中心”“登录后验证令牌”“创建本地会话”等功能。
com.jam.demo.sso.client.goods
├── config/ # 配置类(拦截器、跨域等)
├── controller/ # 接口层(商品查询、首页等)
├── util/ # 工具类(HTTP请求工具等)
├── GoodsClientApplication.java # 启动类
└── 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 https://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>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>sso-client-goods</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sso-client-goods</name>
<description>电商SSO依赖方-商品系统</description>
<properties>
<java.version>17</java.version>
<fastjson2.version>2.0.46</fastjson2.version>
<guava.version>33.2.1-jre</guava.version>
<springdoc.version>2.3.0</springdoc.version>
</properties>
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Swagger3 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
商品系统作为SSO依赖方,需允许与SSO认证中心的跨域交互:
package com.jam.demo.sso.client.goods.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 商品系统跨域配置
* @author ken
*/
@Configuration
publicclass CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 允许SSO认证中心的跨域请求
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
注册SSO登录拦截器,拦截商品系统的所有请求,校验登录状态:
package com.jam.demo.sso.client.goods.config;
import com.jam.demo.sso.client.goods.interceptor.SsoLoginInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* WebMvc配置类,注册SSO拦截器
* @author ken
*/
@Configuration
@RequiredArgsConstructor
publicclass WebMvcConfig implements WebMvcConfigurer {
privatefinal SsoLoginInterceptor ssoLoginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册SSO登录拦截器,拦截所有请求(排除Swagger接口)
registry.addInterceptor(ssoLoginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**");
}
}
用于商品系统调用SSO认证中心的令牌验证接口:
package com.jam.demo.sso.client.goods.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置类(用于跨服务HTTP请求)
* @author ken
*/
@Configuration
publicclass RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
returnnew RestTemplate();
}
}
封装HTTP GET/POST请求,简化调用SSO接口的逻辑:
package com.jam.demo.sso.client.goods.util;
import com.alibaba.fastjson2.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* HTTP请求工具类
* @author ken
*/
@Slf4j
@Component
@RequiredArgsConstructor
publicclass HttpUtil {
privatefinal RestTemplate restTemplate;
/**
* 发送GET请求
* @param url 请求地址
* @param params 请求参数
* @return 响应结果(JSONObject)
*/
public JSONObject doGet(String url, Map<String, String> params) {
if (!StringUtils.hasText(url)) {
log.error("GET请求失败:URL为空");
thrownew IllegalArgumentException("URL不能为空");
}
try {
// 拼接参数
StringBuilder urlBuilder = new StringBuilder(url);
if (params != null && !params.isEmpty()) {
urlBuilder.append("?");
for (Map.Entry<String, String> entry : params.entrySet()) {
urlBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
url = urlBuilder.substring(0, urlBuilder.length() - 1);
}
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(new HttpHeaders()),
String.class
);
return JSONObject.parseObject(response.getBody());
} catch (Exception e) {
log.error("GET请求失败,url:{},params:{},错误信息:{}", url, params, e.getMessage());
thrownew RuntimeException("HTTP GET请求失败", e);
}
}
/**
* 发送POST请求
* @param url 请求地址
* @param params 请求参数
* @return 响应结果(JSONObject)
*/
public JSONObject doPost(String url, Map<String, String> params) {
if (!StringUtils.hasText(url)) {
log.error("POST请求失败:URL为空");
thrownew IllegalArgumentException("URL不能为空");
}
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/x-www-form-urlencoded");
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(params, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
return JSONObject.parseObject(response.getBody());
} catch (Exception e) {
log.error("POST请求失败,url:{},params:{},错误信息:{}", url, params, e.getMessage());
thrownew RuntimeException("HTTP POST请求失败", e);
}
}
}
封装SSO相关逻辑(令牌验证、重定向地址拼接):
package com.jam.demo.sso.client.goods.util;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* SSO客户端工具类
* @author ken
*/
@Slf4j
@Component
@RequiredArgsConstructor
publicclass SsoClientUtil {
/**
* SSO认证中心地址
*/
@Value("${sso.server.url:http://localhost:8080}")
private String ssoServerUrl;
/**
* 当前客户端(商品系统)地址
*/
@Value("${sso.client.url:http://localhost:8081}")
private String ssoClientUrl;
privatefinal HttpUtil httpUtil;
/**
* 拼接SSO登录页面地址(含重定向参数)
* @param request 当前请求(用于获取完整的回调地址)
* @return SSO登录页面地址
*/
public String buildSsoLoginUrl(HttpServletRequest request) {
// 获取当前请求的完整地址(作为登录成功后的回调地址)
String currentUrl = getCurrentRequestUrl(request);
// 拼接SSO登录页面地址
return ssoServerUrl + "/sso/loginPage?redirectUrl=" + currentUrl;
}
/**
* 验证JWT令牌(调用SSO认证中心接口)
* @param token JWT令牌
* @return 验证结果(true=有效,false=无效)
*/
public boolean verifyToken(String token) {
if (!StringUtils.hasText(token)) {
log.error("令牌验证失败:令牌为空");
returnfalse;
}
// 调用SSO认证中心的令牌验证接口
String verifyUrl = ssoServerUrl + "/api/sso/verifyToken";
Map<String, String> params = Maps.newHashMap();
params.put("token", token);
try {
JSONObject result = httpUtil.doGet(verifyUrl, params);
// 校验响应码(200表示令牌有效)
return result.getInteger("code") == 200;
} catch (Exception e) {
log.error("令牌验证失败,token:{},错误信息:{}", token, e.getMessage());
returnfalse;
}
}
/**
* 获取当前请求的完整URL(用于回调)
* @param request HttpServletRequest
* @return 完整URL
*/
private String getCurrentRequestUrl(HttpServletRequest request) {
String scheme = request.getScheme(); // http/https
String serverName = request.getServerName(); // 域名/IP
int serverPort = request.getServerPort(); // 端口
String requestURI = request.getRequestURI(); // 请求路径
String queryString = request.getQueryString(); // 请求参数
StringBuilder url = new StringBuilder();
url.append(scheme).append("://").append(serverName);
if (serverPort != 80 && serverPort != 443) {
url.append(":").append(serverPort);
}
url.append(requestURI);
if (StringUtils.hasText(queryString)) {
url.append("?").append(queryString);
}
return url.toString();
}
/**
* 从SSO重定向的参数中解析JWT令牌
* @param request HttpServletRequest
* @return JWT令牌
*/
public String getTokenFromRequest(HttpServletRequest request) {
// 从请求参数中获取token(SSO登录成功后重定向时携带)
String token = request.getParameter("token");
if (StringUtils.hasText(token)) {
log.info("从请求参数中解析到JWT令牌:{}", token);
return token;
}
returnnull;
}
}
核心拦截器,校验用户登录状态,未登录则重定向到SSO认证中心:
package com.jam.demo.sso.client.goods.interceptor;
import com.jam.demo.sso.client.goods.util.SsoClientUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* SSO登录拦截器
* 校验用户登录状态,未登录则重定向到SSO认证中心
* @author ken
*/
@Slf4j
@Component
@RequiredArgsConstructor
publicclass SsoLoginInterceptor implements HandlerInterceptor {
privatefinal SsoClientUtil ssoClientUtil;
/**
* 预处理:校验登录状态
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
// 1. 检查本地会话是否已登录(存在用户信息)
Object userInfo = session.getAttribute("userInfo");
if (!ObjectUtils.isEmpty(userInfo)) {
log.info("本地会话已登录,放行请求:{}", request.getRequestURI());
returntrue;
}
// 2. 检查请求参数中是否有SSO返回的JWT令牌(登录成功后重定向)
String token = ssoClientUtil.getTokenFromRequest(request);
if (StringUtils.hasText(token)) {
// 3. 验证令牌有效性
boolean valid = ssoClientUtil.verifyToken(token);
if (valid) {
// 4. 令牌有效,创建本地会话(存储用户信息,避免重复验证)
session.setAttribute("userInfo", token); // 简化:直接存储令牌,实际可解析存储用户信息
session.setMaxInactiveInterval(7200); // 本地会话过期时间与JWT一致(2小时)
log.info("令牌验证通过,创建本地会话,放行请求:{}", request.getRequestURI());
returntrue;
} else {
log.error("令牌无效,重定向到SSO登录页:{}", request.getRequestURI());
}
}
// 5. 未登录/令牌无效,重定向到SSO认证中心登录页
String ssoLoginUrl = ssoClientUtil.buildSsoLoginUrl(request);
log.info("未登录,重定向到SSO登录页:{}", ssoLoginUrl);
response.sendRedirect(ssoLoginUrl);
returnfalse;
}
}
提供商品查询、首页等接口,验证SSO登录拦截逻辑:
package com.jam.demo.sso.client.goods.controller;
import com.alibaba.fastjson2.JSONObject;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* 商品系统接口层
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/goods")
@Tag(name = "商品接口", description = "电商商品系统核心接口")
publicclass GoodsController {
/**
* 商品系统首页
*/
@GetMapping("/index")
@Operation(summary = "商品系统首页", description = "验证SSO登录状态,返回用户信息和首页数据")
public JSONObject index(HttpSession session) {
// 获取本地会话中的用户信息(简化:存储的是JWT令牌)
String token = (String) session.getAttribute("userInfo");
Map<String, Object> data = Maps.newHashMap();
data.put("msg", "欢迎访问商品系统首页");
data.put("loginStatus", "已登录");
data.put("token", token);
JSONObject result = new JSONObject();
result.put("code", 200);
result.put("msg", "success");
result.put("data", data);
return result;
}
/**
* 根据商品ID查询商品信息
*/
@GetMapping("/{goodsId}")
@Operation(summary = "查询商品详情", description = "验证SSO登录状态后,返回商品详情")
public JSONObject getGoodsById(@PathVariable Long goodsId) {
// 模拟商品数据
Map<String, Object> goods = Maps.newHashMap();
goods.put("goodsId", goodsId);
goods.put("goodsName", "电商爆款商品-" + goodsId);
goods.put("price", 99.9);
goods.put("stock", 1000);
JSONObject result = new JSONObject();
result.put("code", 200);
result.put("msg", "success");
result.put("data", goods);
return result;
}
/**
* 退出登录(销毁本地会话,并重定向到SSO退出接口)
*/
@GetMapping("/logout")
@Operation(summary = "退出登录", description = "销毁本地会话,并重定向到SSO认证中心退出")
public void logout(HttpSession session, HttpServletResponse response) throws Exception {
// 1. 销毁本地会话
session.invalidate();
log.info("商品系统本地会话已销毁");
// 2. 重定向到SSO认证中心退出接口(实际项目中需实现SSO全局退出逻辑)
String ssoLogoutUrl = "http://localhost:8080/api/sso/logout";
response.sendRedirect(ssoLogoutUrl);
}
}
package com.jam.demo.sso.client.goods;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 商品系统(SSO Client)启动类
* @author ken
*/
@SpringBootApplication
public class GoodsClientApplication {
public static void main(String[] args) {
SpringApplication.run(GoodsClientApplication.class, args);
}
}
server:
port:8081# 商品系统端口
servlet:
context-path:/
# SSO配置
sso:
server:
url:http://localhost:8080# SSO认证中心地址
client:
url:http://localhost:8081# 当前客户端(商品系统)地址
# SpringDoc-OpenAPI(Swagger3)配置
springdoc:
api-docs:
path:/v3/api-docs
swagger-ui:
path:/swagger-ui.html
operationsSorter:method
jam_sso数据库,执行4.3.1中的SQL脚本;http://localhost:8081/api/goods/index。http://localhost:8080/sso/loginPage?redirectUrl=http://localhost:8081/api/goods/index);jam_user、密码123456,提交登录;http://localhost:8081/api/goods/index?token=xxx);verifyToken接口验证,验证通过后创建本地会话;http://localhost:8081/api/goods/1,无需再次登录,直接返回商品详情(本地会话生效);http://localhost:8081/api/goods/logout,销毁本地会话,重定向到SSO退出接口(可扩展全局退出逻辑)。verifyToken接口(跨域),返回200;verifyToken接口,返回401(令牌无效);电商平台SSO单点跨域的核心是“统一认证+跨域通信+令牌传递”,本文从底层逻辑出发,拆解了SSO的核心流程和跨域问题根源,结合实战案例实现了一套可直接运行的SSO系统:
在实际电商项目中,可基于此方案扩展:
通过这套方案,可彻底解决电商分布式系统的单点登录和跨域问题,兼顾用户体验、系统安全和架构扩展性。