一、四种鉴权方式
目前我们常用的鉴权有四种:
- 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+'/public'));
app.get("/Authentication_base",function(req,res){
console.log('req.headers.authorization:',req.headers)
if(!req.headers.authorization){
res.set({
'WWW-Authenticate':'Basic realm="wang"'
});
res.status(401).end();
}else{
let base64 = req.headers.authorization.split(" ")[1];
let userPass = new Buffer(base64, 'base64').toString().split(":");
let user = userPass[0];
let pass = userPass[1];
if(user=="wang"&&pass="wang"){
res.end("OK");
}else{
res.status(401).end();
}
}
})
app.listen(9090)
复制代码
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>
复制代码
优点: 基本认证的一个优点是基本上所有流行的网页浏览器都支持基本认证。基本认证很少在可公开访问的互联网网站上使用,有时候会在小的私有系统中使用(如路由器网页管理接口)。后来的机制HTTP摘要认证是为替代基本认证而开发的,允许密钥以相对安全的方式在不安全的通道上传输。 程序员和系统管理员有时会在可信网络环境中使用基本认证,使用Telnet或其他明文网络协议工具手动地测试Web服务器。这是一个麻烦的过程,但是网络上传输的内容是人可读的,以便进行诊断。
缺点:
虽然基本认证非常容易实现,但该方案创建在以下的假设的基础上,即:客户端和服务器主机之间的连接是安全可信的。特别是,如果没有使用SSL/TLS
这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截。该方案也同样没有对服务器返回的信息提供保护。
现存的浏览器保存认证信息直到标签页或浏览器被关闭,或者用户清除历史记录。HTTP没有为服务器提供一种方法指示客户端丢弃这些被缓存的密钥。这意味着服务器端在用户不关闭浏览器的情况下,并没有一种有效的方法来让用户注销。
三、session-cookie
3.1 cookie
Http协议是一个无状态的协议,服务器不会知道到底是哪一台浏览器访问了它,因此需要一个标识用来让服务器区分不同的浏览器。cookie
就是这个管理服务器与客户端之间状态的标识。
cookie
的原理是,浏览器第一次向服务器发送请求时,服务器在 response
头部设置 Set-Cookie
字段,浏览器收到响应就会设置 cookie
并存储,在下一次该浏览器向服务器发送请求时,就会在 request
头部自动带上 Cookie
字段,服务器端收到该 cookie
用以区分不同的浏览器。当然,这个 cookie
与某个用户的对应关系应该在第一次访问时就存在服务器端,这时就需要 session
了。
const http = require('http')
http.createServer((req, res) => {
if (req.url === '/favicon.ico') {
return
} else {
res.setHeader('Set-Cookie', 'name=zhunny')
res.end('Hello Cookie')
}
}).listen(3000)
复制代码
3.2 session
session
是会话的意思,浏览器第一次访问服务端,服务端就会创建一次会话,在会话中保存标识该浏览器的信息。它与 cookie
的区别就是 session
是缓存在服务端的,cookie
则是缓存在客户端,他们都由服务端生成,为了弥补 Http
协议无状态的缺陷。
3.3 session-cookie认证
- 服务器在接受客户端首次访问时在服务器端创建seesion,然后保存seesion(我们可以将seesion保存在 内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一个唯一的标识字符串,然后在 响应头中种下这个唯一标识字符串。
- 签名。这一步通过秘钥对sid进行签名处理,避免客户端修改sid。(非必需步骤)
- 浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中,浏览器在下次http请求的请求头中会带上该域名下的cookie信息。
- 服务器在接受客户端请求时会去解析请求头cookie中的sid,然后根据这个sid去找服务器端保存的该客户端的session,然后判断该请求是否合法。
const http = require('http')
//此时session存在内存中
const session = {}
http.createServer((req, res) => {
const sessionKey = 'sid'
if (req.url === '/favicon.ico') {
return
} else {
const cookie = req.headers.cookie
//再次访问,对sid请求进行认证
if (cookie && cookie.indexOf(sessionKey) > -1) {
res.end('Come Back')
}
//首次访问,生成sid,保存在服务器端
else {
const sid = (Math.random() * 9999999).toFixed()
res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
session[sid] = { name: 'zhunny' }
res.end('Hello Cookie')
}
}
}).listen(3000)
复制代码
3.4 redis
redis是一个键值服务器,可以专门放session的键值对。如何在koa中使用session:
const koa = require('koa')
const app = new koa()
const session = require('koa-session')
const redisStore = require('koa-redis')
const redis = require('redis')
const redisClient = redis.createClient(6379, 'localhost')
const wrapper = require('co-redis')
const client = wrapper(redisClient)
//加密sessionid
app.keys = ['session secret']
const SESS_CONFIG = {
key: 'kbb:sess',
//此时让session存储在redis中
store: redisStore({ client })
}
app.use(session(SESS_CONFIG, app))
app.use(ctx => {
//查看redis中的内容
redisClient.keys('*', (errr, keys) => {
console.log('keys:', keys)
keys.forEach(key => {
redisClient.get(key, (err, val) => {
console.log(val)
})
})
})
if (ctx.path === '/favicon.ico') return
let n = ctx.session.count || 0
ctx.session.count = ++n
ctx.body = `第${n}次访问`
})
app.listen(3000)
复制代码
3.5 用户登录认证
使用session-cookie做登录认证时,登录时存储session,退出登录时删除session,而其他的需要登录后才能操作的接口需要提前验证是否存在session,存在才能跳转页面,不存在则回到登录页面。
在koa中做一个验证的中间件,在需要验证的接口中使用该中间件。
//前端代码
async login() {
await axios.post('/login', {
username: this.username,
password: this.password
})
},
async logout() {
await axios.post('/logout')
},
async getUser() {
await axios.get('/getUser')
}
复制代码
//中间件 auth.js
module.exports = async (ctx, next) => {
if (!ctx.session.userinfo) {
ctx.body = {
ok: 0,
message: "用户未登录" };
} else {
await next();
} };
//需要验证的接口
router.get('/getUser', require('auth'), async (ctx) => {
ctx.body = {
message: "获取数据成功",
userinfo: ctx.session.userinfo
}
})
//登录
router.post('/login', async (ctx) => {
const {
body
} = ctx.request
console.log('body', body)
//设置session
ctx.session.userinfo = body.username;
ctx.body = {
message: "登录成功"
}
})
//登出
router.post('/logout', async (ctx) => {
//设置session
delete ctx.session.userinfo
ctx.body = {
message: "登出系统"
}
})
复制代码
四、Token
token
是一个令牌,浏览器第一次访问服务端时会签发一张令牌,之后浏览器每次携带这张令牌访问服务端就会认证该令牌是否有效,只要服务端可以解密该令牌,就说明请求是合法的,令牌中包含的用户信息还可以区分不同身份的用户。一般 token
由用户信息、时间戳和由 hash
算法加密的签名构成。
4.1 Token认证流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个
Token
,再把这个Token
发送给客户端 - 客户端收到
Token
以后可以把它存储起来,比如放在Cookie
里或者Local Storage
里 - 客户端每次向服务端请求资源的时候需要带着服务端签发的
Token
- 服务端收到请求,然后去验证客户端请求里面带着的
Token
(request头部添加Authorization),如果验证成功,就向客户端返回请求的数据 ,如果不成功返回401错误码,鉴权失败。
4.2 Token和session的区别
session-cookie
的缺点:
(1)认证方式局限于在浏览器中使用,cookie
是浏览器端的机制,如果在app端就无法使用 cookie
。
(2)为了满足全局一致性,我们最好把 session
存储在 redis
中做持久化,而在分布式环境下,我们可能需要在每个服务器上都备份,占用了大量的存储空间。
(3)在不是 Https
协议下使用 cookie
,容易受到 CSRF 跨站点请求伪造攻击。
token的缺点:
(1)加密解密消耗使得 token
认证比 session-cookie
更消耗性能。
(2)token
比 sessionId
大,更占带宽。
两者对比,它们的区别显而易见:
(1)token
认证不局限于 cookie
,这样就使得这种认证方式可以支持多种客户端,而不仅是浏览器。且不受同源策略的影响。
(2)不使用 cookie
就可以规避CSRF攻击。
(3)token
不需要存储,token
中已包含了用户信息,服务器端变成无状态,服务器端只需要根据定义的规则校验这个 token
是否合法就行。这也使得 token
的可扩展性更强。
4.3 JWT(JSON Web Token)
基于 token
的解决方案有许多,常用的是JWT
,JWT
的原理是,服务器认证以后,生成一个 JSON
对象,这个 JSON
对象肯定不能裸传给用户,那谁都可以篡改这个对象发送请求。因此这个 JSON
对象会被服务器端签名加密后返回给用户,返回的内容就是一张令牌,以后用户每次访问服务器端就带着这张令牌。
这个 JSON
对象可能包含的内容就是用户的信息,用户的身份以及令牌的过期时间。
4.3.1 JWT的组成部分
在该网站JWT,可以解码或编码一个JWT。一个JWT形如:
它由三部分组成:Header(头部)、Payload(负载)、Signature(签名)。
- Header部分是一个JSON对象,描述JWT的元数据。一般描述信息为该Token的加密算法以及Token的类型。{"alg": "HS256","typ": "JWT"}的意思就是,该token使用HS256加密,token类型是JWT。这个部分基本相当于明文,它将这个JSON对象做了一个Base64转码,变成一个字符串。Base64编码解码是有算法的,解码过程是可逆的。头部信息默认携带着两个字段。
- Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。有7个官方字段,还可以在这个部分定义私有字段。一般存放用户名、用户身份以及一些JWT的描述字段。它也只是做了一个Base64编码,因此肯定不能在其中存放秘密信息,比如说登录密码之类的。
- Signature是对前面两个部分的签名,防止数据篡改,如果前面两段信息被人修改了发送给服务器端,此时服务器端是可利用签名来验证信息的正确性的。签名需要密钥,密钥是服务器端保存的,用户不知道。算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
4.3.2 JWT的特点
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不加密的情况下,不能将秘密数据写入 JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
4.3.3 JWT验证用户登录
//前端代码
//axios的请求拦截器,在每个request请求头上加JWT认证信息
axios.interceptors.request.use(
config => {
const token = window.localStorage.getItem("token");
if (token) {
// 判断是否存在token,如果存在的话,则每个http header都加上token
// Bearer是JWT的认证头部信息
config.headers.common["Authorization"] = "Bearer " + token;
}
return config;
},
err => {
return Promise.reject(err);
}
);
//登录方法:在将后端返回的JWT存入localStorage
async login() {
const res = await axios.post("/login-token", {
username: this.username,
password: this.password
});
localStorage.setItem("token", res.data.token);
},
//登出方法:删除JWT
async logout() {
localStorage.removeItem("token");
},
async getUser() {
await axios.get("/getUser-token");
}
复制代码
//后端代码
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
//用来签名的密钥
const secret = "it's a secret";
router.post("/login-token", async ctx => {
const { body } = ctx.request;
//登录逻辑,略,即查找数据库,若该用户和密码合法,即将其信息生成一个JWT令牌传给用户
const userinfo = body.username;
ctx.body = {
message: "登录成功",
user: userinfo,
// 生成 token 返回给客户端
token: jwt.sign(
{
data: userinfo,
// 设置 token 过期时间,一小时后,秒为单位
exp: Math.floor(Date.now() / 1000) + 60 * 60
},
secret
)
};
});
//jwtAuth这个中间件会拿着密钥解析JWT是否合法。
//并且把JWT中的payload的信息解析后放到state中,ctx.state用于中间件的传值。
router.get(
"/getUser-token",
jwtAuth({
secret
}),
async ctx => {
// 验证通过,state.user
console.log(ctx.state.user);
ctx.body = {
message: "获取数据成功",
userinfo: ctx.state.user.data
};
}
)
//这种密码学的方式使得token不需要存储,只要服务端能拿着密钥解析出用户信息,就说明该用户是合法的。
//若要更进一步的权限验证,需要判断解析出的用户身份是管理员还是普通用户。
复制代码
五、OAuth(开放授权)
OAuth(Open Authorization)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供OAuth认证服务的厂商有支付宝,QQ,微信。
OAuth协议又有1.0和2.0两个版本。相比较1.0版,2.0版整个授权验证流程更简单更安全,也是目前最主要的用户身份验证和授权方式。
关于OAuth相关文章,可以查看 OAuth 2.0 的一个简单解释、理解OAuth 2.0、OAuth 2.0 的四种方式
5.1 OAuth认证流程
OAuth就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
OAuth有四种获取令牌的方式,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
在前后端分离的情境下,我们常使用授权码方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
5.2 GitHub第三方登录示例
我们用例子来理清授权码方式的流程。
- 在GitHub中备案第三方应用,拿到属于它的客户端ID和客户端密钥。
在github-settings-developer settings
中创建一个OAuth App。并填写相关内容。填写完成后Github会给你一个客户端ID和客户端密钥。
- 此时在你的第三方网站就可以提供一个Github登录链接,用户点击该链接后会跳转到Github。这一步拿着客户端ID向Github请求授权码code。
const config = {
client_id: '28926186082164bbea8f',
client_secret: '07c4fdae1d5ca458dae3345b6d77a0add5a785ca'
}
router.get('/github/login', async (ctx) => {
var dataStr = (new Date()).valueOf();
//重定向到认证接口,并配置参数
var path = "https://github.com/login/oauth/authorize";
path += '?client_id=' + config.client_id;
//转发到授权服务器
ctx.redirect(path);
})
复制代码
- 用户跳转到Github,输入Github的用户名密码,表示用户同意使用Github身份登录第三方网站。此时就会带着授权码code跳回第三方网站。跳回的地址在创建该OAuth时已经设置好了。http://localhost:3000/github/callback
- 第三方网站收到授权码,就可以拿着授权码、客户端ID和客户端密钥去向Github请求access_token令牌。
- Github收到请求,向第三方网站颁发令牌。
- 第三方网站收到令牌,就可以暂时拥有Github一些请求的权限,比如说拿到用户信息,拿到这个用户信息之后就可以构建自己第三方网站的token,做相关的鉴权操作。
router.get('/github/callback', async (ctx) => {
console.log('callback..')
const code = ctx.query.code;
const params = {
client_id: config.client_id,
client_secret: config.client_secret,
code: code
}
let res = await axios.post('https://github.com/login/oauth/access_token', params)
const access_token = querystring.parse(res.data).access_token
res = await axios.get('https://api.github.com/user?access_token=' + access_token)
console.log('userAccess:', res.data)
ctx.body = `
<h1>Hello ${res.data.login}</h1>
<img src="${res.data.avatar_url}" alt=""/>
`
})
复制代码