OAuth是一个开源的标准协议,主要用于应用之间的授权访问。OAuth1.0和2.0之间有较大出入,互相不兼容,因此目前主流的应用都是使用2.0版本的协议。
OAuth应用十分广泛,比如google, Facebook, twitter和微博之类的应用都会开放出对应的OAuth接口,便于第三方应用使用已有账号进行授权访问特定资源。
在OAuth 2.0中定义有4个角色,它们分别是:
整个OAuth 认证过程中的流程是:
整个流程和传统的认证请求不同的地方主要就是中间加入了一层授权服务器机制,为了资源访问的安全性,要访问资源不是直接通过用户密码的方式进行的,而是采用token的形式。
客户端主要实现以下两个功能:
重定向到认证服务器上获取权限
使用从服务器获取的token访问受保护的资源
而要获得对应的access token, 客户端通常是使用授权码(authorization code)模式进行授权的。主要流程有:
下面这段node.js代码可以描述上述的工作流程(不保证可运行,原理是一样的):
// 认证服务端信息 const authServer = { authorizationEndpoint: 'http://localhost:3000/authorize', tokenEndpoint: 'http://localhost:3000/token' }; // 客户端信息 const client = { "client_id": "xxx", "client_secret": "xxx", "redirect_uris": ["http://localhost:9000/callback"], "scope": "xxx" }; app.get('/authorize', function (req, res) { // 随机生成state state = randomstring.generate(); const authorizeUrl = buildUrl(authServer.authorizationEndpoint, { response_type: 'code', scope: client.scope, client_id: client.client_id, redirect_uri: client.redirect_uris[0], state: state }); res.redirect(authorizeUrl); }); app.get("/callback", function (req, res) { const resState = req.query.state; // 判断resState与生成的state是否相同 , 此处省略 var code = req.query.code; var form_data = qs.stringify({ grant_type: 'authorization_code', code: code, redirect_uri: client.redirect_uris[0] }); var headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + Buffer.from(querystring.escape(client.client_id) + ':' + querystring.escape(client.client_secret)).toString('base64') }; // 请求认证服务器,获取token var tokRes = request('POST', authServer.tokenEndpoint, { body: form_data, headers: headers } ); if (tokRes.statusCode >= 200 && tokRes.statusCode < 300) { var body = JSON.parse(tokRes.getBody()); if (body.access_token) { // 拿到token后就可以访问资源了 // ... } } });
授权服务器的职责主要是校验客户端,生成token、刷新token并管理不同客户端的token。
下面是不完全代码,主要演示思路:
var refreshTokens = {}; var accessTokens = []; var codes = {}; var requests = {}; var clients = [ {client_id: 'xxx'} ]; //所有有效的客户端 app.get("/authorize", function(req, res){ // 通过url中的client_id, 获取客户端信息 var client = getClient(req.query.client_id); // 此处省略错误处理 if (client) { var rscope = req.query.scope ? req.query.scope.split(' ') : undefined; var cscope = client.scope ? client.scope.split(' ') : undefined; if (__.difference(rscope, cscope).length > 0) { // 如果服务端scope和客户端scope不匹配的话,返回错误信息 res.redirect(buildUrl(req.query.redirect_uri, { error: 'invalid_scope' })); return; } // 此处也可以使用session进行判断 var reqid = randomstring.generate(8); requests[reqid] = req.query; // 允许授权 res.render('approve', {client: client, reqid: reqid, scope: rscope}); return; } }); app.post('/approve', function(req, res) { var reqid = req.body.reqid; var query = requests[reqid]; delete requests[reqid]; if (!query) { res.render('error', {error: 'No matching authorization request'}); return; } if (req.body.approve) { if (query.response_type == 'code') { // 用户授权访问 var rscope = getScopesFromForm(req.body); var client = getClient(query.client_id); var cscope = client.scope ? client.scope.split(' ') : undefined; if (__.difference(rscope, cscope).length > 0) { // 判断scope res.redirect(buildUrl(query.redirect_uri, { error: 'invalid_scope' })); return; } var code = randomstring.generate(8); codes[code] = { request: query, scope: rscope }; var urlParsed = buildUrl(query.redirect_uri, { code: code, state: query.state }); res.redirect(urlParsed); return; } else { var urlParsed = buildUrl(query.redirect_uri, { error: 'unsupported_response_type' }); res.redirect(urlParsed); } } else { var urlParsed = buildUrl(query.redirect_uri, { error: 'access_denied' }); res.redirect(urlParsed); return; } }); app.post("/token", function(req, res){ var auth = req.headers['authorization']; if (auth) { // 检查请求头 var clientCredentials = decodeClientCredentials(auth); var clientId = clientCredentials.id; var clientSecret = clientCredentials.secret; } // 检查请求体 if (req.body.client_id) { if (clientId) { // 如果已经在请求头上检查到认证信息的话,判定为错误 console.log('Client attempted to authenticate with multiple methods'); res.status(401).json({error: 'invalid_client'}); return; } var clientId = req.body.client_id; var clientSecret = req.body.client_secret; } var client = getClient(clientId); // 此处省略客户端信息判断错误处理 if (req.body.grant_type == 'authorization_code') { var code = codes[req.body.code]; if (code) { delete codes[req.body.code]; // 表示该code已被使用 if (code.request.client_id == clientId) { var access_token = generateAccessToken(code) // 使用Bearer Tokens算法生成token,推荐jwt库 // 将生成的token持久化,如果是生产环境,一般需要保存到数据库中 accessTokens.push({ access_token: access_token, client_id: clientId, scope: code.scope }); // 刷新token,便于下次使用 var refreshToken = randomstring.generate(); refreshTokens[refreshToken] = { clientId: clientId }; // 将token信息返回 var token_response = { access_token: access_token, token_type: 'Bearer', scope: code.scope.join(' '), refresh_token: refreshToken }; res.status(200).json(token_response); return; } else { res.status(400).json({error: 'invalid_grant'}); return; } } } else if (req.body.grant_type == 'refresh_token') { // 刷新token算法,与生成token的算法大同小异 } });
目前比较成熟的Node.js oauth2框架可以参考https://github.com/jaredhanson/oauth2orize,例子项目可以参考https://github.com/awais786327/oauth2orize-examples
资源服务器的功能相对简单些,主要是验证token的有效性,然后根据不同的scope,来控制资源的权限。