Express结合Passport实现登陆认证

从零开始nodejs系列文章,将介绍如何利Javascript做为服务端脚本,通过Nodejs框架web开发。Nodejs框架是基于V8的引擎,是目前速度最快的Javascript引擎。chrome浏览器就基于V8,同时打开20-30个网页都很流畅。Nodejs标准的web开发框架Express,可以帮助我们迅速建立web站点,比起PHP的开发效率更高,而且学习曲线更低。非常适合小型网站,个性化网站,我们自己的Geek网站!!

关于作者

  • 张丹(Conan), 程序员Java,R,PHP,Javascript
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/nodejs-express-passport/

nodejs-passport

前言

登陆认证,是每个应用都需要的基础功能。但很多的时候,却都被大家所忽略,不仅安全漏洞严重,而且代码紧耦合,混乱不堪。

Passport项目,正是为了解决登陆认证的事情,让认证模块更透明,减少耦合!

目录

  1. 什么是认证(Authentication)?
  2. Passport项目介绍
  3. Express结合Passport实现登陆认证

1. 什么是登陆认证(Authentication)?

认证又称“验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。身份验证的方法有很多,基本上可分为:基于共享密钥的身份验证、基于生物学特征的身份验证和基于公开密钥加密算法的身份验证。

登陆认证,是用户在访问应用或者网站时,通过是先注册的用户名和密码,告诉应用使用者的身份,从而获得访问权限的一种操作。

几乎所有的应用都需要登陆认证!

2. Passport项目介绍

Passport项目是一个基于Nodejs的认证中间件。Passport目的只是为了“登陆认证”,因此,代码干净,易维护,可以方便地集成到其他的应用中。

Web应用一般有2种登陆认证的形式:

  • 用户名和密码认证登陆
  • OAuth认证登陆

Passport可以根据应用程序的特点,配置不同的认证机制。本文将介绍,用户名和密码的认证登陆。

项目网站:http://passportjs.org/

3. Express结合Passport实现登陆认证

系统环境:

  1. Win7 64bit 旗舰版
  2. node v0.10.5
  3. npm 1.2.19

1). 新建项目


D:\workspace\javascript>express -e nodejs-passport
D:\workspace\javascript>cd nodejs-passport && npm install
D:\workspace\javascript\nodejs-passport>npm install passport
D:\workspace\javascript\nodejs-passport>npm install passport-local

2). 实现Session的认证:

  • 启用connet的session中间件
  • connet的session中间件,同时依赖于connect的cookieParser中间件
  • 配置passport中间件

关于connect框架的详细说明,请参考文章:Nodejs基础中间件Connect

修改app.js


app.use(express.cookieParser())
app.use(express.session({secret: 'blog.fens.me', cookie: { maxAge: 60000 }}));
app.use(passport.initialize());
app.use(passport.session());

3). 定义认证策略:

LocalStrategy策略,用于匹配本地环境的用户名和密码,可以扩展这个策略,通过数据库的方式进行匹配。

修改app.js


var passport = require('passport')
    , LocalStrategy = require('passport-local').Strategy;

passport.use('local', new LocalStrategy(
    function (username, password, done) {
        var user = {
            id: '1',
            username: 'admin',
            password: 'pass'
        }; // 可以配置通过数据库方式读取登陆账号

        if (username !== user.username) {
            return done(null, false, { message: 'Incorrect username.' });
        }
        if (password !== user.password) {
            return done(null, false, { message: 'Incorrect password.' });
        }

        return done(null, user);
    }
));

passport.serializeUser(function (user, done) {//保存user对象
    done(null, user);//可以通过数据库方式操作
});

passport.deserializeUser(function (user, done) {//删除user对象
    done(null, user);//可以通过数据库方式操作
});

4). 路由控制和登陆认证

路由页面

  • /: 首页,用于登陆,未登陆用户只能访问首页
  • /login: 登陆请求,用户登陆时,POST到登陆请求,认证成功跳到用户页,认证失败回到首页
  • /users: 用户页,用户通过登陆认证后,可以访问用户页
  • /logout: 登出请求,用户退出系统,GET到登出请求,页面自动跳回首页

修改app.js


app.get('/', routes.index);
app.post('/login',
    passport.authenticate('local', {
        successRedirect: '/users',
        failureRedirect: '/'
    }));

app.all('/users', isLoggedIn);
app.get('/users', user.list);
app.get('/logout', function (req, res) {
    req.logout();
    res.redirect('/');
});

function isLoggedIn(req, res, next) {
    if (req.isAuthenticated())
        return next();

    res.redirect('/');
}

5). 运行程序

passport-login

运行日志


D:\workspace\javascript\nodejs-passport>node app.js
Express server listening on port 3000
POST /login 302 389ms - 68b
GET /users 200 2ms - 50b
GET /logout 302 2ms - 58b
GET / 200 7ms - 540b
GET /stylesheets/style.css 304 6ms
POST /login 302 2ms - 58b
GET / 200 2ms - 540b
GET /stylesheets/style.css 304 2ms

6). 完整的代码

  • app.js代码
  • index.ejs代码
  • user.js代码

app.js代码


var express = require('express')
    , routes = require('./routes')
    , user = require('./routes/user')
    , http = require('http')
    , path = require('path')
    , app = express();

var passport = require('passport')
    , LocalStrategy = require('passport-local').Strategy;

app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser())
app.use(express.session({secret: 'blog.fens.me', cookie: { maxAge: 60000 }}));
app.use(passport.initialize());
app.use(passport.session());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

if ('development' == app.get('env')) {
    app.use(express.errorHandler());
}

passport.use('local', new LocalStrategy(
    function (username, password, done) {
        var user = {
            id: '1',
            username: 'admin',
            password: 'pass'
        }; // 可以配置通过数据库方式读取登陆账号

        if (username !== user.username) {
            return done(null, false, { message: 'Incorrect username.' });
        }
        if (password !== user.password) {
            return done(null, false, { message: 'Incorrect password.' });
        }

        return done(null, user);
    }
));

passport.serializeUser(function (user, done) {//保存user对象
    done(null, user);//可以通过数据库方式操作
});

passport.deserializeUser(function (user, done) {//删除user对象
    done(null, user);//可以通过数据库方式操作
});

app.get('/', routes.index);
app.post('/login',
    passport.authenticate('local', {
        successRedirect: '/users',
        failureRedirect: '/'
    }));

app.all('/users', isLoggedIn);
app.get('/users', user.list);
app.get('/logout', function (req, res) {
    req.logout();
    res.redirect('/');
});

http.createServer(app).listen(app.get('port'), function () {
    console.log('Express server listening on port ' + app.get('port'));
});

function isLoggedIn(req, res, next) {
    if (req.isAuthenticated())
        return next();

    res.redirect('/');
}

index.ejs代码


<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1>Login</h1>
<form action="/login" method="post">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>
</body>
</html>

user.js代码


exports.list = function (req, res) {
var html = "<h2>你好, " + req.user.username + "</h2><a href='/logout'>退出</a>";
res.send(html);
};

通过Passport中间件,我们就把登陆认证和应用程序分离了出来,从而保证了更清晰代码结构。当然,我们也可不用Passport,在Express中直接实现登陆认证,可以参考文章:Nodejs开发框架Express3.0开发手记–从零开始

转载请注明出处:
http://blog.fens.me/nodejs-express-passport/

打赏作者

This entry was posted in Javascript语言实践

  • owen hong

    我现在正在做登录,但是有个权限控制的问题,楼主的路由app.all(‘/users’, isLoggedIn);每个页面通过这个段代码来控制是否登录,如果页面过多的情况下,那不是每个页面都需要在他之前写一个
    app.all(‘/users’, isLoggedIn);来判断是否登录,《nodejs实战指南》登录判断就是那么做的,有没有更简洁的方法来判断

    • 这个问题有两种解决办法:
      1. 所以需要登陆验证的页面,统一用/auth/xxx,这样的路径格式,代码只需要对/auth/* 路径做控制就行了。
      2. 路径用正则表达式来匹配, 比如:’^[user|member|auth]$’

  • owen hong

    你上面的案例我感觉不科学啊,登录表单的内容怎么提交到后台你都没有设置local接收呀,req.body.username没有这句话后台怎么操作呢,难道passport这个模块本身就有接收了吗,求解释

    • 1. 在local里,可以自己增加读取数据库的代码。
      2. POST后,直接就传到了passport的模块里,passport已经封装好了,自己看源代码。

      • owen hong

        请教下,你上面的例子我已经跑起来了,都没什么问题,但是现在就是如果报错了之后不是会判断验证后弹出return done(null, false, { message: ‘Incorrect username.’ });对应的消息吗,但是不知道为什么报错后不会显示错误信息呢,而直接跳转到了failureRedirect: ‘/admin/login’设置的页面中,这个倒不是问题,不知道能否做到跳转页面后以json的形式返回数据来,尝试了下因为passport插件不能使用res,req所以不知道怎么处理

      • jackey

        是否可以理解未local 类型的验证时, 表单post请求中必须包含username, password?

        • 对,默认是这两个字段名,如果你需要修改,中间封装一层或者改一下passport的原代码。

  • Philipp Li

    如果登录的时候需要将username和password与数据库进行匹配,怎样将参数传给passport

    • 在代码验证username和password的,增加数据访问就行了。
      passport.use(‘local’, new LocalStrategy(

      • Philipp Li

        已经实现了,原来参数默认是直接传过去的

  • rookie

    您好,我想请问,我登录之后,用户信息保存在session里,之后每个页面都要显示
    用户名(用户名是header的一块),那我怎么做合适呢,还是每个页面通过session往前段传

    • 每个面页,都要从后端传这个值到前端。

  • hsinlu

    您好,如何实现RSA前台用公钥对输入密码加密,nodejs接收后,再根据私钥解密

    • 标准的passport好像没有提供这个功能,你需要用插件。
      比如:https://github.com/bergie/passport-saml

  • jakey

    我遇到一个问题,使用passport登陆后,session在redis中生成了。后续进行其他url访问时,它又会生成个另外的session(未验证过的)。请教大神有无思路啊
    认证是生成了这个有user认证的session
    127.0.0.1:6379[2]> GET sess:fCohB0Jtgn_1KDlpzz5WcRmR89RbDQie

    “{“cookie”:{“originalMaxAge”:null,”expires”:null,”httpOnly”:true,”path”:”/”},”passport”:{“user”:1014503}}”

    进行其他链接访问时,有产生了这个session,
    127.0.0.1:6379[2]> get sess:2FQQp28zNRcrt9PbgcuRjB1sIDCVVQ7Z

    “{“cookie”:{“originalMaxAge”:null,”expires”:null,”httpOnly”:true,”path”:”/”},”passport”:{}}”

    • 程序可以这样设计。用户登陆后,生成一个状态码active=TRUE,保存这个状态码到redis,每个请求都要检查这个状态码;用户登出或超时,这个状态码active=FALSE。

      你需要确认,你使用框架生成的session是不是同理等于状态码,如果是则用,如果不是则不用。

      • jakey

        多谢,找到原因了,是客户端cookie功能未打开。

  • jakey

    补充下中间件加载顺序哈

    this.http.use(morgan(‘dev’));

    this.http.use(cookieParser());

    this.http.use(bodyParser.urlencoded({extended: true}));

    this.http.use(bodyParser.json());

    this.http.use(methodOverride());

    this.http.use(compress());

    this.http.use(session({

    secret: “xxxxxx”,

    cookie: { maxAge: 60000 },

    name: “xxx.xxxxxx”,

    store: new RedisStore(opts.session_redis),

    proxy: true,

    resave: true,

    saveUninitialized: true

    }));

    this.http.use(passport.initialize());

    this.http.use(passport.session());

  • lyp0110

    请问我怎么做出客户端cookie记住一周或者一个月功能?

    • app.use(express.session({secret: ‘blog.fens.me’, cookie: { maxAge: 60000 }}))

      maxAge设置就行了,保证主程序不重启。

  • lyp0110

    app.use(express.session({secret: ‘blog.fens.me’, cookie: { maxAge: 60000 }})) 如果这样设置的话是不是session和cookie的超时时间是一样的啊?如果session超时了,就算是cookie设置了很长时间,还是需要重新登录,是不是这样?另外secret这个值需要加密吗?

    • 是的,对于node来说session和cookie是一个东西,和PHP机制是一样的,和JAVA是完全不同的。一个在服务端存储,一个在客户端存储,两个要匹配才能保证一直登陆有效。

      secret 加上吧,不过这里加密只是一种很初级的加密手段,黑客真想攻击,也是很容易的。

  • yf z (追随)

    我的node服务器要和restful的java服务器通信,所以用户信息不在这里,也不能序列化到session和反序列化,还有什么解决方案呢,暂时只想到在new Strategy中用可以过期的用户信息生成的token来控制过期,也避免重复请求服务器。还有一个问题就是,res.redirect,为什么返回到页面一个对象,显示undefined,手动点击链接重定向,这是什么问题。

    • 没看懂的你的问题。

      • yf z (追随)

        解决了。都是看了别人的帖子,所以对序列化以及反序列化产生了误解。至于redirect是把render的参数写到redirect所以出错了。

  • zhangguoruiwo

    routes = require(‘./routes’)这有点问题,没有给出index文件,我是菜鸟

    • 默认会找到 routes目录 下面的index文件。

      • zhangguoruiwo

        哦哦,json文件中的依赖项是自己一个个添加的吗?谢谢帮我解惑

        • zhangguoruiwo

          我知道为什么报错了,因为我装的express4 ,与3有很大不同,所以要装中间件,但是问题又来了,他怎说我的routes下的index.js文件中的router.get(‘/’, function(req, res, next)有问题 ,错误类型Cannot call method ‘get’ of undefined

          • express4和express3有很大的不一样。你可以先用express3运行,本文的例子,等通过了,再切换到express4去开发自己的应用。

      • zhangguoruiwo

        不知道为啥我这总是报模块没安装的那种错误,Most middleware (like favicon) is no longer bundled with Express

      • xuehanxin

        routes目录下不是放路由的么,请问下,那个index.ejs是放在route下么

    • xuehanxin

      不鞥理解诶

  • 云烟

    请问博主是否可以写一篇关于node RBAC用户权限控制的文章?

    • 我之前找过一个node RBAC的包,当时没有特别好的。最近可能没有时间,写这方面的文章。

  • Pingback: Passport现实社交网络OAuth登陆 | 粉丝日志()