SSO 与 OAuth 2.0
介绍
SSO 指的是单点登录,允许用户使用一组凭证登录多个相关的应用或服务,而不需要每次都输入凭证。
用户在登录一个应用后,可以无需重新登录直接访问其他已授权的应用。
SSO 旨在提高用户体验和简化身份验证流程。
OAuth 是一个开放标准,用于授权用户让第三方应用访问其在另一个服务提供者上存储的私密资源,而无需提供其密码。
OAuth 不是单点登录的技术,而是一种授权机制,允许用户授权访问其在服务提供者上的受保护资源。
需求
目前我们开发的论坛 KUN Visual Novel 要编写一个后台管理系统,它将会使用 Nextjs 作为技术栈,我们计划将其部署在 admin.kungal.com 这个域名上
我们需要明确下面几个问题
- 管理系统和论坛使用同一套用户及话题数据
- 管理系统和论坛均使用
JWT - 用户如果是管理员,只需要登录论坛,不需要再次登录管理系统即可使用管理系统
我们还需要考虑将来的扩展性问题
- 客户端不止一个,目前有
Vue3和Nextjs技术栈,以后会有Nuxt3,Nextjs,Solid-start,Svelte-kit等 - 服务端暂时只有一个,以后必然会有多个服务端
但是这个需求带来了这么几个问题
JWT只能在kungal.com域名下进行认证(准确来说是www.kungal.com),在admin.kungal.com会受到跨域影响,如何实现跨域鉴权认证- 如何在用户不用再次登录的情况下,实现跨域鉴权认证
- 用户直接访问
admin.kungal.com的时候,应该如何鉴权
实现这这些要求,SSO 和 OAuth 都可以做到,我们选择了 OAuth,理由是我们将来会有多个服务端
实际上,我们的需求完全不需要多个客户端多个服务端,为什么这么做呢?
我爱折腾,来打我呀
双 Token 认证
在我们目前的应用中,我们使用了 access-token 和 refresh-token 的双 token 认证方式,下面是它的基本原理
JWT Payload
这个叫 JWT 的载荷,说人话就是给 JWT 一些自定义的信息,它可以通过 JWT 进行传递,并且进行解码
它的 interface 如下
export interface JwtPayload {
[key: string]: any;
iss?: string | undefined;
sub?: string | undefined;
aud?: string | string[] | undefined;
exp?: number | undefined;
nbf?: number | undefined;
iat?: number | undefined;
jti?: string | undefined;
}生成 Token
我们的 Koa 后端使用了以下方式签发 JWT
export function generateToken(uid: number, name: string, expire: string) {
const payload: Payload = { iss: env.JWT_ISS, aud: env.JWT_AUD, uid, name }
const token = jwt.sign(payload, env.JWT_SECRET, { expiresIn: expire })
return token
}我们签发了这么几个属性
签发人:谁发放的 JWT
受众:JWT 发给谁
uid:用户的 ID
name:用户名
我们之后的鉴权都会使用这几个属性
这里需要注意认证和鉴权的区别,认证是证明你是你自己本人,鉴权是证明你有哪些权限
认证流程
下面的认证设置以下几个点
Http-only Cookie,仅在服务器端可访问,客户端不可通过 Javascript 访问该值,但是可以由用户本人在控制台的 Cookie 查看到,存放 refresh-token
localStorage,存放 access-token
鉴权步骤
- 用户登录成功,服务端生成两个 token,一个
refresh-token放在Http-only Cookie,还有一个access-token发送给客户端,客户端将其存放在localStorage refresh-token的过期时间为7d,access-token的过期时间为60m- 之后用户请求任何后端
API时均需在请求头带上Authorization: Bearer + access-token - 用户请求路由之前,先要经过一个
authMiddleware,这个authMiddleware会查看用户的Authorization头部是否有access-token,并验证其是否有效 - 倘若验证无效(过期,签发人和受众不对等等)则返回 401
- 客户端收到 401 的响应,尝试请求刷新
token的接口,向/api/auth/token/refresh发送POST请求 - 服务端收到刷新
token的请求,检查当前的refresh-token是否有效。如果有效,则返回新的access-token给客户端。如果无效,则返回错误,客户端跳转到登陆界面提示用户重新登录。 - 客户端到新的
access-token,重新请求服务端资源,服务端重新检查access-token,返回资源
经过上面的认证,可以实现的效果是
- 网站每过 60 分钟会自动刷新一次
access-token - 每过一周,
refresh-token会失效,用户需要重新登陆
安全性
Http-only Cookie Token
这是存放 refresh-token 的地方,较为安全,因为客户端的 Javascript 不可访问,可以防止 XSS
但是防 XSS 也不是万无一失的,因为攻击者可以直接在 XSS 代码中发送请求,这样 Http-only Cookie 就没有意义了
还有就是它无法防止 CSRF 的攻击
localStorage Token
这是存放 access-token 的地方,localStorage 天生防 CSRF 攻击,但是它不防 XSS
需要注意的是,一旦 refresh-token 被盗,那么 access-token 也就没有任何用了
SSO 的实现原理(JWT)
下面我说一下基于 JWT 是如何实现 SSO 的,感谢 yurzhang 对我的指点!
需要注意的是,本来的应用就已经使用了 access-token 和 refresh-token 的双 token 认证
鉴权过程
当用户通过 用户主页 -> 管理论坛 访问 admin.kungal.com 时
- 在
kungal.com通过URL 参数的形式将localStorage中的access-token给admin.kungal.com - 在
admin.kungal.com发送POST请求携带该access-token给/api/admin/login - 服务器解码该
access-token,获得用户权限,如果用户有权限,则返回一个新的admin-token给admin.kungal.com。注意,access-token中已经签发了标志用户权限的信息 admin.kungal.com拿到该admin-token,通过该 token 访问服务器资源。该token可以作为访问admin.kungal.com的凭证,并可以访问某些admin专属的接口资源
当用户直接访问 admin.kungal.com 时,注意,此时用户并未处于 kungal.com
- 服务器先检查
localStorage有没有admin-token,没有则跳转到kungal.com,例如kungal.com/sso?callback=admin.kungal.com/login - 用户现在处于
kungal.com/sso,服务器检查用户的refresh-token,检查无误后签发admin-refresh-token,并跳转回admin.kungal.com,例如admin.kungal.com/login?jwt=xxx - 用户现在处于
admin.kungal.com,服务器接收到admin-refresh-token,验证其有效性,验证无误后签发admin-token,然后有客户端保存在localStorage
需要注意的是,这里的 admin-refresh-token 过期时间很短,例如 30s,这样签发完 admin-token 后可以使其在短时间内失效,保证其安全性
OAuth 的实现原理(JWT)
OAuth 是一种授权框架,它有四种授权模式,我们这里仅研究最流行的授权码授权模式
OAuth 的核心有三个主体,用户,授权服务器,资源服务器
下面的过程,我们模拟 GitHub 的 OAuth 流程
鉴权过程
当用户直接访问 admin.kungal.com 时
- 用户在
admin.kungal.com点击登录 - 用户被
admin.kungal.com重定向到kungal.com/oauth kungal.com/oauth展示关于admin.kungal.com的有关信息,并询问用户是否授权admin.kungal.com访问自己账户的有关信息- 用户点击确认授权,
kungal.com/oauth将用户重定向到admin.kungal.com,并在 URL 中携带授权码 admin.kungal.com使用该授权码向kungal.com/oauth(授权服务器)请求访问tokenkungal.com/oauth验证admin.kungal.com的授权码,返回给admin.kungal.com一个admin-tokenadmin.kungal.com使用该admin-token请求资源服务器- 资源服务器验证该
admin-token,返回admin.kungal.com想要的数据
授权服务器负责验证用户身份、颁发令牌,而资源服务器负责存储受保护的资源,并根据令牌控制资源的访问。
CSRF
这个过程中很容易受到 CSRF 攻击,需要添加一些防范措施
在过程 admin.kungal.com 将用户重定向到 kungal.com/oauth 时,可能的 url 为
客户端 ID(client_id)
admin.kungal.com 需要证明自己是自己,所以需要先在 kungal.com 的数据库中记录自己预注册的数据,例如,网站标志,网站介绍等。
重定向地址 (redirect_uri)
重定向地址需要和 kungal.com 保存的 admin.kungal.com 预注册地址相等,否则,攻击者可以将 redirect_uri 换为自己的网站,让自己的网站拿到授权码
授权范围(scope)
admin.kungal.com 需要访问用户的哪些权限,例如头像,uid,萌萌点,用户名等
状态码(state)
这是为了防止 CSRF 攻击的,之后 admin.kungal.com 携带授权码向 kungal.com/oauth 获取 admin-token 时,可以验证这个请求的真伪
在过程用户确认授权之后, kungal.com/oauth 将用户重定向到 admin.kungal.com 时,可能的 url 为
https://admin.kungal.com/auth?code=XXXXXXXXXXXXXXXXXXXX&state=XXXXXXXXXXXXXXXXXXXXXXXX
授权码(code)
这是认证服务器颁发给请求服务器的授权码,请求服务器之后会使用这个授权码向认证服务器请求 admin-token
整个过程的示意图大概是这样的

https://dev.mi.com/console/doc/detail?pId=711#_2_1
实现
目前我开发的论坛 KUN Visual Novel 需要上线一个后台管理系统,我想着是时候折腾这个东东了。
因为前端一个 Vue3 一个 Nextjs,可预见的将来会有多个服务端
这个实现。。。感觉好麻烦。。。
我研究研究吧