常见的鉴权方式:Session/Cookie、Token(JWT)、OAuth、SSO
Session/Cookie
Session
基于Cookie
,Session
存在服务端,Cookie
存在客户端。
// 服务端 - Node
const http = require('http');
http.createServer((req, res) => {
req.headers.cookie // 获取客户端携带的Cookie
res.setHeader('Set-Cookie', 'cookie1=abc;') // 让客户端设置Cookie
res.end()
}).listen(3000)
客户端收到的Response Headers:
Set-Cookie: cookie1=abc;
客户端再次请求时,会在 Request Headers
中携带 Cookie
Cookie: cookie1=abc
Cookie
的容量有限,且容易被篡改,不安全,所以出现了Session
在客户端的Cookie
中,存储一个随机的Session Id
,Session
信息存储在服务端
// 模拟实现Cookie-Session模式
let session = {}, sessionKey = 'sid'
http.createServer((req, res) => {
const cookie req.headers.cookie
if(cookie && cookie.indexOf(sessionKey) !== -1) {
res.end('Come Back!')
// 存在,取出Session
const pattern = new RegExp(`${sessionKey}=(^;]+);?\s*`)
const sid = pattern.exec(cookie)[1]
session[sid] // Session
} else {
// 不存在Cookie-Session
const sid = (Math.random() * 999999).toFixed()
// 设置Cookie
res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
session[sid] = { name: 'Jack' }
res.end('Hi, Jack')
}
}).listen(3000)
Session
会话机制是一种服务端机制,它使用类似哈希表的结果来保存信息。
在客户端首次访问服务器时创建
Session
,并为这个Session
生成一个唯一的表示字符串sid
,作为键。如果需要,还会通过密匙对sid
进行签名处理,避免客户端篡改。把sid
作为Cookie
返回给客户端。客户端在随后的HTTP
请求中会携带这个sid
,服务端通过这个sid查询客户端的Session
信息。
哈希 Hash
- 哈希 Hash 的特点
- 把一个不定长摘要定长结果
- 摘要
xialaoshi -> x4sdfdsafsdafl3s3 --
按照某种规则进行摘要,防篡改 - 雪崩效应:牵一发动全身,如
xialaoshi
和zialaoshi
虽然只有一个字母不同,但摘要的结果完全不同,让人完全找不到规律。
- 常见哈希算法:
MD5、SHA1、hmac
- 摘要(加密算法)
- 对称
DES
- 非对称
RSA
- 对称
-
koa-session
中启用签名
const Koa = require('koa'), session = require('koa-session')
const app = new Koa()
app.keys = ['some secret'] //用于对cookie签名的key
const SESS_CONFIG = {
key: 'ply:sess', // cookie的键名
maxAge: 86400000,
httpOnly: true, // 仅允许服务器修改
signed: true // 启用签名
}
app.use(session(SESS_CONFIG, app))
Session
通常存在于内存、Redis
等介质中,但分布式部署无法共享内存,所以大多数情况下存储于Redis
中。
Token 令牌
Token
是一个独立的概念,是通过认证后发放的某个 凭证,这个凭证可以通过Cookie
、自定义http
请求头(如X-Token
),URL
的query
进行传递。所以Token
不存在跨域、跨平台的问题。
- 常见加密形式(携带信息):
用户Id + 过期时间 + 签名
-
Token
需要服务端查库验证是否有效,通常存储于Redis
中。
JWT - 服务端无状态化
JWT
Json Web Token
,是Token
的子集,是一种规则,采用Json
格式以及附带一些规范约束,将用户信息加密到 Token
中。服务器不保存任何用户状态信息(无需查库),而是通过保存的密匙校验 Token
,也就是说 服务端是无状态的,即使服务重启也不会有影响。
JWT
的加密过程(原理) 包括三部分:头部header
,载荷payload
,签名signature
,用 .
分隔。
-
Header
用于描述元信息,通常包括Token
类型和Signature
的加密算法
经过{ "alg": "HS256", // 默认的哈希(加密)算法 "typ": "JWT" // Token类型 }
base64
编码,得到第一部分 -
Payload
载荷信息,包含一些声明Claim
(实体描述,通常是一个用户信息,还有一些其他的元数据),即携带希望向服务器发送的信息。
这些信息可以是官方字段如iss(Issuer)、sub(Subject)、exp(Expiration time)
,也可以是自定义字段如userId,data
经过{ "iss": "JWT Builder", // 该JWT的签发者 "sub": "aaa@example.com", // 该JWT所面向的用户 "exp": 1448333419, // 过期时间 "userId": "b08f86af-35da-48f2-8fab-cef3904660bd" // 用户Id }
base64
编码,得到第二部分 -
Signature
第一部分+ "." +
第二个部分 拼接成字符串,通过Header
中声明的加密方式进行加盐secret
组合加密,得到第三部分。
加盐secret
也就是服务端提供的私钥,防篡改。解密Token
时仅仅使用设定的secret
,无需查询数据库,所以secret
一定不能泄露!!!
一旦客户端得到这个secret
,那就意味着客户端可以自我签发JWT
了!
第一部分 + "." +
第二个部分 + "." +
第三个部分 = JWT
由此可知,JWT(Token)
并不是为了隐藏或保密数据,而是为了确保它是被授权的,所以其中不能放诸如用户密码等 敏感信息。
// 前端
axios.interceptors.request.use(config => {
const token = window.localStorage.getItem('token')
if(token) {
// 通常约定 Bearer 是 JWT 的认证头部
config.headers.common['Authorization'] = 'Bearer ' + token
}
return config
}, err => Promise.reject(err))
Node Koa
后端:
koa、koa-router、koa-bodyparser、koa-static
jsonwebtoken、koa-jwt、koa2-cors(跨域)
对比
-
Cookie-Session
模式会受跨域、分布式部署、服务重启的影响; -
Token + Redis
模式需要每次都查询数据库,验证Token
是否有效; -
JWT
是一种认证授权机制,服务端无状态(无需存储),也就不能主动清除,不能踢离线。
OAuth
随着大量开放平台的出现,建立在开放平台之上的各种第三方应用也在大量冒出。出于对安全性和统一标准的要求,就有了OAuth
协议。
简单来说,OAuth是一种开放的协议,一种授权机制,它能为第三方应用提供一种简单的标准方式去访问需要用户授权的API服务。而且在为第三方提供服务的过程中,能够保护用户账号的安全。
2007年OAuth1.0
,存在严重漏洞。2009年6月OAuth1.0 Revision A
,修复了前一版本的安全漏洞。
2010年4月OAuth2.0
,与1.0
互不兼容。OAuth 2.0
也是目前最流行的授权机制,授权第三方应用,获取用户数据。
举个栗子:
我们想做一个关于新浪微博的第三方应用,那么就需要新浪微博提供一系列API,但前提是要去授权验证,这就用到OAuth
了。作为第三方开发者,我们并没有得到用户实质性的私密信息,只是获取了一个短期的进入令牌(必须保密,不能泄露),确保了用户账号的安全。
在应用OAuth
时,常常与OpenID
作比较:
OAuth
的关注点在于授权,OpenID
的侧重于证明鉴定。简单来说,OAuth
解决用户能(想)做什么,是 WHAT
的问题;OpenID
则为我们验证用户是谁,解决WHO
的问题
1.0 与 2.0 的授权方式不同
浏览器、目标服务器、第三方认证服务器
- 2.0授权过程分3步:
获取授权码Authorization Code -->
换取访问令牌access_token -->
访问授权资源
具体过程:浏览器向目标服务器发起认证请求-->
目标服务器通知浏览器重定向到第三方认证服务器-->
浏览器输入第三方认证的用户名和密码并发送-->
认证成功后回调给目的服务器授权码-->
目的服务器使用授权码向第三方认证服务器换取访问令牌 --> 第三方认证服务器返回令牌给目的服务器-->
目的服务器通知浏览器刷新页面,登录授权成功,使用令牌访问得到授权的资源
![](https://img.haomeiwen.com/i5131519/dc8aa15e06ff6d2e.png)
- 1.0授权过程分4步
- 请求未授权的
Request Token
(比如显示新浪微博的授权登录界面) - 请求授权的
Request Token
,用户确认授权,认证服务器返回授权的OAuth Token
- 用
OAuth Token
换取Access Token
,获得访问令牌oauth_token
和oauth_token_secret
- 用访问令牌访问得到授权的资源
- 请求未授权的
![](https://img.haomeiwen.com/i5131519/29772034de4998ab.png)
虽然
1.0
协议每个Token
都有加密,2.0
则不需要,但2.0
要求使用https
协议,安全性高于1.0
;而且2.0
充分考虑了客户端的各种姿态,提供了多种途径获取访问令牌,包括授权码,客户端私有整数,资源拥有者密码证书,刷新令牌等等,且验证过程更加简洁。相比之下,1.0
只有一个用户授权流程。
以Github
认证为例
需要先开通Github
账号,主页头像 --> Settings --> Developer settings --> OAuth Apps --> New OAuth App
注册一个第三方OAuth
认证的应用(应用名称、应用的主页URL
、认证回调的URL
),得到 Client ID,Client Secret
- 前端页面提供第三方
Github
登录的入口:
<a href="/github/login">login with github</a>
- 后端处理第三方认证:
const Koa = require('koa'),
Router = require('@koa/router'),
static = require('koa-static'),
querystring = require('querystring'),
axios = require('axios');
const config = {
client_id: '76a2dacb24ead9fc4596',
client_secret: '657124b0a8073d5538d12ad68f6d56114761283c'
}
// github 的认证接口
const OAuthURL = 'https://github.com/login/oauth/authorize';
// github 的Token接口
const TokenURL = 'https://github.com/login/oauth/access_token'
let thirdUser; // 保存第三方认证用户的数据
const app = new Koa();
const router = new Router();
app.use(static(__dirname + '/'));
router.get('/github/login', async ctx => {
// 重定向到Github认证接口
const url = OAuthURL + '?client_id=' + config.client_id;
ctx.redirect(url)
});
/**
Github上配置的认证回调接口:
http://localhost:7000/auth/github/callback
*/
router.get('/auth/github/callback', async ctx => {
// 获取到授权码Code
const code = ctx.query.code;
const params = Object.assign({ code }, config)
// 根据授权码 换取令牌Token
let resp = await axios.post(TokenURL, params)
const access_token = querystring.parse(resp.data).access_token
// 已经获取了Token,可以请求Github的接口了:获取用户数据
thirdUser = await axios.get('https://api.github.com/user?access_token='+access_token)
// 重定向到主页
ctx.redirect('/home')
});
router.get('/home', async ctx => {
const { login, avatar_url } = thirdUser.data
ctx.body = `
<h1>Hello ${login}</h1>
<img src="${avatar_url}" />
`
})
app.use(router.routes()).use(router.allowedMethods()).listen(7000);
SSO
单点登录:Single Sign On - SSO
是一个在多系统共存的环境下,用户登录其中一个系统,便可在其他所有系统中得到授权 而无需再次登录。包括 单点登录 和 单点注销 两部分。
Web系统由单系统发展成多个系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体;也就是说,用户访问Web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。
单点登录
单点登录可分为同域和跨域两种场景
同域
同域单点登录的核心就是共享Cookie
,所有子系统都使用同一个一级域名,通过不同的二级域名来区分。
举个栗子:公司有一个一级域名 zlt.com
,三个子系统:门户系统sso.zlt.com
,应用1app1.zlt.com
,应用2app2.zlt.com
- 设置它们的
Cookie
域为一级域名(domain=zlt.com
),这样就可以共享门户系统的Cookie
给所有使用*.zlt.com
作为域名的系统。 - 使用
Spring Session
等技术让所有系统共享Session
。这样只要门户系统登录之后,无论跳转 应用1 还是应用2,都能通过门户系统Cookie
中的sessionId
读取到Session
中的登录信息,实现单点登录。
然而,共享Cookie
的方式存在众多局限。首先,应用群域名必须统一;其次,应用群各系统使用的技术(至少是Web
服务器)要相同,否则Cookie
的key
值(Tomcat
为JSESSIONID
)不同,无法维持会话,即共享Cookie
的方式是无法实现跨语言技术平台登录的,比如java、php、.net
系统之间;第三,Cookie
本身不安全。
跨域
单点登录之间的系统域名不同,无法共享Cookie
,需要一个独立的 授权系统/
认证中心。子系统本身不参与登录,而是通过认证中心验证身份,完成间接授权。当一个系统登录成功后,认证中心会颁发一个令牌给子系统,子系统拿着令牌去获取受保护的资源。为了减少频繁认证,各个子系统在被认证中心授权以后,会建立一个局部会话,在一定时间内无需再次向认证中心发起认证。
![](https://img.haomeiwen.com/i5131519/b006b3b70f62c6a5.png)
- 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至
SSO
认证中心,并将自己的地址作为参数 -
SSO
认证中心发现用户未登录,将用户引导至登录页面 - 用户输入用户名密码提交登录申请
-
SSO
认证中心校验用户信息,创建用户与SSO
认证中心之间的会话,称为全局会话,同时创建授权令牌 -
SSO
认证中心带着令牌跳转回最初的请求地址(系统1) -
系统1拿到令牌,去
SSO认
证中心校验令牌是否有效(安全性,必须校验) -
SSO
认证中心校验令牌,返回有效,注册系统1 - 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
- 用户访问系统2的受保护资源,系统2发现用户未登录,跳转至
SSO
认证中心,并将自己的地址作为参数 -
SSO
认证中心发现用户已登录,跳转回系统2的地址,并附上令牌 -
系统2拿到令牌,去
SSO
认证中心校验令牌是否有效,SSO
认证中心返回有效,注册系统2 - 系统2使用该令牌创建与用户的局部会话,返回受保护资源
用户登录成功之后,会与SSO
认证中心及各个子系统建立会话,用户与SSO
认证中心建立的会话称为全局会话,与各个子系统建立的会话称为局部会话。局部会话建立之后,用户访问子系统受保护资源将不再通过SSO
认证中心。
全局会话与局部会话的约束关系:
- 局部会话存在,全局会话一定存在
- 全局会话存在,局部会话不一定存在
- 全局会话销毁,局部会话必须销毁
SSO
系统登录后,带着令牌跳回原业务系统时,业务系统还要拿令牌再次访问SSO
进行验证,这个步骤看似有点多余。如果SSO
登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?!
其实问题是很严重的,如果在SSO
没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的!
单点注销
单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁;
![](https://img.haomeiwen.com/i5131519/076a9454aa141f52.png)
SSO
认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作
- 用户向
系统1
发起注销请求; -
系统1
根据与用户建立的会话id
拿到令牌,向SSO
认证中心发起注销请求; -
SSO
认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址,向所有注册系统发起注销请求; - 各注册系统接收
SSO
认证中心的注销请求,销毁局部会话; -
SSO
认证中心引导用户至登录页面。
网友评论