OAuth
OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。常见的采用微信、QQ、微博、Facebook、Google账号登陆网站的过程都是采用了OAuth技术。这一章我们会以使用Google账号登陆第三方网站为例,展示如何使用这项技术。
Google OAuth工作流程
- 整个OAuth过程主要设计三个方面,客户端(对于网站而言则是浏览器)、第三方服务器(对应网站的服务器)和Google服务器。当用户点击使用Google账号登陆网站时,第三方服务器会直接把这个请求传递给Google服务器,响应后页面跳转至Google的验证授权页面,询问用户是否同意授权。用户同意后,谷歌服务器会跳转至第三方服务器中,并且在跳转URL上会携带一个code参数,第三方服务器拿到code后会凭借这个code再次向Google服务器发送请求,并换取用户信息。拿到用户信息后,第三方服务器会检查数据库,如果没有这个用户则存入数据库,并登陆成功,如果有则直接登陆成功。与此同时,给浏览器种一个标识用户信息的cookie,此后在cookie的有效期内,浏览器接下来每次对第三方服务器的请求中都会携带cookie,因此可以表示用户身份,做一些需要权限才能做的事情。具体流程如下图所示:
- 我使用passport这个库帮助我们实现验证流程。
passportJS
- 两个问题:
- passportJS会自动化OAuth流程,但需要代码深入到流程细节中,并不能完全自动化整个流程
- 库的结构,实际上我们需要两个库才能使用passportJS——passport、passport strategy,第一个是核心库,用以提供验证流程的工具方法,第二个是针对不同的授权提供方(Google、Facebook、Wechat etc.)所需要的定制方法,也就是说你如果需要同时提供Google、Facebook、Wechat三种验证方式,那你就需要三个strategy库。在 passportjs.org中提供了很多strategies库。
- 安装passport到项目中
npm install --save passport passport-google-oauth20复制代码
- 20的意思是版本为
2.0
,因为npm的包名称中不能有.
,所以就起名为20了,其实这里也可以不加20,那么安装的就是一个1.0
和2.0
的组合版。鉴于现在基本知名的auth provider都已经支持OAuth2.0,所以这里采用2.0
版本。详情参见passport-google-oauth github。 - 使用passport
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy());复制代码
- 在使用Google OAuth之前,需要两个参数appid和api secrect,要获取这两个参数,需要在 console.developers.google.com 上创建项目(只要有Google账号,very easy)
- 创建完项目,进入项目面板,进入api板块,点击
启用Google API
,搜索Google +
,选择Google + API
, 点击启用
现在此API依然不能使用,需要点击点击
创建凭据
按钮,按照提示流程一直走到最后,生成凭据,主要包含两个信息clientID和client密钥。如果想要看详细步骤,参考这里。- 这个流程里需要注意的点是,设置JavaScript授权域和授权回调URL,因为我们现在建立的是一个开发项目,两者分别设为 http://localhost:5000和http://localhost:5000/*
- clientID: 用于生成登陆用的URL
- clientSecrect: 用于证明该APP是否有权访问token
接下来要把刚才生成的clientID和clientSectrect,传入Google OAuth模块中。注意,clientSecrect不能公布,谨慎起见clientID也应该保密。所以我们不希望别人通过查看源代码的形式获取这两个值。目前我们先通过不提交这部分代码的形式做到隐藏这部分信息。
- 创建cong/keys.js,存放clientID和clientSecrect
module.exports = { googleClientId: '1229722414-eeujg12q0q9gvisar.apps.googleusercontent.com', googleClientSecret: 'ANPiCt5QFTa' };复制代码
- 在.gitignore中写入
keys.js
,确保包含敏感信息的文件不会被提交 - 在index.js中,引入keys模块,并将对应的clientKey和clientID传入GoogleStrategy模块中。
- 注意这里还添加了回调URL。这是因为当用户点击授权后,Google服务器会返回一个code到应用的服务器,那我们服务器应该如何接收并处理这个服务器的返回呢。Google服务器返回给app服务器信息,可以看做是一次请求(服务器不就是用来处理请求的吗),所以我们必须要指定请求的route是什么,因此我们需要一个回调URL的参数,google服务器会将code拼接到这个URL的参数里。
- 这里还添加了一个回调函数,所有验证的目的就是为了拿到token,以便用户随后的操作,回调函数定义了拿到token做什么。目前仅仅先把token打出来看一下。
const keys = require('./config/keys'); passport.use(new GoogleStrategy({ clientID: keys.googleClientId, clientSecret: keys.googleClientSecret, callbackURL: '/auth/google/callback' }, (accessToken, refreshToken, profile, done) => {console.log(accessToken)}));复制代码
- 最后我们需要添加一个route handler,用以接收用户login的请求,并进入Google OAuth流程,如下面的代码所示。
- 首先解释一下代码的意思:如果服务器接收到
/auth/google
的请求,使用passport启用Google OAuth的验证流程,需要获取的信息有用户资料和邮箱。 - 这里面的字符串'google'看起来很让人费解,因为在之前的代码中我们并没有任何用这个字符串代表Google OAuth strategy的意思。这是passport广为人诟病的一点。事实上,Google OAuth strategy模块中设定了这一点,也就是说这个模块告诉passport如果
passport.authenticate
方法第一个参数传入了google
,那么就采用Google OAuth strategy模块验证。
- 首先解释一下代码的意思:如果服务器接收到
app.get(
"/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"]
})
);复制代码
- 现在启动我们的本地服务器,访问
localhost:5000/auth/google
,按理应该会弹出google认证的页面,但是不幸的是并没有,这时弹出的是一个400页面,大概的意思是说实际提供的验证回调地址和在console.developers.google.com中设定的不一致。还提供了一个链接,直接访问这个链接就进入了修改验证回调URL的页面。- 为什么会出现这个错误页?还记得之前我们把
已获授权的重定向 URI
这一项设为http://localhost:5000/*
,事实上这里需要严格匹配。之前在代码中我们设定callbackURL
为/auth/google/callback
,所以我们应该在这个修改页面中将已获授权的重定向 URI
这一项设为http://localhost:5000/auth/google/callback
,这样之后应该就能正常弹出授权页面了。 - 为什么需要回调验证URL匹配?我们访问google服务器要求提供授权时,提供的参数是clientID,并且明文传输。攻击者拿到clientID,并把redirect_uri改为恶意网站,那么用户授权后就肯能会跳转到恶意网站,并提供所有的授权信息。显然,这种情况是坚决不能发生的,所以我们需要在google那边配置允许的回调URL,并严格匹配。如果不匹配是不会成功回调的。
- 为什么会出现这个错误页?还记得之前我们把
- 点击对应的Google账户登陆,会跳到一个错误页显示
Cannot GET /auth/google/callback
。我们还没有设置针对回调route的handler,所以当然会报错了。在这个页面的URL中,会看到一个参数code,这就是在之前流程图中提到的Google服务器返回的code。我们app的服务器拿到code后,就可以通过code再次向Google服务器发请求,并拿到用户的资料、邮箱等信息了。所以接下来需要补上对应的route handler。app.get('/auth/google/callback', passport.authenticate('google'));复制代码
- 再次访问
localhost:5000/auth/google
,点击账户登录,可以看到在启动server的控制台中打印出了一坨东西。之前我们在配置passport中传入了一个回调函数,在回调函数中打印出了token。这一坨就是取到的token。- 实际上passport在回调URL的handler中自动将code传递给了google服务器,并换取了token、用户信息(资料、邮箱等)。这些信息时通过函数参数的形式传递回来的。 因此,在这之后,那个打印token的函数被调用,我们的app可以在这个回调函数中利用这些信息做一些不可描述的事情。
- 在继续之前,我们可以先把这些返回的信息打印出来,看看长什么样子。修改代码,重启server,重新访问登陆连接,可以看到控制台中打印出了token(string)、profile(object)、done(function)。
- accessToken: app后续访问用户信息的凭证。
- accessToken过一段时间就会过期,refreshToken会允许我们刷新得到最新的token。
- profile:用户所有的资料。
- done函数的参数有三个:err(错误信息),user(用户信息),info(其他信息)
- 为什么会pending?回调函数中,我们并没有给出响应response。
passport.use( new GoogleStrategy( { clientID: keys.googleClientId, clientSecret: keys.googleClientSecret, callbackURL: "/auth/google/callback" }, (accessToken, refreshToken, profile, done) => { console.log('accessToken', accessToken); console.log('refreshToken', refreshToken); console.log('profile', profile); console.log('done', done); } ) );复制代码
- 至此,所有授权的工作(在passport的帮助下)已经完成,接下来是创建用户信息到数据库、登陆完成。
使用nodemon使开发自动化
- 至此,应该已经厌倦了修改代码,重启server的过程。幸运的是已经有工具使这一切自动化,这个工具就是
nodemon
。 npm install --save-dev nodemon
- 修改package.json
"scripts": { "start": "node index.js", "dev": "nodemon index.js" },复制代码
- 之后只需要在命令行中输入
npm run dev
,就可以启动服务器,并且每次修改代码保存后,nodemon都会帮我们自动重启服务器了。重构目前的代码
- 之前我们把所有的逻辑都写在index.js文件中,为了便于维护和迭代,我们把逻辑分散在不同的目录下。目前我们把逻辑分为三个部分config,routes,services。三个部分的含义如下图所示。重构之后的目录如下所示。基本的工作就是把routehandler的逻辑移动到authRoutes.js中,把配置passport的逻辑,移动到passport.js中,然后在两个文件中引入依赖的包或者其他模块。再在index中引入这两个文件。
├── config │ └── keys.js ├── index.js ├── package-lock.json ├── package.json ├── routes │ └── authRoutes.js └── services └── passport.js复制代码
- routes/authRoutes.js
const passport = require('passport');
module.exports = (app) => {
app.get(
"/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"]
})
);
app.get("/auth/google/callback", passport.authenticate("google"));
}复制代码
- servics/passport.js
const passport = require('passport');
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const keys = require("../config/keys");
passport.use(
new GoogleStrategy(
{
clientID: keys.googleClientId,
clientSecret: keys.googleClientSecret,
callbackURL: "/auth/google/callback"
},
(accessToken, refreshToken, profile, done) => {
console.log('accessToken', accessToken);
console.log('refreshToken', refreshToken);
console.log('profile', profile);
console.log('done', done);
}
)
);复制代码
- index.js
const express = require("express"); const app = express(); require('./services/passport'); require('./routes/authRoutes')(app); app.get("/", (req, res) => { res.send({ hi: "there" }); }); ) const PORT = process.env.PORT || 5000; app.listen(PORT);复制代码