手记

OAuth认证和Node.js

OAuth是一个开源的标准协议,主要用于应用之间的授权访问。OAuth1.0和2.0之间有较大出入,互相不兼容,因此目前主流的应用都是使用2.0版本的协议。

OAuth应用十分广泛,比如google, Facebook, twitter和微博之类的应用都会开放出对应的OAuth接口,便于第三方应用使用已有账号进行授权访问特定资源。

工作原理

在OAuth 2.0中定义有4个角色,它们分别是:

  • 资源拥有者(Resource Owner) 也就是通常意义下的用户。
  • 客户端(Client): 比较常见的是浏览器
  • 资源服务器(Resource Server): 存放资源的服务器
  • 授权服务器(Authorization Server): 专门用来处理认证的服务器

整个OAuth 认证过程中的流程是:

  1. 客户端询问用户是否进行授权
  2. 用户允许授权后,客户端向认证服务器发送请求,认证服务器此时会发给客户端一个token
  3. 客户端拿到这个token再向资源服务器获得资源

整个流程和传统的认证请求不同的地方主要就是中间加入了一层授权服务器机制,为了资源访问的安全性,要访问资源不是直接通过用户密码的方式进行的,而是采用token的形式。

客户端

客户端主要实现以下两个功能:

  • 重定向到认证服务器上获取权限

  • 使用从服务器获取的token访问受保护的资源

而要获得对应的access token, 客户端通常是使用授权码(authorization code)模式进行授权的。主要流程有:

  • 打开客户端,查看是否用户是否登录,如果没有登录将用户导向认证服务器
  • 申请认证过程中,客户端的uri会带上一堆参数,包括了客户端ID(client_id)、授权类型(response_type: code)、资源访问范围(scope)、客户端状态(state)以及重定向uri(redirect_uri)。
  • 用户在新的地址登录成功后,服务器会生成授权码(authentication_code)并附在重定向uri上,返回给客户端
  • 客户端拿着授权码进一步访问认证服务器,获得token
  • 客户端拿到access token后就可以进一步请求所需要的资源了

下面这段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,来控制资源的权限。

参考资料

1人推荐
随时随地看视频
慕课网APP