从单页应用看node的token(二)

972 阅读6分钟

上一篇中,讲了下cookie+session的方式check用户状态,但是处理CSRF(跨站请求伪造)上会麻烦一点。

既然说到了CSRF,那先稍微解释一下。

CSRF

csrf是一种攻击方法,通俗的就是说攻击者伪装成你进行行骗。 这种是如何做到的呢? 从原理上说,这种攻击方式不需要窃取到用户的cookie,而是直接使用用户的cookie去进行违法行为。

怎么做到的?

我模拟一下这种情况:

  • 有一个网站A,A中有个post或get请求,用于对视频进行点赞。客户端和服务端的认证是用cookie进行的(参照第一篇的cookie+session的方式)
  • 现在有个网站B,是攻击者设置的,可能是通过诱惑标题引你进去,抑或是通过通过xss方式往C网站植入了一个iframe,等等,无论哪种设置,都是当你进入相应的B或C页面时,就会自动去请求A网站那个点赞的接口。
  • 如果我们之前登陆过网站A,并且并没有登出,那么请求A网站那个接口时,浏览器就会用登陆A网站时创建的cookie,去请求(这是浏览器自身特性)。比如B网站写了一个<img src="a网站/点赞接口/path"/>
  • 这时,可就等于是你在点赞了。

这可能会造成一个很大的问题,比如,会让某个视频资源点赞数非常高而顺序排的非常靠前, 或者是将一个资源标记为举报,导致资源下架。

还可能会因为网站的不严谨造成更严重的资金损失的问题,等等等等。

到这里,大家可能会看到问题的严重性了。前端同学因为之前不需要关注这些问题,当转到node时,也很容易忽略这类安全问题。

如何防范

如果大家通过上面的例子知道了深层原因,那也就有了大概的防范思路。但是我们作为技术开发者,不能因为有这种问题而强制用户每次访问后都立即登出,这对用户是非常不友好的。

对了,那我们就不让居心不良的人使用cookie通过session让服务端确定用户。也就是说,让其即使在B网站进行A网站的接口请求,A网站服务端并匹配不上用户,导致登录不成功,那就没有问题了。

所以,token这种形式就特别适合了。

token

1. token 是什么

可以从网上搜一下,会看到很多的解释。我想通俗的说一下,就是在登录成功时,我们产生一段唯一的,不会轻易被解析的字符串,发送到客户端,客户端把这个字符串存起来,每次请求时把这段字符串带着,让服务端反解。

看到这里,是不是感觉跟cookie+session的形式差不多? 是的,从原理上来说,是差不多的,只是token我们一般不把它放在cookie中,会放在比如loaclstorage中,即便坏人想用csrf的方式搞破坏,但是,他拿不到token,所以也就没法让服务端认证为登录状态,他的阴谋也就无法得逞了。

2. 如何做?

第一,在node中生成好token

// 写一个中间件token-middleware.js
const setting = require('../../config/setting');
const verify = require('../../config/verify');

function tokenMiddleWare(req, res, next) {
    let token = req.headers[setting.token.header];
    if(token === undefined){
        return next();
    }else{
    	// 可以token校验并将校验结果保存至请求头中
        verify.getToken(token).then(data => {
            logger.info('校验的data是:::', data);
            req.data = data;
            return next();
        }).catch(err =>{
            logger.error('校验出现错误:', err);
            return next();
        })
    }
}
module.exports = tokenMiddleWare;
//setting.js
module.exports = {
    token: {
    	// token密钥
        signKey: 'test_key_@@',
        // 过期时间300s
        signTime:  300,
        // 请求头参数
        header: 'authorization',
        // 不用校验的路由
        unRoute: [
            {url: /\.(jpg|png|css|js)$/, methods: ['GET']}
        ]
    }
}

// verify.js
const jwt = require('jsonwebtoken');
const setting = require('./setting');

const verify = {
	// 设置token
    setToken(username, _id){
        return new Promise(resolve => {
            let token = jwt.sign(
            	// 存储数据,自定义
                {username, _id},
                // 密钥
                setting.token.signKey,
                {expiresIn: setting.token.signTime, algorithm: 'HS256'}
            );
            resolve(token);
        })
    },
    getToken(token){
        return new Promise((resolve, reject) => {
        	// 处理token字符串
            if(!token.split(' ').length){
                reject({error: 'The token value is empty'})
            }else{
            	// 解密token并返回数据
                let data = jwt.verify(token.split(' ')[1],setting.token.signKey)
                resolve(data)
            }
        })
    }
}

module.exports = verify;

当登录成功时,进行token的设置:

const verify = require('../../config/verify');

// loginInfo.username -> 登录名
// loginInfo.passwd -> 登录密码
verify.setToken(loginInfo.username, loginInfo.passwd).then(token => {
    // 生成token后,返回给客户端
    res.json({
        code: 0,
        mesg: 'success',
        token
    });
});

然后,需要把写的中间件和express-jwt应用在app.js(你的根文件)中

const expressJwt = require('express-jwt');

// 加载token中间件
app.use(tokenMiddleware);
// 验证token是否可用
app.use(expressJwt({
    secret: setting.token.signKey,
    algorithms: ['HS256'],
    credentialsRequired: false, // 允许无token请求
    requestProperty: 'auth' // 把解析的值放在req.auth上
})
.unless({
    //除了这个path,其他的URL都需要验证
    path: setting.token.unRoute
}));


注意,当使用express-jwt中间件时,需要一个兜底的中间件,来承接解析错误、token过期等结果。

如果出现错误,我们默认返回401,所以我们来设置一下。

app.use(function (err, req, res, next) {
    // 当验证token出现问题时,比如对不上,过期等情况,则返回401
    if (err.name === 'UnauthorizedError') {
      res.status(401).send(err.message);
    }
});

这样node这一层就处理好了。 注意: 上面自己写的那个中间件是自己简单写的express-jwt功能,所以用express-jwt, 可以不用我写的那个中间件。

第二,客户端处理(使用vue)

客户端,首先要做的,就是保存服务端返回的token,我把它保存到了loaclstorage上。 比如:

res.data.token && localStorage.setItem('authToken', res.data.token);

然后,需要处理每次前端向服务端的请求头:

// 同样,还是用axios
// 拦截前端要发出去的请求
axios.interceptors.request.use(config => {
    let token = localStorage.getItem('authToken');
    if (token) {
        config.headers['Authorization'] = 'Bearer ' + token;
    }
    return config;
},
error => {
    console.error('拦截request出现错误', error);
});

这样,每次前端的请求,都会带着这个Authorization头,node层拿到并且解析就可以了。

同时需要注意,如果token解析后,返回401,那么我们也需要承接,并且转到登录页面

axios.interceptors.response.use(response => {
     return response
    },
    error => {
        if(error.response.status === 401) {
            router.push('/login');
        }
    }
);

token需要注意的问题

要想token不被csrf利用,前提是别让攻击者通过xss获取到,所以,需要处理好xss攻击。

总结

  • 跟cookie+session的基本原理很相近,都是处理http协议无状态的情况
  • token通过设置header头的形式,避开cookie,防止登录相关的cookie被利用
  • 需要注意处理xss,xss是另外一个攻击方式,但如果被xss了,token也就有危险了
  • token跟cookie并不是谁替代谁的问题,而是在什么场景下用什么更为合适一些。

好了,关于登录的时候涉及到的点和要规避的坑就先写到这。

希望大家能有所收获,用1个多小时的看文章和实地开发测试,解决新手可能要3天才能研究透的问题。 如果大家喜欢,别忘了点个赞哈, 哈哈哈