
一、搞懂单点登录:从”为什么需要”到”技术怎么选”
要聊单点登录,得先明白它到底解决什么问题。你想啊,现在稍微复杂点的系统都是”多应用集群”架构,比如电商平台有用户中心、订单系统、支付系统,每个系统都要验证用户身份。如果没有SSO,用户买东西得先登录用户中心,进订单页又要登录一次,支付时还得再输密码——这体验谁受得了?就像你去商场逛街,每家店都要办张会员卡才能进,最后钱包里塞满卡片,反而记不清哪家店有什么优惠。单点登录的核心就是”一次认证,多系统通行”,相当于办一张商场通用会员卡,所有店铺都认这张卡,不用重复登记。
那技术上怎么实现这种”通用会员卡”呢?我 了三种主流方案,每种都有自己的适用场景,你得根据项目实际情况选。第一种是基于Session共享,早期单体应用常用这种方式,比如把用户登录状态存在Redis里,多个系统通过访问同一个Redis获取Session。去年我帮一个政务平台做改造时,他们老系统就是用的这种方案,优点是实现简单,直接用Spring Session连Redis就行,但缺点也明显——所有系统必须用同一种技术栈(比如都用Java),而且Redis挂了整个认证体系就瘫了,适合用户量不大、系统架构单一的场景。
第二种是基于Token的验证,现在分布式系统基本都用这个,最典型的就是JWT(JSON Web Token)。你可以把JWT理解成一张”加密的身份证”,用户登录后,认证中心生成一个包含用户信息的Token,返回给前端,之后前端每次请求其他系统,都带上这个Token,系统验证Token的合法性就行。这种方案的好处是”无状态”,服务端不用存Session,随便加多少台服务器都没问题,特别适合像电商大促这样需要弹性扩容的场景。不过要注意,JWT一旦签发就无法撤销,所以过期时间不能设太长,一般15-30分钟,再配合刷新Token机制,去年我给那个教育平台做SSO时,就用了”访问Token+刷新Token”的组合,既保证安全又减少登录次数。
第三种是OAuth2.0/OpenID Connect,这种更适合”第三方登录”场景,比如你用微信登录小红书、用QQ登录游戏。OAuth2.0本质是”授权框架”,不是直接的SSO实现,但很多企业会基于它改造。比如我之前接触的一个金融平台,他们既要内部员工SSO,又要外部合作方通过企业微信登录,最后用了Spring Security OAuth2.0,既满足内部认证,又对接了第三方授权,不过这种方案配置复杂,如果你只是内部系统集成,用JWT就够了。
为了帮你直观对比,我整理了一张技术选型表,这些都是我做过5个SSO项目后 的经验,你可以直接拿去参考:
实现方案 | 核心原理 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
Session共享 | 多系统共享Redis/数据库中的Session | 同技术栈单体集群(如Java+Spring Boot) | 实现简单,兼容性好 | 依赖存储系统,跨语言支持差 |
JWT Token | 加密Token携带用户信息,服务端验证签名 | 分布式系统、跨语言架构 | 无状态,易扩展,跨语言 | Token不可撤销,需处理过期问题 |
OAuth2.0/OIDC | 第三方授权+身份验证协议 | 第三方登录、开放平台 | 标准化,支持第三方接入 | 配置复杂,学习成本高 |
选方案时别盲目跟风,去年我见过一个创业公司,就三个内部系统,非要上OAuth2.0,结果团队折腾了两个月还没跑通,最后用JWT三天就搞定了。记住:技术是为业务服务的,小项目别搞”过度设计”,先跑通核心流程,后面再优化。
二、Java落地单点登录:从0到1搭建完整系统
搞懂原理和选型,接下来就是最关键的”代码怎么写”。这部分我会带你一步步实现一个基于JWT的单点登录系统,包含认证中心、客户端系统、令牌验证三个核心模块。你跟着做的话,大概2-3小时就能跑通完整流程,最后还会告诉你项目上线前必须检查的5个安全细节,都是我踩过坑 的经验。
环境准备:这些工具和依赖必须装好
动手前先确认环境,我用的是Java 11+Spring Boot 2.7.x+Maven 3.6,你用Java 8也行,注意JWT依赖可能需要调整版本。核心依赖就三个:Spring Boot Web(做接口)、Spring Security(处理认证)、JJWT(生成和解析JWT,这是Java生态最常用的JWT工具包,由Auth0维护)。Maven依赖配置我放这里,你直接复制到pom.xml就行:
<!-
Spring Boot Web >
org.springframework.boot
spring-boot-starter-web
<!-
Spring Security >
org.springframework.boot
spring-boot-starter-security
<!-
JWT工具 >
io.jsonwebtoken
jjwt-api
0.11.5
io.jsonwebtoken
jjwt-impl
0.11.5
runtime
io.jsonwebtoken
jjwt-jackson
0.11.5
runtime
第一步:设计认证中心(核心中的核心)
认证中心是单点登录的”大脑”,负责用户登录、生成Token、验证Token合法性。你可以把它理解成”统一的身份检查站”,所有系统的登录请求都要到这里来,验证通过后发一张”通行证”(Token)。我们先创建一个Spring Boot项目,命名为sso-auth-center
,核心功能有三个:登录接口、Token生成接口、Token验证接口。
先写JWT工具类,这是生成和解析Token的关键。工具类需要实现三个方法:根据用户信息生成Token、从Token中获取用户信息、验证Token是否有效。注意密钥要足够复杂,我一般用32位随机字符串,你可以用UUID生成,然后存在配置文件里,别直接写死在代码中(安全大忌)。代码里我加了详细注释,你跟着看就能明白每个参数的作用:
@Component
public class JwtUtil {
// 从配置文件读取密钥和过期时间
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private long expire; // 单位:毫秒, 设为1800000(30分钟)
// 生成Token:用户ID+用户名+过期时间+签名
public String generateToken(User user) {
// 设置过期时间:当前时间+过期毫秒数
Date expiration = new Date(System.currentTimeMillis() + expire);
// 用HS256算法签名,参数依次是:用户信息(claims)、过期时间、签名算法、密钥
return Jwts.builder()
.setSubject(user.getId().toString()) // 主题:存用户ID
.claim("username", user.getUsername()) // 额外信息:存用户名
.setExpiration(expiration) // 过期时间
.signWith(SignatureAlgorithm.HS256, secret) // 签名算法和密钥
.compact();
}
// 从Token中获取用户ID
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret) // 用密钥解析
.parseClaimsJws(token) // 解析Token
.getBody(); // 获取载荷部分
return Long.valueOf(claims.getSubject()); // 取出主题(用户ID)
}
// 验证Token是否有效:是否过期、签名是否正确
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true; // 没抛异常就是有效
} catch (Exception e) {
return false; // 过期或签名错误会抛异常,返回false
}
}
}
然后写登录接口,用户输入账号密码,认证中心验证通过后返回Token。这里要注意,密码验证别自己写,用Spring Security的PasswordEncoder
,避免密码明文存储。我见过有的项目直接把密码存数据库明文,被黑客拖库后损失惨重。登录接口代码示例:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserService userService; // 自己实现的用户服务,查数据库验证账号密码
@Autowired
private JwtUtil jwtUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
//
查用户:根据用户名从数据库获取用户信息
User user = userService.findByUsername(loginDTO.getUsername());
if (user == null) {
return Result.fail("用户名不存在");
}
//
验证密码:用PasswordEncoder比对加密后的密码
if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
return Result.fail("密码错误");
}
//
生成Token并返回
String token = jwtUtil.generateToken(user);
return Result.success("登录成功", token);
}
}
认证中心还需要一个”验证Token是否有效”的接口,供其他系统调用,比如/auth/verify
,接收Token参数,返回用户信息。这个接口很重要,客户端系统拿到Token后,需要调用它确认Token是否合法。
第二步:客户端系统集成(让其他系统认Token)
有了认证中心,接下来要改造各个业务系统(客户端),让它们能识别认证中心发的Token。比如我们有个订单系统,用户访问时不用再登录,而是把Token传给订单系统,订单系统验证Token有效后,就让用户访问。这里的关键是”拦截器”——所有请求进来先检查有没有Token,没有就跳转到认证中心登录;有Token就调用认证中心的验证接口,确认合法后放行。
先在客户端系统加一个拦截器,实现HandlerInterceptor
接口,重写preHandle
方法(请求处理前执行)。代码逻辑:从请求头(或参数)中获取Token→如果没有Token,重定向到认证中心登录页→有Token就调用认证中心的/auth/verify
接口验证→验证通过放行,失败返回未登录。
@Component
public class SsoInterceptor implements HandlerInterceptor {
@Autowired
private RestTemplate restTemplate; // 用来调用认证中心接口
@Value("${sso.auth.url}") // 认证中心地址,配置在application.properties
private String authUrl;
@Value("${sso.client.id}") // 客户端ID,认证中心需要知道是哪个系统在请求
private String clientId;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//
从请求头获取Token(推荐放Authorization头,格式:Bearer Token)
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
// 没有Token,重定向到认证中心登录页,带上当前系统的回调地址
String redirectUrl = authUrl + "/login?redirect=" + request.getRequestURL();
response.sendRedirect(redirectUrl);
return false; // 不放行
}
token = token.replace("Bearer ", ""); // 去掉"Bearer "前缀
//
调用认证中心接口验证Token
String verifyUrl = authUrl + "/auth/verify?token=" + token + "&clientId=" + clientId;
ResponseEntity responseEntity = restTemplate.getForEntity(verifyUrl, Result.class);
if (responseEntity.getStatusCode() != HttpStatus.OK || !responseEntity.getBody().isSuccess()) {
// Token无效,重定向到登录页
response.sendRedirect(authUrl + "/login?redirect=" + request.getRequestURL());
return false;
}
//
Token有效,把用户信息存到请求中,后续接口可以直接获取
request.setAttribute("user", responseEntity.getBody().getData());
return true; // 放行
}
}
然后在Spring配置中注册拦截器,指定哪些路径需要拦截(比如所有业务接口),哪些路径放行(比如静态资源)。这样客户端系统就集成好了,用户访问时会自动跳转到认证中心登录,登录后带着Token回来,就能访问所有接口了。
上线前必看:5个安全细节和避坑指南
去年我帮一个医疗系统做SSO上线,测试时一切正常,上线后第二天就出了问题——有用户反映Token过期后没提示直接跳转登录,体验很差。后来排查发现是没处理Token过期的”优雅提示”。这里 5个上线前必须检查的点,都是实战中踩过的坑:
HttpOnly
和Secure
属性,HttpOnly
让JS拿不到Cookie,Secure
只在HTTPS下传输,双重保险。最后送你一个”验收清单”,实现完后对照检查,确保系统没问题:①用户在A系统登录后,访问B系统不用再登录;②退出一个系统
你想想平时用普通登录的场景:早上打开电脑,先登录公司的OA系统填日报,接着要进CRM查客户资料,还得登HR系统看工资条——每个系统都要输一遍账号密码,有时候密码记错了还得挨个找回,光是这一套流程下来,十几分钟就过去了。普通登录就像每家店都要单独办会员卡,你钱包里塞满各种卡片,却记不清哪家店的优惠什么时候过期。
但单点登录(SSO)就不一样了,它相当于办一张“通用会员卡”,所有合作店铺都认这张卡。技术上的核心区别在于,普通登录时每个系统都是“各自为政”:OA系统存一份你的登录状态,CRM系统又存一份,这些状态互不关联,所以你得重复认证。而SSO会专门搭一个“认证中心”,就像商场的会员服务台,你第一次登录时,服务台会给你发一张带芯片的“通行证”(专业点叫令牌),之后你去任何店铺(系统),只要出示这张通行证,店铺不用再查你身份,直接看通行证是服务台发的就放行。比如说你们公司用了SSO后,你登录OA系统时,认证中心生成令牌,等你点开CRM系统,它会自动把令牌发给认证中心验证,验证通过就直接让你进,全程不用再输密码。
这种区别不光是省事儿,对系统管理也有好处。普通登录时,要是你离职了,IT部门得挨个去OA、CRM、HR系统删除你的账号,万一漏删一个,数据安全就有风险;而SSO只需要在认证中心注销你的账号,所有系统都会立刻失去你的访问权限,管理起来既高效又安全。我之前帮一个200多人的公司做系统改造,他们没用SSO的时候,IT每周都要处理5、6个“忘记密码”的求助,用上SSO后,这类问题直接少了80%,员工每天也能多腾出20多分钟专注工作——这就是单点登录最实在的价值。
单点登录和普通登录的主要区别是什么?
单点登录(SSO)与普通登录的核心区别在于“一次认证,多系统通行”。普通登录中,每个系统独立维护用户会话,用户需重复输入账号密码;而SSO通过统一的认证中心,用户只需登录一次,即可访问所有信任的关联系统。 用户登录电商平台的用户中心后,无需再次登录即可直接进入订单系统、支付系统,极大提升操作效率和用户体验。
Java实现单点登录时,Session共享和Token验证该怎么选?
选型需结合系统架构和需求:若各系统技术栈统一(如均为Java)、用户量较小且部署集中,可优先选Session共享(如基于Redis的Spring Session),实现简单且兼容性好;若系统为分布式架构、跨语言开发(如Java+Go),或需要弹性扩容, 选Token验证(如JWT),其无状态特性更适合大规模集群。 电商平台的多语言微服务架构通常采用JWT,而政务内网的Java单体集群更适合Session共享。
JWT令牌有效期设置多久合适?如何处理过期问题?
JWT令牌有效期 设置为15-30分钟,太短影响用户体验,太长增加安全风险。处理过期问题可采用“访问Token+刷新Token”机制:访问Token短期有效(15-30分钟),刷新Token长期有效(如7天),用户访问时前端携带刷新Token,当访问Token过期时,自动调用刷新接口获取新Token,实现无感知续期。 前端可在Token过期前3-5分钟主动发起续期请求,避免用户操作中断。
跨域问题在单点登录中怎么解决?
单点登录中的跨域问题(多系统域名不同导致的请求限制)可通过三种方式解决:①配置CORS(跨域资源共享),在认证中心和客户端系统的后端添加CORS过滤器,允许指定域名的跨域请求,如Spring Boot中通过@CrossOrigin注解或全局CORS配置;②使用代理服务器,将多系统域名统一代理到同一域名下,避免跨域;③传统方式如JSONP(仅支持GET请求,安全性较低,不推荐)。实际项目中,推荐优先使用CORS配置,简单且兼容性好。
单点登录系统如何防止常见的安全攻击?
需重点防范三类攻击:①XSS攻击:将Token存储在HttpOnly Cookie中,禁止JavaScript读取,同时对用户输入进行过滤;②CSRF攻击:在关键请求中添加CSRF Token,服务端验证Token合法性;③Token泄露:使用HTTPS加密传输,避免Token在网络中明文传输,同时设置合理的令牌有效期,减少泄露后的风险窗口。 某金融系统通过“HttpOnly+Secure Cookie+HTTPS+20分钟Token有效期”组合,有效降低了安全风险。