单点登录(SSO)是一种安全机制,允许用户通过一次登录即可访问多个相关联的系统或应用,无需重复输入用户名和密码,从而提升用户体验并增强安全性。
应用场景
跨域登录的需求
比如:我登录了淘宝后,访问飞猪或阿里云等其它域名下的网站时,无需重新输入账号密码即可登录。
传统的跨点登录:只能同域名单点登录
传统的跨点登录方式受限于Cookie的安全策略,Cookie默认只能在同域名下共享。这意味着用户在登录主站点后,无法直接在子站点或其他相关站点中保持登录状态,导致需要重新登录。
Cookie是不能跨域的,我们Cookie的domain属性是sso.a.com,在给app1.a.com和app2.a.com发送请求是带不上的。
那么如何解决这个问题呢?我们在设置Cookie时,将Cookie的domain属性设置为顶域(如a.com),这样所有该顶域的子域名(如app1.a.com、app2.a.com等)都能共享这个Cookie。
注意:这只实现了同域下的单点登录,但这还不是真正的单点登录 。
基于sso的单独认证服务:实现了跨域名登录
把SSO看作是一个单独的认证服务。不处理业务逻辑,只是做用户信息的管理以及授权给第三方应用。
单点登录要求不同域下的系统「一次登录,全线通用」,通常由独立的 SSO 系统记录登录状态、下发登录凭证ticket(token),各业务系统配合存储和认证 ticket(token)。
什么是种sso凭证?简单来说就是sso系统会生成session ID,并将Session ID存储到Cookie(sso.com域名)中,当访问其他关联系统时,浏览器就会携带Cookie重定向到SSO,SSO会通过Cookie(Session ID)来验证登录。
OAuth 2.0协议:更强大的身份验证解决方案
什么是OAuth 2.0协议?
OAuth 2.0协议是一种开放的授权标准,旨在为第三方应用提供一种安全且高效的方式来访问用户资源,而无需直接暴露用户的敏感信息(如密码)。它广泛应用于现代互联网服务中,尤其是在需要身份验证和授权的场景。
OAuth 2.0的核心思想是通过令牌(Token)机制实现授权管理。用户可以通过授权令牌授予第三方应用有限的权限,而不是直接共享账户密码。这种方式不仅提升了安全性,还简化了用户与服务之间的信任关系。
OAuth 2.0协议的优势
- 安全性更高
OAuth 2.0通过令牌机制避免了密码的直接传输,减少了密码泄露的风险。同时,它支持多种授权类型和作用域(Scope),可以精确控制第三方应用的权限范围。 - 简化授权流程
OAuth 2.0的授权流程更加简洁,用户只需一次授权操作,即可让第三方应用在一段时间内合法访问资源,而无需每次请求都重新输入密码。 - 支持多种应用场景
OAuth 2.0支持多种授权类型(如授权码、密码、令牌、客户端凭证等),适用于不同的使用场景。无论是Web应用、移动应用还是桌面应用,都可以通过OAuth 2.0实现安全的授权管理。 - 易于集成和扩展
OAuth 2.0的设计具有高度的灵活性和扩展性,能够轻松集成到现有的系统中。它还支持多种认证方式(如OpenID Connect),进一步增强了其功能。
演示地址:OAuth.com - OAuth 2.0 Simplified,玩一下就明白了。
功能原理
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期,"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
OAuth 2.0的运行流程如下图,摘自RFC 6749:
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向授权服务器申请令牌。
(D)授权服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
(1)令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
(2)令牌可以被数据所有者撤销,会立即失效。密码一般不允许被他人撤销。
(3)令牌有权限范围(scope)。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
客户端授权模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细。具体来说,一共分成四种授权类型(authorization grant),即四种颁发令牌的方式,适用于不同的互联网场景。
- 授权码模式(authorization code)
- 密码模式(resource owner password credentials)
- 简化(隐式)模式(implicit)
- 客户端模式(client credentials)
不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
授权码模式
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
适用场景:目前市面上主流的第三方验证都是采用这种模式
它的步骤如下:
(A)用户访问客户端,后者将前者导向授权服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,授权服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向授权服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)授权服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
总结:用户通过授权服务器获取授权码,再用该码在后端换取访问令牌和刷新令牌。
简化(隐式)模式
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)
简化模式不通过第三方应用程序的服务器,直接在浏览器中向授权服务器申请令牌,跳过了"授权码"这个步骤,所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
它的步骤如下:
(A)客户端将用户导向授权服务器。
(B)用户决定是否给予客户端授权。
(C)假设用户给予授权,授权服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端。
总结:用户通过授权服务器直接获取访问令牌和刷新令牌。
密码模式
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
适用场景:自家公司搭建的授权服务器
它的步骤如下:
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给授权服务器,向后者请求令牌。
(C)授权服务器确认无误后,向客户端提供访问令牌。
总结:用户通过客户端直接向授权服务器获取访问令牌和刷新令牌。
注意:
- 简化模式与密码模式的主要区别在于登录页的位置。
- 如果登录页位于授权服务器,则是简化模式。
- 如果登录页位于客户端,则是密码模式。
- 在密码模式下,客户端会直接获取用户的账号和密码,存在较大的安全隐患。
- 即使不考虑安全性问题,也需要在每个系统中单独实现登录页,增加了开发和维护的复杂性。
建议在实际应用中优先选择简化模式,以提高安全性和降低系统集成复杂度。
客户端模式
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行授权。
适用于没有前端的命令行应用,即在命令行下请求令牌。 一般用来提供给我们完全信任的服务器端服务。
它的步骤如下:
(A)客户端向授权服务器进行身份认证,并要求一个访问令牌。
(B)授权服务器确认无误后,向客户端提供访问令牌。
总结:用户无需登录,客户端直接向授权服务器获取访问与刷新令牌。
使用示例
因为是go语言项目,直接使用llaoj/oauth2nsso 项目,它是基于 go-oauth2 打造的独立的 OAuth2.0 和 SSO 服务,提供了开箱即用的 OAuth2.0服务和单点登录SSO服务。
authorization_code
1 获取授权code
请求方式
GET
/authorize
参数说明
参数 | 类型 | 说明 |
---|---|---|
client_id | string | 在oauth2 server注册的client_id,见配置文件oauth2.client.id |
response_type | string | 固定值:code |
scope | string | 权限范围,如:str1,str2,str3 ,str为配置文件中oauth2.client.scope.id的值 |
state | string | 表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值 |
redirect_uri | string | 回调uri,会在后面添加query参数?code=xxx&state=xxx ,发放的code就在其中 |
请求示例
# 浏览器请求
http://localhost:9096/authorize?client_id=test_client_1&response_type=code&scope=all&state=xyz&redirect_uri=http://localhost:9093/cb
# 302跳转,返回code
http://localhost:9093/cb?code=XUNKO4OPPROWAPFKEWNZWA&state=xyz
2 使用code
交换token
请求方式
POST
/token
请求头 Authorization
- basic auth
- username:
client_id
- password:
client_secret
Header Content-Type: application/x-www-form-urlencoded
Body参数说明
参数 | 类型 | 说明 |
---|---|---|
grant_type | string | 固定值authorization_code |
code | string | 1-1 发放的code |
redirect_uri | string | 1-1 填写的redirect_uri |
Response返回示例
{
"access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIyMjIyMjIiLCJleHAiOjE1ODU3MTU1NTksInN1YiI6InRlc3QifQ.ZMgIDQMW7FGxbF1V8zWOmEkmB7aLH1suGYjhDdrT7aCYMEudWUoiCkWHSvBmJahGm0RDXa3IyDoGFxeMfzlDNQ",
"expires_in": 7200,
"refresh_token": "JG7_WGLWXUOW2KV2VLJKSG",
"scope": "all",
"token_type": "Bearer"
}
使用建议
session的共享和持久化
问题1:多个SSO副本的会话不共享,导致用户在不同副本之间跳转时无法识别身份。
问题2:服务重启后,内存中的会话数据丢失。
sessions
提供了扩展接口,方便扩展使用其他的后端存储 session 内容。目前 GitHub 上已经有很多的第三方后端扩展了,详细 list 见sessions
库的 GitHub 首页:GitHub - gorilla/sessions: Package gorilla/sessions provides cookie and filesystem sessions and infrastructure for custom session backends.
我们只介绍基于 redis 的后端存储,其他的扩展感兴趣可自行研究。首先安装扩展:
go get github.com/boj/redistore
创建一个 redistore 的实例:
store, err = redistore.NewRediStore(config.Get().Redis.Default.DB,
"tcp", config.Get().Redis.Default.Addr, "",
config.Get().Redis.Default.Password, []byte(config.Get().Session.SecretKey))
参数依次为:
size
:最大空闲连接数;size
:最大空闲连接数;network
:连接类型,一般是 TCP;addr
:网络地址+端口;username
:redis的账户名,如果未启用,填空password
:redis 的密码,如果未启用,填空;keyPairs
:依次是 hashKey 和 blockKey(可省略),不再赘述。