从单页应用的登录功能看node的cookie和session处理(1)

1,455

在开发一个网站时,绝大多数数情况都会用到登录、自动登录功能。本篇是第一篇,用cookie和session来记录用户数据。

大家都知道http协议是无状态的,也就是,从这个页面到另一个页面,服务端是不知道是否为同一用户的。所以,慢慢发展成了前端用cookie保存sessionID,后端使用session进行加密及解密的一套方法。

一、cookie和session总体思路

在这里我稍微讲一下cookie和session是如何合作的,我们前端人员刚接触node开发时,思维可能是需要进行转化的,可能转化需要一段时间。但是当你搞明白了以后,成为全栈工程师后,就会对整个流程了然于胸了。这里使用express来讲解

首先,明确要记录用户,cookie里应该要存什么。注意,cookie里尽量不要存储密码和用户名,一是有被窃取的危险,而是增加cookie的大小。(当然并不是说一定不能这么做,比如把用户名和密码进行md5等方式处理,也相对会很安全)cookie里存储的是sessionID,那这个sessionID是如何跟服务端进行交互的,后面详细讲。

express-session是处理session的中间件。这个中间件如果阅读下源码,就会看到,主要是进行了两件事:

(1)请求体中没有cookie的时候

用户的请求过来以后,如果在请求体中没有cookie,则创建一个新的session,并且创建一个不会重复的sessionID, 把seesionID和session在store里进行关联。(store默认是内存)

MemoryStore.prototype.set = function set(sessionId, session, callback) {
  this.sessions[sessionId] = JSON.stringify(session)
  callback && defer(callback)
}

当setCookie的时候,把注册session中间件中的option进行处理:

具体是把option中的name, secret跟生成的sessionID进行签名加密,设置res.setHeader('Set-Cookie', header) 把加密后的结果传到客户端。

(2)当再次访问页面的时候

这时,cookie就会随请求头到了服务端, 那么express-session就会对cookie进行解密,把sessionID解出来,然后通过这个id,就找到了store中存储的session,也就拿到session中的用户数据,比如username等(前提是在登录时,把username存到了session上面,比如req.ression.username = user)。

这时,服务端就知道访问者是谁了,就可以把属于这个访问者的数据返回到页面中。

二、真实项目开发

上面讲解了整体流程,虽然没有展开讲,但我理解可以帮助大家抓到整体脉络。

好了,下面开始展开讲代码。具体的各个key是什么含义,我就不讲了,具体可看文档。

express-session是个中间件,那么就需要把这个中间件加载到express程序中。

const session = require('express-session');
app.use(session({
    secret: 'test-node-session',
    resave: false,
    saveUninitialized: true,
    cookie: {
        path: '/',
        httpOnly: true,
        secure: false,
        // 如果不设置maxAge,则退出关闭浏览器tab,当前cookie就会过期
        //maxAge: 1000 * 60 
    }
}));

通过上面设置,当一个请求来了以后,如果之前页面没有对应的cookie,则产生一个新的cookie,如果有,会进行更新后重新发送到浏览器中。

当然,页面请求后就会产生这个name为connect.sid的cookie。 这时我们进行登录:

登录时,通过post请求,把数据和cookie都给到了服务端。

app.post('/author/gologin', (req, res) => {
    // 通过req.body拿到用户名和密码、是否自动登录
    ...
    // 去数据库里进行匹配,如果匹配到,说明登录成功
    ....
    // 这时,可以把用户名存储到session中,
    req.session.user = req.body.username;
    // 因为我们是单页应用,所以返回json,让前端路由到对应的页面
    res.json({
        code: 0,
        mesg: ''
    });
})

前端页面拿到请求的返回值,决定路由到哪个页面。

// 使用vue
axios.post('/author/gologin', {
        data: JSON.stringify(this.accountFormItem)
    })
    .then(res => {
        console.log('登录账号返回数据:', JSON.stringify(res));
        switch (+res.data.code) {
            case 0:
                this.$router.push('/go/to/path/list');
                break;
            case 2004:
                this.inputError = true;
                break;
            default:
                break;
        }
    })
    .catch(err => {
        console.error('登录账号出现错误:', err);
    });

这时,vue-router会进行导航,到了对应的页面。 当到了新的页面后,比如到了列表页,那就会涉及去访问列表的异步接口,

这里注意,应该在所有的异步请求中去判断一下用户是否存在,也就是先查一下用户是否已经登录。比如客户端发送了一个get请求 '/get/total/list', 到了服务端,进行处理:

// node进行处理
app.get('/get/total/list', (req, res) => {
    if (!req.session.user) {
        return res.status(401).send({msg: 'Unauthorize'});
    }
    // 去数据库里查数据
    ...
    // 把数据返回给客户端
    return res.json({code: 0, data: result});
})

在上面的代码中,有很重要的一句话,那就是如有找到req.session.user,则继续流程,如果没有找到,就返回401状态,也就是未认证,这个状态是符合http语义的。

那么服务端返回这个状态,客户端需要拿到并且路由到登录页。这种情况一般不会出现,但是要处理,比如说有人更改了cookie的时候,就匹配不到用户数据了。

// 客户端使用axios,可以对数据的返回进行拦截,在单页的app.js页面
axios.interceptors.response.use(response => {
     // 查看返回的状态
     return response
    },
    error => {
        if(error.response.status === 401) {
            router.push('/login');
        }
    }
);

这时页面回到了登录页,要求用户重新登录。

上面就是整个过程了。当然,上面还是个比较粗略的实现,没有包含用户直接刷新页面的情况。下面稍微提一下。

比如当用户直接刷新列表页,我们知道,单页应用只有一个html入口文件,比如默认是index.html,那么过程就是需要把index.html返回到浏览器,浏览器根据路由渲染对应的页面,然后进行页面内容的异步请求。(具体如何做到,请看我对单页应用在node中运行起来的相关文章)

这里有个体验问题: 如果用户在这时把cookie删除了,那么页面会先进行上面说的步骤,最后才返回401,然后再跳转到登录页,会有一个很明显的闪烁过程,先把列表页加载了出来,又跳到了登录页,用户体验不好。

那么我们可以做些优化。

优化点

  • 在登录的时候,可以把用户数据,比如用户名保存在浏览器的sessionStorage中,当刷新页面时,先通过vue的路由守卫来判断有没有用户名,如果没有,则直接跳到登录页,就不需要经过上面复杂而又难看的流程中去。
  • sessionStorage不太需要担心泄漏,因为只要xss做好过滤,别有用心的人拿不到,并且当关闭浏览器时,会自动清空。
  • 当用户关闭浏览器后,又打开,这时,因为sessionStorage已经没有了,所以,需要在路由守卫中异步请求一下,看下用户是否还在登录状态(比如7天自动登录),进行处理。
// vue 全局路由守卫
router.beforeEach((to, from, next) => {
    function getRouteEach(to, next) {
        if (sessionStorage.getItem('testuser')) {
            // 如果url是登录页,则路由到内容页
            if (to.path === '/login') {
                return next({path: '/to/the/path/list'});
            }
            next();
        } else {
            // 如果发现没有登录,则去请求一次登录状态
            axios.get('/api/getsid')
            .then(res => {
                // 服务端有登录状态
                if (res.data.code === 0) {
                    sessionStorage.setItem('testuser', res.data.data.sessionName);
                    // 路由到对应页面
                    return getRouteEach(to, next);
                } else if (to.path !== '/login') {
                    // 如果确认没有登录,则跳到登录页面
                    return next({path:'/login'});
                }
                next();
            })
            .catch(e => {
                console.error('查看sid失败:', e);
            })
        }
    }
    getRouteEach(to, next);
    
});

总结

  • node异步请求,check一下用户是否真的已经登录。
  • 通过 axios的全局监听返回状态,拦截401,并路由到登录页。
  • 通过vue的路由守卫,优化用户体验。

安全性

大家都知道,很长一段时间内,大家都用这种方式来处理,但是这里有个问题,就是在防csrf攻击(跨站请求伪造)的时候,就容易防不住了。当然可以用比如双cookie认证等方式加强,但是下一篇我要讲的token方式更为方便。<<从单页应用看node的token>> 欢迎大家继续关注~~