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
(授权服务器)请求访问token
kungal.com/oauth
验证admin.kungal.com
的授权码,返回给admin.kungal.com
一个admin-token
admin.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
,可预见的将来会有多个服务端
这个实现。。。感觉好麻烦。。。
我研究研究吧