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语言实践

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

39 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
owen hong

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

Conan Zhang

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

owen hong

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

Conan Zhang

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?

Conan Zhang

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

Philipp Li

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

Conan Zhang

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

Philipp Li

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

Conan Zhang

嗯。

rookie

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

Conan Zhang

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

hsinlu

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

Conan Zhang

标准的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”:{}}”

Conan Zhang

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

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

jakey

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

Conan Zhang

🙂

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记住一周或者一个月功能?

Conan Zhang

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这个值需要加密吗?

Conan Zhang

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

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

yf z (追随)

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

Conan Zhang

没看懂的你的问题。

yf z (追随)

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

zhangguoruiwo

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

Conan Zhang

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

zhangguoruiwo

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

zhangguoruiwo

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

Conan Zhang

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

zhangguoruiwo

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

xuehanxin

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

Conan Zhang

index.ejs,是用于渲染HTML的模板

xuehanxin

不鞥理解诶

云烟

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

Conan Zhang

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

[…] 在上一篇文章中,我们介绍了Passport的项目,通过用户名和密码认证登陆。Express结合Passport实现登陆认证 […]

39
0
Would love your thoughts, please comment.x
()
x