内容介绍:
本篇文章是《琢·磨》系列技术分享第四讲,分享三种常见鉴权方式的原理及实现,包括session-cookie、JWT、OAuth; 本篇文章使用的是koa + MongoDB + Vue实现的demo逻辑。
session-cookie
1.cookie的诞生
说到cookie的诞生,首先要说到http是个无状态协议。每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态。为了解决会话跟踪的问题,cookie诞生了。
2.什么是cookie
cookie是由服务器发送给客户端(浏览器)的小量信息,以{key:value}的形式存在,再次请求时会再发回服务器。
3.cookie的工作原理
客户端请求服务器时,如果服务器需要记录该用户状态,就会在响应头里通过Set-cookie头部字段设置一个Cookie。而浏览器这边检查到响应头里有Set-cookie字段,就会把Cookie保存起来。当浏览器再请求 服务器时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器通过检查该Cookie来获取用户状态。
接下来做个cookie的代码演示:
const Koa = require('koa');
const app = new Koa();
app.use(ctx => {
// 从请求里获取cookie为sid的字段
const cookieSid = ctx.cookies.get('sid');
console.log('cookieSid :>> ', cookieSid);
if (cookieSid) {
ctx.body = `常客儿, 欢迎${cookieSid}`;
} else {
// Set-Cookie 约定
// 如果没有拿到值,就给它设置一个值
ctx.cookies.set('sid', 'yupeng');
ctx.body = '第一次来';
}
})
app.listen(3000);
我们运行一下,可以看到第一次的时候显示的是第一次来,并且设置了sid: 'yupeng'
当我们再次刷新,因为获取到了cookie数据,所以显示常客:
4.cookie的缺陷
1、通常大小4KB,太小 2、在浏览器端存储不安全,易被篡改 3、每次请求都携带,浪费带宽
为了解决上面cookie的一些问题,就有了session
5.session的工作原理
用户第一次登录后,浏览器会将用户信息发送给服务器,服务器会为该用户创建一个Session,并在响应内容(Cookie)中将SessionId一并返回给浏览器,浏览器将这些数据保存在本地。当用户再次发送请求时,浏览器会自动的把上次请求存储的Cookie数据自动的携带给服务器。
服务器接收到请求信息后,会通过浏览器请求的数据中的SessionId判断当前是哪个用户,然后根据SessionId在Session库中获取用户的Session数据返回给浏览器。
// 这个代码和上边的cookie的基本一致,只是我们创建了一个session对象来存储cookie信息,然后把key返还给客户端
const Koa = require('koa');
const app = new Koa();
const session = {};
app.use(ctx => {
const cookieSid = ctx.cookies.get('sid');
console.log('cookieSid :>> ', cookieSid);
if (cookieSid) {
console.log('session :>> ', session[cookieSid]);
ctx.body = `常客儿, 欢迎${session[cookieSid].name}`;
} else {
// Set-Cookie 约定
const sid = (Math.random() * 1000000).toFixed();
ctx.cookies.set('sid', sid);
session[sid] = { name: 'yupeng' };
ctx.body = '第一次来';
}
})
app.listen(3000);
我们来看一下运行结果:
第一次来的时候同样设置了cookie,这里设置的值为服务端随机生成的字符串
当我们再次刷新的时候可以看到,cookie没有变化,这是服务器也已经返回了,我们存储在服务器端的session对象里的数据
6.koa中session的使用
上面了解session的工作原理,接下来我们看一下koa中,session的真实使用案例。 这里我做了一个页面计数的功能:
const Koa = require('koa');
// 引用了koa处理session的库
const session = require('koa-session');
const app = new Koa();
// 加密时所使用的key
app.keys = ['some secret'];
// sessionde的配置,key是客户端存储的cookie key值;maxAge是过期时间;httpOnly是是否只在http请求中使用,不允许客户端操作;signed是是否加密;
const SESS_CONFIG = {
key: 'koa.sess',
maxAge: 10000, // ms
httpOnly: true,
signed: false,
};
app.use(session(SESS_CONFIG, app));
app.use((ctx) => {
// 这里做了个计数的功能,来一次累加一次
let n = ctx.session.views || 0;
ctx.session.views = ++n;
ctx.body = n + ' views';
});
app.listen(3000);
我们看一下效果:
7.使用koa-session实现登陆注册
接下来我们使用koa实现一个简单的登陆注册案例
先来看一下代码结构
model里存储use表操作模型
utils里提供了连接数库的逻辑,和一个检测账号密码的方法
axios.min.js、vue.js和index.html是前端使用的代码,为了便于演示,我将这个项目都设置成了一个静态资源服务器,这样就能直接访问页面了
index.js里写着服务端的逻辑
我们先来看一下model里的逻辑:
// 这里使用了mongoose库做MongoDB的操作
const mongoose = require('mongoose');
// 这里定义了表的数据模型
const schema = mongoose.Schema({
username: String,
password: String,
});
// 这里挂了两个方法,获取用户和设置用户
schema.statics.getUser = function(username) {
return this.model('user')
.findOne({ username })
.exec();
};
schema.statics.createUser = function({ username, password }) {
return this.model('user')
.create({
username,
password,
});
};
// 这里对表与模型做了关联
const model = mongoose.model('user', schema);
module.exports = model;
下面看一下utils里的逻辑,先是checkLogin.js
// checkLogin.js
// 这里引用了UserModel,简单的判断了下登陆的密码和数据库存的一不一样
const UserModel = require('../models/user');
exports.checkPassword = async function ({ username, password }) {
const res = await UserModel.getUser(username);
if (res && res.password === password) {
return true;
}
return false
}
然后是mongoose.js:
// 同样引用mongoose做处理
const mongoose = require('mongoose');
// 做了数据库的连接
mongoose.connect('mongodb://127.0.0.1:27027/loginshare', {
useNewUrlParser: true,
useUnifiedTopology: true,
}).catch(error => {
console.log('数据库error', error)
});;
const conn = mongoose.connection;
conn.on('error', () => console.log('数据库连接失败'));
conn.once('open', () => console.log('数据库连接成功'));
接下来看一下服务器端的代码:
const Koa = require('koa');
// koa-router来处理路由
const router = require('koa-router')();
const session = require('koa-session');
// 用来解析post请求的数据,会挂在ctx.request.body中
const bodyParser = require('koa-bodyparser');
// 用来做静态服务的处理
const static = require('koa-static');
require('./utils/mongoose');
const UserModel = require('./models/user');
const {
checkPassword
} = require('./utils/checkLogin');
const app = new Koa();
app.keys = ['some secret'];
// 将根目录设置为静态服务
app.use(static(__dirname + '/'));
// 使用bodyParser中间件,解析post
app.use(bodyParser());
app.use(session({
key: 'koa.sess',
maxAge: 10000,
httpOnly: true,
signed: false,
}, app));
// 登陆接口,密码不对提示密码错误,正确提示登陆成功,设置session信息
router.post('/login', async (ctx) => {
const {
body: {
username,
password,
}
} = ctx.request
// 检验账号密码
if (!(await checkPassword({
username,
password,
}))) {
ctx.body = {
message: '账号或者密码不对'
}
return;
}
ctx.session.userinfo = {
username,
password
};
ctx.body = {
message: "登录成功"
}
})
// 注册接口,在数据库存储用户信息
router.post('/register', async (ctx, next) => {
const {
body: {
username,
password,
}
} = ctx.request
await UserModel.create({
username,
password
})
ctx.body = {
message: "注册成功",
}
})
// 登出接口,删除session中的信息
router.post('/logout', async (ctx) => {
//设置session
delete ctx.session.userinfo
ctx.body = {
message: "登出系统"
}
})
// 获取用户信息接口,返回用户数据
router.get('/getUser', async (ctx) => {
ctx.body = {
message: "获取数据成功",
userinfo: ctx.session.userinfo
}
})
// 这里写了个中间件,用于过滤登陆和注册不用检测登陆态,其他接口需要登陆才能使用
app.use(async (ctx, next) => {
if (ctx.url.indexOf('login') > -1 || ctx.url.indexOf('register') > -1) {
await next()
} else {
if (!ctx.session.userinfo) {
ctx.body = {
message: "未登陆"
}
} else {
await next()
}
}
})
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
以上是后端逻辑,接下来看下前端逻辑,前端比较简单,就是写了一个调用后端接口的方法:
<body>
<div id="app">
<div>
<input v-model="username">
<input v-model="password">
</div>
<div>
<button v-on:click="login">登陆</button>
<button v-on:click="register">注册</button>
<button v-on:click="logout">登出</button>
<button v-on:click="getUser">获取用户信息</button>
</div>
<div>
<button onclick="document.getElementById('log').innerHTML = ''">清除log</button>
</div>
</div>
<div id="log"></div>
</div>
<script>
// 这里做了一个相应的拦截,便于调试都打在页面里了
axios.interceptors.response.use(
response => {
var element = document.createElement("p");
element.innerHTML = JSON.stringify(response.data)
document.getElementById('log').appendChild(element)
return response;
}
);
var app = new Vue({
el: '#app',
data: {
username: '',
password: ''
},
methods: {
async login() {
await axios.post('/login', {
username: this.username,
password: this.password
})
},
async logout() {
await axios.post('/logout')
},
async getUser() {
await axios.get('/getUser')
},
async register() {
await axios.post('/register', {
username: this.username,
password: this.password
})
}
}
});
</script>
</body>
以上就是demo逻辑的实现,接下来看一下实现效果:
8.session的问题
1.服务器要记录所有的session id信息,体量大的时候是不小的开销 2.扩展性不好,涉及到服务器集群的时候,就需要共享session信息
所以就衍生了jwt token这种鉴权方式,所有数据都保存在客户端,每次请求都发回服务器。
JWT (json web token)
1、token的认证流程
1.客户端使用用户名跟密码请求登录 2.服务端收到请求,去验证用户名与密码 3.验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端 4.客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里 5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token 6.服务端收到请求,然后去验证客户端请求里面带着的 Token(request头部添加Authorization),如果验证成功,就向客户端返回请求的数据,如果不成功返回401错误码,鉴权失败。
2.JWT数据结构
JWT是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
Header(头部)
Payload(负载)
Signature(签名)
Header部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{ "alg": "HS256", "typ": "JWT" }
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
使用base64进行加密
Payload部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
我们还可以在这个部分定义私有字段,这里我们就定义了一个data字段
JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
3.JWT使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。较好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。
4.koa中JWT的使用演示
接下来我们使用JWT实现一个简单的登陆注册案例
先来看一下代码结构,和上面的cookie的基本一致,只有服务端和客户端的部分逻辑不太一样,所以我就介绍一下差异的部分index.js和index.html
先来看下index.js:
const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const bodyParser = require('koa-bodyparser')
// 这里我们引用了jsonwebtoken和koa-jwt做jwt的处理
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
require('./utils/mongoose');
const UserModel = require('./models/user');
const {
checkPassword
} = require('./utils/checkLogin');
const app = new Koa();
// 这里是jwt加密时候使用的秘钥
const secret = "loginshare";
app.use(bodyParser())
app.use(static(__dirname + '/'));
// 鉴权错误处理,在jwt鉴权不通过时返回401状态码,这里我们做个token验证失败的提示
app.use((ctx, next) => {
return next().catch((err) => {
if(err.status === 401){
ctx.status = 200;
ctx.body = {
message: 'token验证失败'
};
}else{
throw err;
}
})
})
// 这里是jwt不用鉴权的过滤
app.use(jwtAuth({
secret
}).unless({
path: [/^\/login/, /^\/register/]
}));
// 登陆接口
router.post("/login", async ctx => {
const {
body: {
username,
password,
}
} = ctx.request
// 检验账号密码
if (!(await checkPassword({
username,
password,
}))) {
ctx.body = {
message: '账号或者密码不对'
}
return;
}
ctx.body = {
message: "登录成功",
user: username,
// 生成 token 返回给客户端
token: jwt.sign({
data: username,
// 设置 token 过期时间
exp: Math.floor(Date.now() / 1000) + 10
},
secret
)
};
});
router.get(
"/getUser",
async ctx => {
// token验证通过,会挂一个对象,state.user存着我们自定义的信息,比如上面定义的data
console.log(ctx.state.user);
//获取session
ctx.body = {
message: "获取数据成功",
username: ctx.state.user.data
};
}
)
// 注册接口没变
router.post('/register', async (ctx, next) => {
const {
body: {
username,
password,
}
} = ctx.request
await UserModel.create({
username,
password
})
ctx.body = {
message: "注册成功",
}
})
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000)
上面是服务端的逻辑,接下来看一下客户端的逻辑
<body>
<div id="app">
<div>
<input v-model="username">
<input v-model="password">
</div>
<div>
<button v-on:click="login">登陆</button>
<button v-on:click="register">注册</button>
<button v-on:click="logout">登出</button>
<button v-on:click="getUser">获取用户信息</button>
</div>
<div>
<button onclick="document.getElementById('log').innerHTML = ''">清除log</button>
</div>
</div>
<div id="log"></div>
</div>
<script>
// 做请求拦截,在头里加token
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;
},
);
axios.interceptors.response.use(
response => {
var element = document.createElement("p");
element.innerHTML = JSON.stringify(response.data)
document.getElementById('log').appendChild(element)
return response;
},
);
var app = new Vue({
el: '#app',
data: {
username: '',
password: ''
},
methods: {
async login() {
const res = await axios.post("/login", {
username: this.username,
password: this.password
});
localStorage.setItem("token", res.data.token);
},
// 登出不再请求服务端,就删了本地的token存放
async logout() {
localStorage.removeItem("token");
},
async getUser() {
await axios.get("/getUser");
},
async register() {
await axios.post('/register', {
username: this.username,
password: this.password
})
}
}
});
</script>
</body>
以上是客户端的逻辑,接下来看一下代码运行效果:
5.JWT的问题
1.JWT 默认是不加密,但也是可以生成原始 Token 再加密一次。 2.JWT 不加密的情况下,不能将秘密数据写入 JWT。 3.JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。 4.JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。 5.为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
OAuth
1.什么是OAuth
OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
OAuth 的核心就是向第三方应用颁发令牌
2.OAuth授权码方式
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏
3.授权码的认证流程
第一步,琢磨提供一个链接,用户点击后就会跳转到 github;
第二步,用户跳转后,github会要求用户登录,然后询问是否同意给予琢磨授权。用户表示同意,这时github就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码
第三步,琢磨拿到授权码以后,就可以在后端,向 github请求令牌。
第四步,github收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。
4.授权码认证方式演示
OAuth的demo就比较简单了,不需要数据库做一些存储,所以只用到index.js和index.html
先来看一下服务端的逻辑index.js
const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const app = new Koa();
// 因为需要向github发请求,所以引了个axios库
const axios = require('axios')
// 引了个query解析的库
const querystring = require('querystring')
app.use(static(__dirname + '/'));
// https://github.com/settings/applications/new
// 这里是我们在github申请到的账号和秘钥,用来申请认证
const config = {
client_id: '509844df45c6e1cf6784',
client_secret: 'd8c896d68c6dadd2597a5f94ec70cf8e9ccd3816',
}
// 这里定义了一个github登陆的接口,在登陆的时候我们跳转到github,获取授权码
router.get('/auth/github/login', async (ctx) => {
//重定向到认证接口,并配置参数
var path = `https://github.com/login/oauth/authorize?${querystring.stringify({ client_id: config.client_id })}`;
//转发到授权服务器
ctx.redirect(path);
})
// 这里是github返回授权码的会跳地址,github为什么知道要会跳这个地址呢?是因为我们在线上申请账号密码的时候设置了哈
router.get('/auth/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
}
// 使用授权码去请求token
let res = await axios.post('https://github.com/login/oauth/access_token', params)
const access_token = querystring.parse(res.data).access_token
// 这里设计的是在返回token后,把token存在本地,然后关闭掉当前申请授权的页面
ctx.response.type = 'html';
ctx.response.body = ` <script>window.localStorage.setItem("authSuccess","true");window.localStorage.setItem("token","${access_token}");window.close();</script>`;
})
router.get('/auth/github/userinfo', async (ctx) => {
// 通过github下发的token去获取用户信息
res = await axios.get('https://api.github.com/user?access_token=' + ctx.request.header.authorization)
console.log('userAccess:', res.data)
ctx.body = res.data
})
app.use(router.routes()); /*启动路由*/
app.use(router.allowedMethods());
app.listen(3000);
以上是服务端的代码,接下来看一下客户端的逻辑:
<body>
<div id="app">
<button @click='oauth()'>Github登陆</button>
<div v-if="userInfo">
Hello {{userInfo.login}}
<img :src="userInfo.avatar_url" />
</div>
</div>
<script>
</script>
<script>
axios.interceptors.request.use(
config => {
const token = window.localStorage.getItem("token");
if (token) {
// 判断是否存在token,如果存在的话,则每个http header都加上token
// Bearer,这里没有加这个前缀了,因为不是jwt不需要了
config.headers.common["Authorization"] = token;
}
return config;
},
err => {
return Promise.reject(err);
}
);
var app = new Vue({
el: "#app",
data: {
userInfo: null
},
methods: {
async oauth() {
// 在请求授权时,我们新开一个页面,然后当前页面不断轮训,当服务端设置好了token信息,就去请求用户数据
window.open('/auth/github/login', '_blank')
const intervalId = setInterval(() => {
console.log("等待认证中..");
if (window.localStorage.getItem("authSuccess")) {
clearInterval(intervalId);
window.localStorage.removeItem("authSuccess");
this.getUser()
}
}, 500);
},
async getUser() {
const res = await axios.get("/auth/github/userinfo");
console.log('res:', res.data)
this.userInfo = res.data
}
}
});
</script>
</body>
接下来我们看一下demo效果:
参考文献
以上就是本期的全部分享了,希望可以对各位伙伴有所帮助!