SSO 简单介绍

SSO(Single Sign-On,单点登录)是一种身份认证过程,它允许用户使用一组凭证(如用户名和密码)登录到多个相关但是技术上独立的系统。一旦用户被认证,他们就可以访问所有使用SSO服务的应用程序或服务,而无需为每个系统重新输入凭证。

在实习的时候就思考过这个服务的实现方案,现在有时间了就打算了解一下技术方案和实现。这篇博客就是我自己思考和实践的过程。

GitHub 地址

思考过程

首先从业务的视角去思考这个服务的架构,虽然业务逻辑视角上SSO服务是鉴权和认证的身份提供者(Identity Provider, IdP),而其他资源服务是服务的服务提供者(Service Provider, SP),SSO供足够信任的权限凭证,资源服务才能对用户提供服务或返回资源,业务上资源服务是依赖 SSO 服务的。

但是设计上,服务提供者分别是自己的业务线,自己的运行逻辑,甚至有自己的内部的权限管理系统,这个时候 SSO 应该以一种低侵入的设计方式能过丝滑接入所有资源服务,对服务的业务入侵要尽可能地少,这是对整个实现方案的一个前置视角。

功能思考

接下来是网上搜集的关于 SSO 应该(或许)提供的功能:

  • 同域名下的单点登录登出
  • 权限管理,这个我暂时有比较不一样的意见。
  • 跨域登录,或者跨域 SSO,本质上其实和同域名下的 SSO 是一样的实现思路,

SSO 的权限管理这个功能,我实际上是有不一样的想法的,资源服务的鉴权管理功能不应该交给 SSO 服务,或者说鉴权管理服务应该是另一个专注于权限管理的微服务,而不应该是 SSO 该干的事,下面是我给出的理由。

如果 SSO 只提供登录会话的管理功能,也就是只管理用户的管理状态,那么整个系统的架构应该是:用户在 SSO 注册,并且受 SSO 管理登录会话状态。其他所有需要依赖用户登录状态的资源服务接入 SSO 服务,向 SSO 询问用户的登录状态,同时 SSO 服务并不需要存储或者记忆任何有关资源服务的信息。

对于权限管理,权限管理本质由三个部分组成:用户,资源,行为,鉴权的过程本质是判断用户对资源的行为是否允许,基于这个视角,权限管理的方案可以分为两种,ABAC(Attribute-Based Access Control)基于属性的访问控制,和RBAC(Role-Based Access Control)基于角色的访问控制,后续会详细介绍这两种方案。

于是我们可以得出的结论是,权限管理的逻辑本质是:资源和行为限制角色或者属性,用户作为角色或者具备属性,比对两者得出是否允许。访问控制涉及三个方面的要求,而SSO本质只对用户的登录会话状态负责,所以权限管理本不应该耦合到 SSO 登录会话管理服务里面。

简单的例子就是:资源服务提供者拉黑一个用户,并不需要通知 SSO,SSO 只关心用户是否在线就行。

在 ABAC 和 RBAC 中,SSO 更像是一个角色的提供者(在线用户角色)或者属性的认证方(在线属性),而资源服务应该根据这些做更细致和具体的权限管理。

从这个视角来看,其实权限管理服务本质上是一个角色或属性的管理平台,资源注册行为对应的角色或者属性,用户也在管理平台认领这些角色或者属性,然后判断是否同访问。

这是两者的边界,SSO 只对用户的在线属性负责,鉴权服务同时对资源,行为和用户三方负责。

所以这样设计或许更加合理的:

用户发起对资源的请求
= = = = 》资源服务询问权限管理服务是否鉴权通过
= = = = 》鉴权服务发现需要得知用户的在线状态
= = = = 》访问 SSO 服务获取在线状态(认证在线角色或者在线属性)
= = = = 》完成鉴权

关于跨域认证,这个其实更多涉及到跨域资源获取,可以采用 OAuth2.0 协议完成,所以这个也算不上是SSO的功能范畴,但是同时也借鉴了 OAuth2.0 的思想。

接口设计

  • SSO-Server
  • SSO-client
  • Resource-Server

Restful-API

SSO-Server

登录界面: /sso/login?original_url=[URL编码]

默认登录成功界面:/sso/success

登录接口: /sso/api

接口描述 URI 方法 Request \ Response
登录接口 /login POST Request : userId, password, originalUrl
Response : 设置 cookie 和重定向
登录校验接口 /validate GET Request : @Nullable acc_token
Response : valid, updateToken
登出接口 /logout POST Request : 无
Response : 删除 Cookie
Token 续期接口 /renewal POST Request:updateToken
Response : 更新 cookie
SSO-client SSO

Spring MVC 拦截器 TokenParseInterceptor 避免对业务的侵入

暂时的实现方案是将登录信息,也就是用户名放入 TTL 中作为请求的上下文,没有上下文至少表示这次请求不是在线用户发起的。

调用验证接口的方案暂时用的 HttpClient,为什么不同 RPC 是因为 RPC 存在服务之间的依赖关系,想了想感觉还是不太合适。

@Configuration
public class WebInterceptorConfig implements WebMvcConfigurer {

    @Value("${sso.validate-location}")
    private String ssoServer;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenParseInterceptor(ssoServer));
    }
}
Resource-Server

资源界面:/user.html

资源API 基础路径:/user/api

  • [GET] /info 获取基础信息接口
JWT

标准结构示例:

{
  "iss" : "Issuer(发行人):表示JWT的发行者。",
  "exp" : "Expiration Time(过期时间):表示JWT的过期时间。",
  "sub" : "Subject(主题):通常指代JWT的主体,例如用户的ID。",
  "aud" : "Audience(受众):表示JWT的预期接收者。",
  "nbf" : "Not Before(生效时间):表示JWT在此时间之前不可用。",
  "iat" : "Issued At(签发时间):表示JWT的签发时间。",
  "jti" : "JWT ID(JWT的唯一标识符):JWT的唯一标识符。"
}

在这个 Demo 中 token 中只需要 sub 和 exp,sub 存储 UserID,exp 存储过期时间。

代码流程

SSO 未登录时单点登录流程(默认采用 Cookie 登录的方式)

SSO_login.png

SSO 令牌续期流程

SSO_renew.png

要点思考

SSO 的高性能和高可用:

在访问量很少的时候,可以直接考虑单点负载,登录登出状态直接保存在缓存里就行,服务下线则用户下线

在访问量增加的时候,SSO 的性能瓶颈会在 JWT 解析和用户登录(涉及到数据库瓶颈)这两个功能上面,所以采用水平扩展然后 Redis 作为一致性缓存的方案即可。

JWT 的续期:

提供续期接口,SSO 客户端每次验权的时候,SSO 服务可以返回一个 JWT 续期的 Advice(随机码,有效时常5分钟),客户端可以自己选择是否在响应头里设置这个 Advice,拦截器可以直接在响应头里面设置,前端 js 拿到这个 advice 后再去请求续期接口,会更新 JWT。

CSRF 防护:

一般来说,可以使用这个方案:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf(); // 启用CSRF保护
    }
}

但是深入原理了解得知,Spring Security中的CSRF(跨站请求伪造)保护原理主要基于同步器令牌模式(Synchronizer Token Pattern),而这个随机字段是添加在 Session 里面的,对于单节点来说,这个当然没有上什么问题,但是,对于集群来说,问题就大了。

对于后端集群,我们通常采用 Nginx 作为反向代理,Nginx 的默认反向代理模式是轮流代理,也就是会导致前后两次访问路由不到同一个服务器上,Session 就没啥用了,这个时候还是得用 Redis 代替 Session 的功能。

所以其实最好的方案是把 token 塞到 header 里面,因为跨域伪造请求攻击的原理是浏览器默认会携带 cookie,但是自定义 header 字段却不能默认携带,攻击页面本身也无法拿到其他域名下的 cookie 字段。

其他内容(待填坑)

OAuth2.0 认证协议

RBAC 和 ABAC 权限访问控制