最近在重构公司以前产品的前端代码,摈弃了以前的session-cookie鉴权方式,采用token鉴权,忙里偷闲觉得有必要对几种常见的鉴权方式整理一下。
目前我们常用的鉴权有四种:
HTTP Basic Authentication
session-cookie
Token 验证
OAuth(开放授权)
一.HTTP Basic Authentication
这种授权方式是浏览器遵守http协议实现的基本授权方式,HTTP协议进行通信的过程中,HTTP协议定义了基本认证认证允许HTTP服务器对客户端进行用户身份证的方法。
认证过程:
1. 客户端向服务器请求数据,请求的内容可能是一个网页或者是一个ajax异步请求,此时,假设客户端尚未被验证,则客户端提供如下请求至服务器:
Get /index.html HTTP/1.0 Host:www.google.com
2. 服务器向客户端发送验证请求代码401,(WWW-Authenticate: Basic realm=”google.com”这句话是关键,如果没有客户端不会弹出用户名和密码输入界面)服务器返回的数据大抵如下:
HTTP/1.0 401 Unauthorised Server: SokEvo/1.0 WWW-Authenticate: Basic realm=”google.com” Content-Type: text/html Content-Length: xxx
3. 当符合http1.0或1.1规范的客户端(如IE,FIREFOX)收到401返回值时,将自动弹出一个登录窗口,要求用户输入用户名和密码。
4. 用户输入用户名和密码后,将用户名及密码以BASE64加密方式加密,并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成如下内容:
Get /index.html HTTP/1.0 Host:www.google.com Authorization: Basic d2FuZzp3YW5n
注:d2FuZzp3YW5n表示加密后的用户名及密码(用户名:密码 然后通过base64加密,加密过程是浏览器默认的行为,不需要我们人为加密,我们只需要输入用户名密码即可)
5. 服务器收到上述请求信息后,将Authorization字段后的用户信息取出、解密,将解密后的用户名及密码与用户数据库进行比较验证,如用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端
效果: 客户端未未认证的时候,会弹出用户名密码输入框,这个时候请求时属于pending状态,这个时候其实服务当用户输入用户名密码的时候客户端会再次发送带Authentication头的请求。
认证成功:
server.js
let express = require ("express" );
let app = express();
app.use(express.static(__dirname+<span class="hljs-string">'/public'</span>));
app.get(<span class="hljs-string">"/Authentication_base"</span>,<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(req,res)</span>{<!-- --></span>
console.log(<span class="hljs-string">'req.headers.authorization:'</span>,req.headers)
<span class="hljs-keyword">if</span>(!req.headers.authorization){
res.set({
<span class="hljs-string">'WWW-Authenticate'</span>:<span class="hljs-string">'Basic realm="wang"'</span>
});
res.status(<span class="hljs-number">401</span>).end();
}<span class="hljs-keyword">else</span>{
<span class="hljs-keyword">let</span> base64 = req.headers.authorization.split(<span class="hljs-string">" "</span>)[<span class="hljs-number">1</span>];
<span class="hljs-keyword">let</span> userPass = <span class="hljs-keyword">new</span> Buffer(base64, <span class="hljs-string">'base64'</span>).toString().split(<span class="hljs-string">":"</span>);
<span class="hljs-keyword">let</span> user = userPass[<span class="hljs-number">0</span>];
<span class="hljs-keyword">let</span> pass = userPass[<span class="hljs-number">1</span>];
<span class="hljs-keyword">if</span>(user==<span class="hljs-string">"wang"</span>&&pass=<span class="hljs-string">"wang"</span>){
res.end(<span class="hljs-string">"OK"</span>);
}<span class="hljs-keyword">else</span>{
res.status(<span class="hljs-number">401</span>).end();
}
}
})
app.listen(<span class="hljs-number">9090</span>)<div class="hljs-button {2}" data-title="复制" data-report-click="{"spm":"1001.2101.3001.4259"}"></div></code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li><li style="color: rgb(153, 153, 153);">16</li><li style="color: rgb(153, 153, 153);">17</li><li style="color: rgb(153, 153, 153);">18</li><li style="color: rgb(153, 153, 153);">19</li><li style="color: rgb(153, 153, 153);">20</li><li style="color: rgb(153, 153, 153);">21</li><li style="color: rgb(153, 153, 153);">22</li><li style="color: rgb(153, 153, 153);">23</li><li style="color: rgb(153, 153, 153);">24</li><li style="color: rgb(153, 153, 153);">25</li><li style="color: rgb(153, 153, 153);">26</li><li style="color: rgb(153, 153, 153);">27</li><li style="color: rgb(153, 153, 153);">28</li></ul></pre>
index.html:
<!DOCTYPE html>
<html >
<head >
<meta charset ="UTF-8" >
<title > HTTP Basic Authentication</title >
</head >
<body >
<div > </div >
<script src ="js/jquery-3.2.1.js" > </script >
<script >
$(function () {
send('./Authentication_base' );
})
var send = function (url) {
$.ajax({
url : url,
method : 'GET' ,
});
}
</script >
</body >
</html >
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
当然有登陆就有注销,我们会发现当我们认证成功后每次请求请求头都会带上Authentication及里面的内容,那么如何做到让这次登陆失效的?
网上查了半天,目前最有效的方式就是在注销操作的时候,专门在服务器设置一个专门的注销账号,当接收到的Authentication信息为注销用户名密码的时候纠就带便注销成功了,而客户端在注销操作的时候,手动的的去修改请求头重的Authentication,将他设置未服务器默认的注销账号和密码。
通过上面的简单讲解 其实我们已经可以返现这种验证方式的缺陷加密方式简单,仅仅是base64加密,这种加密方式是可逆的。同时在每个请求的头上都会附带上用户名和密码信息,这样在外网是很容易被嗅探器探测到的。
总结:
正式因为这样,这种加密方式一般多被用在内部安全性要求不高的的系统上,只是相对的多,总的来说现在使用这种鉴权比较少了。如果项目需要部署在公网上,这种方式不推荐,当然你也可以和SSL来加密传输,这样会好一点,这个如果我后面有时间来研究一下。
二.session-cookie
第二种这个方式是利用服务器端的session(会话)和浏览器端的cookie来实现前后端的认证,由于http请求时是无状态的,服务器正常情况下是不知道当前请求之前有没有来过,这个时候我们如果要记录状态,就需要在服务器端创建一个会话(seesion),将同一个客户端的请求都维护在各自得会会话中,每当请求到达服务器端的时候,先去查一下该客户端有没有在服务器端创建seesion,如果有则已经认证成功了,否则就没有认证。 session-cookie认证主要分四步: 1,服务器在接受客户端首次访问时在服务器端创建seesion,然后保存seesion(我们可以将seesion保存在内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一个唯一的标识字符串,然后在响应头中种下这个唯一标识字符串。 2.签名。这一步只是对sid进行加密处理,服务端会根据这个secret密钥进行解密。(非必需步骤) 3.浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求de 请求头中会带上该域名下的cookie信息, 4.服务器在接受客户端请求时会去解析请求头cookie中的sid,然后根据这个sid去找服务器端保存的该客户端的session,然后判断该请求是否合法。
server.js(nodejs+express+seesion+redis)
var express = require ('express' );
var RedisStore = require ('connect-redis' )(express.session);
var app = express();
var secret = "wang839305939"
app.use (express.cookieParser(secret));
app.use (express.session({
store: new RedisStore({
host: “127.0.0.1” , port: 6379 , db: “session_db” }), secret: secret }))
app.get("/" , function (req, res) { var session = req.session; session.time= session.time|| 0 ; var n = session.time++; res.send(‘hello, session id:’ + session.id + ’ count:’ + n); });
app.listen(9080 );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
三.Token 验证
使用基于 Token 的身份验证方法,大概的流程是这样的:
1. 客户端使用用户名跟密码请求登录 2. 服务端收到请求,去验证用户名与密码 3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端 4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token 6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
总的来说就是客户端在首次登陆以后,服务端再次接收http请求的时候,就只认token了,请求只要每次把token带上就行了,服务器端会拦截所有的请求,然后校验token的合法性,合法就放行,不合法就返回401(鉴权失败)。
乍的一看好像和前面的seesion-cookie有点像,seesion-cookie是通过seesionid来作为浏览器和服务端的链接桥梁,而token验证方式貌似是token来起到seesionid的角色。其实这两者差别是很大的。 1. sessionid 他只是一个唯一标识的字符串,服务端是根据这个字符串,来查询在服务器端保持的seesion,这里面才保存着用户的登陆状态。但是token本身就是一种登陆成功凭证,他是在登陆成功后根据某种规则生成的一种信息凭证,他里面本身就保存着用户的登陆状态。服务器端只需要根据定义的规则校验这个token是否合法就行。 2. session-cookie是需要cookie配合的,居然要cookie,那么在http代理客户端的选择上就是只有浏览器了,因为只有浏览器才会去解析请求响应头里面的cookie,然后每次请求再默认带上该域名下的cookie。但是我们知道http代理客户端不只有浏览器,还有原生APP等等,这个时候cookie是不起作用的,或者浏览器端是可以禁止cookie的(虽然可以,但是这基本上是属于吃饱没事干的人干的事)…,但是token 就不一样,他是登陆请求在登陆成功后再请求响应体中返回的信息,客户端在收到响应的时候,可以把他存在本地的cookie,storage,或者内存中,然后再下一次请求的请求头重带上这个token就行了。简单点来说cookie-session机制他限制了客户端的类型,而token验证机制丰富了客户端类型。 3. 时效性。session-cookie的sessionid实在登陆的时候生成的而且在登出事时一直不变的,在一定程度上安全就会低,而token是可以在一段时间内动态改变的。 4. 可扩展性。token验证本身是比较灵活的,一是token的解决方案有许多,常用的是JWT,二来我们可以基于token验证机制,专门做一个鉴权服务,用它向多个服务的请求进行统一鉴权。
下面就拿最常用的JWT(JSON WEB TOKEN)来说:
JWT是Auth0提出的通过对JSON进行加密签名来实现授权验证的方案,就是登陆成功后将相关信息组成json对象,然后对这个对象进行某中方式的加密,返回给客户端,客户端在下次请求时带上这个token,服务端再收到请求时校验token合法性,其实也就是在校验请求的合法性。 JWT对象通常由三部分构成:
Headers: 包括类别(typ)、加密算法(alg)
{
"alg ": "HS256" ,
"typ ": "JWT"
}
Claims :包括需要传递的用户信息
{
"sub ": "1234567890" ,
"name ": "John Doe" ,
"admin ": true
}
Signature: 根据alg算法与私有秘钥进行加密得到的签名字串, 这一段是最重要的敏感信息,只能在服务端解密;
HMACSHA256(
base64UrlEncode(Headers) + "." +
base64UrlEncode(Claims) ,
SECREATE_KEY
)
编码之后的JWT看起来是这样的一串字符:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
nodejs+express+jwt-simple auth.js
let jwt = require ('jwt-simple' );
let secret = "wangyy" ;
let time = 10 ;
module.exports = {
validate:function (req,res,next) {
let token = req.body.token||req.headers["xssToken" ];
if (token){
let decodeToken = null ;
try {
decodeToken = jwt.decode(token,secret,'HS256' );
} catch (err) {
res.status(401 ).send("非法访问" ); return ;
}
let exp = decodeToken.exp; if (!exp){
res.status(401 ).send("非法访问" );
}
let now = new Date ().getTime();
if (exp>(now+time*60 *1000 )){
res.send({code:'002' ,"errorMsg" :"授权超时" })
}
next();
}else {
res.status(401 ).send("非法访问" );
}
},
makeToken(){
let Token = null ;
let payload = {
time:new Date ().getTime(),
exp:this .makeExp(time)
}
Token = jwt.encode(payload,secret,HS256) return Token;
},
makeExp:function (time) {
let stam = time601000;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
server.js
let express = require ("express" );
let app = express();
let bodyParser = require ('body-parser' );
let auth = require ('./lib/auth.js' );
let chalk = require ('chalk' ); app.use(bodyParser.json()); app.post('/login' ,function (req,res,next) {
let Token = auth.makeToken();
res.json({result:"success" ,token:Token},200 )
});
app.use('*' ,[auth.validate],function (req,res,next) {
res.send('success' );
});
app.listen('9999' )
1
2
3
4
5
6
7
8
9
10
11
12
上面只是一个简单的token生成和校验,如果有需要可以根据实际需要进行逻辑处理
四.OAuth(开放授权)
OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供OAuth认证服务的厂商有支付宝,QQ,微信。 OAuth协议又有1.0和2.0两个版本。相比较1.0版,2.0版整个授权验证流程更简单更安全,也是目前最主要的用户身份验证和授权方式。 下面是一张auth2.0的流程图:
从图中我们可以看出,auth2.0流程分为六布(我们就以csdn登陆为例):
第一步. 向用户请求授权,现在很多的网站在登陆的时候都有第三方登陆的入口,当我们点击等第三方入口时,第三方授权服务会引导我们进入第三方登陆授权页面。
通过第三方请求授权页面的浏览器地址栏地址可以看出,
https://graph .qq.com/oauth2.0 /show?which=Login&display=pc&response_type=code&client_id=100270989 &redirect_uri=https%3A %2F %2Fpassport .csdn.net%2Faccount %2Flogin %3Foauth_provider %3DQQProvider &state =test
这里的地址里面的%是浏览器强制编码后的显示我们可以使用decodeURIComponent进行解码,解码后是这样:
https: //graph.qq .com /oauth2.0 /show?which=Login&display=pc&response_type=code&client_id=100270989 &redirect_uri=https://passport.csdn .net /account/login?oauth_provider=QQProvider&state=test
这个url地址我们可以看见Auth2.0常见的几个参数: response_type,返回类型 client_id,第三方应用id,由授权服务器(qq)在第三方应用提交时颁发给第三方应用。 redirect_uri,登陆成功重定向页面 oauth_provider,第三方授权提供方 state,由第三方应用给出的随机码 第二步. 返回用户凭证(code),并返回一个凭证(code),当用户点击授权并登陆后,授权服务器将生成一个用户凭证(code)。这个用户凭证会附加在重定向的地址redirect_uri的后面
https: //passport.csdn .net /account/login?code=9e3 efa6cea739f9aaab2&state=XXX
第3步. 请求授权服务器授权:
经过第二部获取code后后面的工作就可以交给后台去处理的,和用户的交互就结束了。接下来我的需要获取Access Token,我们需要用他来向授权服务器获取用户信息等资源。 第三方应用后台通过第二步的凭证(code)向授权服务器请求Access Token,这时候需要以下几个信息:
client_id 标识第三方应用的id,由授权服务器(Github)在第三方应用提交时颁发给第三方应用
client_secret 第三方应用和授权服务器之间的安全凭证,由授权服务器(Github)在第三方应用提交时颁发给第三方应用
code 第一步中返回的用户凭证redirect_uri 第一步生成用户凭证后跳转到第二步时的地址
state 由第三方应用给出的随机码
第四步. 授权服务器同意授权后,返回一个资源访问的凭证(Access Token)。
第五步. 第三方应用通过第四步的凭证(Access Token)向资源服务器请求相关资源。
第六步. 资源服务器验证凭证(Access Token)通过后,将第三方应用请求的资源返回。
从用户角度来说,第三方授权可以让我们快速的登陆应用,无需进行繁琐的注册,同时不用记住各种账号密码。只需要记住自己常用的几个账号就ok了。 从产品经理的角度来所,这种授权方式提高用户的体验满意度。另一方面可以获取更多的用户。
总结:
授权方式多种多样,主要还是要取决于我们对于产品的定位。如果我们的产品只是在企业内部使用,token和session就可以满足我们的需求,如果是面向互联网的大众用户,那么第三方授权在用户体验度上会有一个很大的提升。
还是那句话,上面可能有很多‘通假字’勿怪,我写作的目的一方面是希望和大家分享我掌握的点点滴滴,另一方面也是梳理一下掌握的知识。