OAuth2.0协议的四种模式,分别是授权码模式、简化模式、密码模式、客户端模式。至于这些模式的详细说明网上一搜一大堆,我这里就不做搬运了。我这里来简单实现授权码模式。本打算用Python来给大家说明,后考虑到自己身边会JS的人比Python多,所以将原来Python的代码重新用NodeJs写了一遍。
OAuth2.0协议官网文档说明:
其他网站参考:
- http://wiki.open.qq.com/wiki/mobile/OAuth2.0%E7%AE%80%E4%BB%8B
- http://wiki.connect.qq.com/oauth2-0%E7%AE%80%E4%BB%8B
- https://open.weibo.com/wiki/Oauth
代码下载:
1.授权码模式简单原理说明
原始英文说明可参考:https://tools.ietf.org/html/rfc6749
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B) 重定向到授权服务器授权页面
+----|-----+ Client Identifier +---------------+
| 浏览器 -+----(A)-- & Redirection URI ---->| 授权服务器 |
| User- | | |
| Agent | 用户确定授权 | Authorization |
| -+----(B)-- User authenticates --->| Server |
| | | |
| | 授权服务器返回授权码 | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ 应用后台通过授权码申请令牌 | |
| 应用 |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| | 服务器审核通过后返回令牌 |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
Note: The lines illustrating steps (A), (B), and (C) are broken into
two parts as they pass through the user-agent.
Figure 3: Authorization Code Flow
假设授权认证服务器(Authorization Server)上的账号已经登陆,客户端应用(Client)未登陆
(A)用户通过浏览器访问客户端应用,应用重定向到认证服务器。
(B)用户确定授权。
(C)确定授权后,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码(code)。
(D)应用收到授权码,附上早先的"重定向URI",同时还可以添加认证字符串或密钥,向认证服务器申请令牌(token)。这一步是在客户端应用的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端应用发送访问令牌(access token)和更新令牌(refresh token)。
1.代码示例
代码授权服务.js
:
const express = require('express'); //express 4
const app = express();
const bodyParser = require('body-parser');
const multer = require('multer');
const upload = multer();
const cookieParser = require('cookie-parser')
app.use(cookieParser());
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
//模拟会话 这里偷下懒啊
const Sessions = {};
//模拟数据库中用户数据
const Users = [
{ id: "01", name: "root", rname: "超级管理员", pwd: "root", mail: "cjgly@233.com", age: "28" },
{ id: "02", name: "admin", rname: "管理员", pwd: "admin", mail: "gly@233.com", age: "27" },
{ id: "03", name: "test1", rname: "测试1", pwd: "test1", mail: "cs1@233.com", age: "18" },
{ id: "04", name: "test2", rname: "测试2", pwd: "test2", mail: "cs2@233.com", age: "18" },
];
//模拟授权应用列表
const ClientApps = [
{
//客户ID
client_id: "1881139527",
//应用名称
client_name: "测试应用2",
//应用网站
home_url: "http://app2.com:8082",
//描述
info: "一些自定义信息(比如APP2)",
//创建时间
creation_date: "2019-3-29",
//认证密钥
authorization: '623098a7c4018ae2f840b0d8763fb61732f1f7ce'
}
];
//首页
app.get('/', function (req, res) {
let sid = req.cookies.session_id;
let s = Sessions[sid];
if (sid && s) {
//用户登陆显示用户信息
let user = s.user;
return res.send(`<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h3>Hello ${user.name}</h3><a href="/login">退出登陆</a>
</body>
</html>`);
}
//用户未登录则重定向
return res.redirect('/login');
});
//登陆
app.get('/login', function (req, res) {
let sid = req.cookies.session_id;
if (sid) {
//清除会话与coockie
if (Sessions[sid]) delete Sessions[sid];
res.clearCookie('session_id', { domain: '.app1.com' });
}
res.write(`<html>
<head>
<meta charset="UTF-8">
</head>
<body> <h1>APP1:授权服务-登录</h1>
<form action="/onlogin" method="post">
用户名: <input name="name" type="text" /><br />
密码: <input name="pwd" type="password" />
<input value="提交" type="submit" />
</form>
</body>
</html>`);
res.send();
});
//提交登陆
app.post('/onLogin', upload.array(), function (req, res) {
let name = req.body.name;
let pwd = req.body.pwd;
if (name && pwd) {
let user = Users.find(u => name == u.name && pwd == u.pwd);
if (user) {
//创建登陆会话
let sid = Buffer.from(user.name + "_" + (+ new Date())).toString("base64");
Sessions[sid] = { user: Object.assign({}, user, { pwd: "", id: "" }) };
// res.cookie('session_id', sid);
res.cookie('session_id', sid, { domain: '.app1.com' });
return res.send(`正在跳转....<script>setTimeout(function(){location.href="/";},2000);</script>`);
}
}
res.send(`<html>
<head>
<meta charset="UTF-8">
</head>
<body> 登陆失败<br/><a href="/">返回首页</a></body>
</html>`);
});
//授权页面
app.get('/oauth2/authorize', function (req, res) {
let sid = req.cookies.session_id;
let client_id = req.param('client_id');
if (sid && client_id) {
let s = Sessions[sid];
if (s && s.user) {
for (let app of ClientApps) {
if (app.client_id == client_id) {
s.flag = (+new Date() - 200000000000).toString(); //生成请求标识示例
//显示授权页面
res.write(`<html>
<head>
<meta charset="UTF-8">
</head> <h1>APP1:授权请求</h1>
<h4>发起应用:${app.client_name}</h4>
<h4>应用网址:<a href="${app.home_url}">${app.home_url}</a></h4>
<h4>描述:${app.info}</h4>
<form action="" method="post">
<input value="${s.flag}" name="flag" hidden style="display: none;" />
<input value="授权" type="submit" />
</form>
<script>document.querySelector("form").action=location.href;</script></body>
</html>`);
res.send();
return;
}
}
}
}
//重定向到登陆
return res.redirect('/login');
});
//授权请求
app.post('/oauth2/authorize', upload.array(), function (req, res) {
let sid = req.cookies.session_id;
let s = Sessions[sid];
if (!sid || !s || !req.body || !req.body.flag || req.body.flag != s.flag) return res.redirect('/login');
// let scope = request.query.scope;//表示申请的权限范围,可选项
let client_id = req.param('client_id');//表示客户端的ID,必选项
let redirect_uri = req.param('redirect_uri');//表示重定向URI,可选项
let response_type = req.param('response_type');//表示授权类型,必选项,此处的值固定为"code"
let state = req.param('state');//表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
if (client_id && redirect_uri && response_type == "code" && s.user) {
for (let app of ClientApps) {
if (app.client_id == client_id) {
//记录
Sessions[sid].client_id = client_id;
Sessions[sid][client_id] = Object.assign({}, app);
Sessions[sid][client_id].redirect_uri = redirect_uri;
Sessions[sid][client_id].grant_type = "authorization_code";
let code = Sessions[sid][client_id].code = (+new Date() - 100000000000).toString(); //生成code 你可以用随机数啥的
return res.redirect(`${redirect_uri}?code=${code}&state=${state}`);
}
}
}
//这里重定向到登陆,可以自行根据实际情况做处理
return res.redirect('/login');
});
app.post('/oauth2/token', upload.array(), function (req, res) {
let grant_type = req.param('grant_type');//表示使用的授权模式,必选项,此处的值固定为"authorization_code"
let code = req.param('code');//表示上一步获得的授权码,必选项
let redirect_uri = req.param('redirect_uri');//表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致
let client_id = req.param('client_id');//表示客户端ID,必选项
let s, sid;
for (let k in Sessions) {
s = Sessions[k];
if (s.client_id == client_id) sid = k;
}
if (!sid) return res.json({ "access_token": null });
let authorization = req.headers.authorization;
s = Sessions[sid];
if (!s) return res.json({ "access_token": null });
let c = s[client_id];
if (c && c.grant_type == grant_type && c.code == code && c.redirect_uri == redirect_uri && c.authorization == authorization) {
if (!s.tokens) s.tokens = {};
let resBody = {
"access_token": sid,//表示访问令牌,必选项 ,我这里就直接用sessionID来模拟token
"token_type": "Bearer",//表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
"expires_in": 3600,//表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间
"refresh_token": null,//表示更新令牌,用来获取下一次的访问令牌,可选项
"scope": null//scope:表示权限范围,如果与客户端申请的范围一致,此项可省略
};
s.tokens[sid] = Object.assign({ timestamp: (+new Date()) }, resBody);
return res.json(resBody);
}
return res.json({ "access_token": null });
});
//验证是否到期
function tokenIsExpires(token, session) {
//自行修改以下代码 来模拟到期的情况
let tokenInfo = session.tokens[token];
if (!token) return true;
let nowTimestamp = (+new Date());
let tokenTimestamp = tokenInfo.timestamp;
let y = (nowTimestamp - tokenTimestamp) > (tokenInfo.expires_in * 1000);
if (y) delete session.tokens[token];
return y
}
//模拟获取用户信息
app.post('/user/getuserinfo', upload.array(), function (req, res) {
let token = req.param('access_token');//表示客户端的ID,必选项
if (token) {
if (!tokenIsExpires(token, Sessions[token])) {
let user = Object.assign({}, Sessions[token].user, { pwd: "", id: "" });
if (user) return res.json({ "error": null, "success": true, "data": user });
}
}
res.json({ "error": "授权验证失败", "success": false });
});
app.listen(8081);
代码应用服务.js
:
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser')
const fetch = require("node-fetch");
const crypto = require('crypto');
app.use(cookieParser());
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
//模拟会话
const Sessions = {};
//首页
app.get('/', async function (req, res) {
let sid = req.cookies.session_id;
let s = Sessions[sid];
//判断是否存在会话
if (sid && s) {
let user = s.user;
//判断是否存用户信息
if (!user) {
//模拟通过token获取用户信息
let data = await getuserinfo(req, res);
console.log("test get:", data);
if (data.data) user = s.user = data.data;
}
//存在用户信息则展示
if (user) {
return res.send(`<html>
<head>
<meta charset="UTF-8">
</head><h1>APP2</h1>
<p>用户名:${user.name}</p>
<p>真实姓名:${user.rname}</p>
<p>邮箱:${user.mail}</p>
<p>年龄:${user.age}</p>
<a href="/login">退出</a></body>
</html>`);
}
}
return res.redirect('/login');
});
//login
app.get('/login', function (req, res) {
let sid = req.cookies.session_id;
if (sid) {
let s = Sessions[sid];
if (s) { delete s; };
res.clearCookie('session_id', { domain: '.app2.com' });
}
res.write(`<html>
<head>
<meta charset="UTF-8">
</head><h1>APP2</h1><a href="/oauth/app1" target="_blank">通过(应用1)获取授权</a><br />
<a href="/oauth/app2" target="_blank">通过(应用2)获取授权</a><br />
<a href="/oauth/app3" target="_blank">通过(应用3)获取授权</a></body>
</html>`);
res.send();
});
//认证
app.get('/oauth/:id', function (req, res) {
//根据自定义规则生成state示例
let state = req.headers["user-agent"] + "_" + (+new Date());
let md5 = crypto.createHash('md5');
state = md5.update(state).digest('hex');
//当然啦state可以存在session中
res.cookie('state', state, { domain: '.app2.com' });
// console.log(state);
//根据参数判断请求认证的地址
if (req.params.id == "app1") return res.redirect('http://app1.com:8081/oauth2/authorize?client_id=1881139527&redirect_uri=http%3A%2F%2Fapp2.com%3A8082%2Foauth_callback%2Fapp1&response_type=code&state=' + encodeURI(state));
res.send('此应用未授权');
});
//认证回调 通过后台请求 token
const OauthCB = {
"app1"(req, res) {
//获取与解析参数
let code = req.param('code');
let state = req.param('state');
state = decodeURI(state);
res.clearCookie('state', { domain: '.app2.com' });
//验证state是否一致
if (state != req.cookies.state)
return res.redirect('/oauth_fail?error=%E8%AE%A4%E8%AF%81%E5%A4%B1%E8%B4%A5');
//通过后台发送相关信息来获取token
fetch(`http://app1.com:8081/oauth2/token?grant_type=authorization_code&code=${code}&redirect_uri=http%3A%2F%2Fapp2.com%3A8082%2Foauth_callback%2Fapp1&client_id=1881139527`, {
method: 'POST',
// body: null,
redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect
timeout: 10000, //ms
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': '623098a7c4018ae2f840b0d8763fb61732f1f7ce'//<====认证字符串或密钥
}
}).then(function (res) {
console.log("Response Headers ============ ");
res.headers.forEach(function (v, i, a) {
console.log(i + " : " + v);
});
return res.json();
}).then(function (data) {
console.log("Response Body ============ ");
console.log(data);
//判断是否请求成功
if (data.access_token) {
//创建会话并记录token
Sessions[state] = { token: data.access_token, oauth2: { url: "http://app1.com:8081", name: "app1" } };
res.cookie('session_id', state, { domain: '.app2.com' });
}
res.redirect('/');
}).catch(function (e) {
console.log(e);
res.redirect('/oauth_fail?error=' + e.toString());
});
},
"app2"(req, res) {
res.redirect('/oauth_fail?error=%E5%BA%94%E7%94%A8%E4%B8%8D%E5%AD%98%E5%9C%A8');
}
};
app.get('/oauth_callback/:app', function (req, res) {
let fun = OauthCB[req.params.app];
if (fun) fun(req, res);
else return res.redirect('/oauth_fail?error=%E5%BA%94%E7%94%A8%E4%B8%8D%E5%AD%98%E5%9C%A8');
});
//通过token获取用户信息
async function getuserinfo(req, res) {
let sid = req.cookies.session_id;
let s = Sessions[sid];
//判断是否存在会话与认证服务器
if (s && s.oauth2.name == "app1") {
//请求数据 请求数据的参数根据情况添加
return await fetch(`${s.oauth2.url}/user/getuserinfo?access_token=${s.token}`, {
method: 'POST',
// body: null,
redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect
timeout: 100000, //ms
}).then(function (res) {
console.log("Response Headers ============ ");
res.headers.forEach(function (v, i, a) {
console.log(i + " : " + v);
});
return res.json();
});
}
};
//模拟失败提示页面
app.get('/oauth_fail', function (req, res) {
let error = req.param('error');
res.write(`<html>
<head>
<meta charset="UTF-8">
</head><h3>错误: ${error}</h3><a href="/login">返回登陆页面</a></body>
</html>`);
res.send();
});
app.listen(8082);
3.测试部署
假设我们现在有一个认证服务A与客户端应用B
A的域名是http://app1.com
B的域名是http://app2.com
直接在操作系统的host文件中添加了这两个域名
运行授权服务.js 浏览器访问 http://app1.com:8081
运行应用服务.js 浏览器访问 http://app2.com:8082
首先登陆 http://app1.com:8081/login
(登陆账号密码请看源代码)
登陆完成后再在 http://app2.com:8082/login
中点击 通过(应用1)获取授权
点击后会打开 http://app1.com:8081/oauth2/authorize?
授权确认页面
点击授权后授权服务会重定向并返回code授权码到 应用服务 给定的授权回调地址(http://app2.com:8082/oauth_callback/app1
)
应用服务 后端根据服务类型发送code授权码、redirect_uri重定向地址标识以及一些密钥标识码来请求token令牌,地址是http://app1.com:8081/oauth2/token
授权服务 解析与验证参数,验证成功后将token令牌以及其他相关信息返回给应用服务
应用服务 获取token成功后 使用token来获取授权服务中的用户信息
4.效果

网友评论