美文网首页
NodeJs实现OAuth2.0协议 简单讲解与示例参考

NodeJs实现OAuth2.0协议 简单讲解与示例参考

作者: 关爱单身狗成长协会 | 来源:发表于2019-04-01 22:50 被阅读0次

OAuth2.0协议的四种模式,分别是授权码模式简化模式密码模式客户端模式。至于这些模式的详细说明网上一搜一大堆,我这里就不做搬运了。我这里来简单实现授权码模式。本打算用Python来给大家说明,后考虑到自己身边会JS的人比Python多,所以将原来Python的代码重新用NodeJs写了一遍。

OAuth2.0协议官网文档说明:

其他网站参考:

代码下载:

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.效果

image

相关文章

网友评论

      本文标题:NodeJs实现OAuth2.0协议 简单讲解与示例参考

      本文链接:https://www.haomeiwen.com/subject/yelibqtx.html