[zzerX@blog ~ ]:

vue实现完整登录 记住密码(token + localStorage)

之前一直在写界面UI相关,才发现自己几乎没有完整写过登录实现方面的东西,浏览器与服务器交互,保存用户登录信息等。于是就来写一个练习项目恶补一下缺失的知识。

大致流程

upload successful

登录界面/登录后页面

先简单写一下登录窗口UI部分,包括输入用户名,密码,和记住密码(😅救命 我还是喜欢写UI)

pages/login.vue
登录窗口UI

pages/index.vue
登录后页面

项目目录

upload successful
pages/index.vue为登录后显示的界面

前端输入限制

  • 弱口令检查
  • 空口令检查
  • 登录防抖

登录的实现

前端请求服务器响应后获得token,此时可以将token保存至cookie或者localStorage,个人理解的区别在于存在cookie可以随着请求自动发送,并且每次都会发送,而存在localStorage可以选择性的发送,有些页面并不需要验证令牌,相比之下更灵活,其次就是cookie会产生跨域问题。

这里我选择了存在localStorage的方式。

/**  点击登录 */
function onLoginClick(userName, password, isRemeber) {
  // console.log("father read:", userName, password, isRemeber);
  /** axios 请求登录
   *  用户名:zzerx 密码:a123456
   *  */
  axios
    .post("/login", {
      username: userName,
      pswmd5: md5(password),
    })
    .then((response) => {
      console.log(response.data);
      if (response.data.msg == "登录成功") {
        //保存token到localStorage
        window.localStorage.setItem("token", response.data.token);
        //跳转到指定路由
        router.push({ path: "/index" });
      } else {
        sendMsg.value = {
          state: "登陆失败",
          loginButtonDisable: false,
          loginLoading: false,
        };
      }
    })
    .catch((error) => {
      console.log(error);
    });
}

/index界面的保存登录态

在将token保存在localStorage后,访问其他页面时附带token到服务器.

...
axios
  .get("/index", {
    headers: {
      //Bearer token
      Authorization: "Bearer " + token,
    },
  })
  .then((response) => {
    console.log("success:", response.data);
    switch (response.data.state) {
      //设置token过期或无效时删除localStorage中的token
      case -1:
        router.push({ path: "/login" });
        window.localStorage.removeItem(token);
        break;
      case 0:
        router.push({ path: "/login" });
        window.localStorage.removeItem(token);
        break;
      case 1:
        userName.value = response.data.userInfo.username;
        perview.value = response.data.userInfo.preview;
        avatar.value = response.data.userInfo.avatar;
        iat.value = response.data.payload.iat;
        exp.value = response.data.payload.exp;
        break;
      default:
        console.log(response.data);
    }
  })
  .catch((err) => console.log(err));


  /**退出登录 */
  function outLogin(){
      window.localStorage.removeItem(token);
      router.push({ path: "/login" });
  }

把token放到专门用于验证的头Authorization, 其中 Bearer + 空格 + token 的方式遵照了Auth2.0规范,响应的response.data.state是在服务器自定义的状态信息。

nodejs后台简单接收和处理

后端使用nodejs,用到express jsonwebtoken。jsonwebtoken签名数据使用.sign方法,验证token使用.verify方法. 前端把用户名和md5密码发送到服务器进行校验,成功则返回用户信息和token。

这里的jwtKey应是一串使用非对称加密生成的密钥(项目里暂时是随便写的)。
expiresIn 设置token过期时间

.../login

        //使用jwt签名数据
        jwt.sign({ username },     //给客户端的token数据
            jwtKey,                //密钥
            { expiresIn: '90s' },  //过期时间
            (err, token) => {      //回调
                //返回token和用户数据
                res.json({
                    msg: "登录成功", userInfo: {
                        username,
                        preview: database.preview,
                        avatar: database.avatar
                    },
                    token
                });
  ...

  .../index
app.get('/index',(req,res) => {
    const headers = req.headers;
    const token = getAuthorzation(headers).token;
    //验证token
    jwt.verify(token, jwtKey, (err, payload) => {
        let state = 0;
        if(err){
            let errMsg = '验证失败:';
            if(err.name == 'TokenExpiredError'){//token过期
                errMsg += 'token过期'
                state = -1;
            }else if(err.name == 'JsonWebTokenError'){//无效的token
                errMsg += '无效的token';
                state = 0;
            }
            console.log(errMsg, state);
            res.json({msg: errMsg, state, payload});
        }else{
            state = 1;
            res.json({msg:"验证成功", state, payload, userInfo: {
                username: database.username,
                preview: database.preview,
                avatar: database.avatar
            }});
        }
    })
})

app.listen(3888, 'localhost', () => {
    console.log("服务器已启动![localhost:3888]...")
})

/** 获取authorization */
function getAuthorzation(headers){
   let authorzation = headers['authorization'] || '';
   authorzation = authorzation.split(' ');
//    console.log(authorzation)
   if(Array.isArray(authorzation) && authorzation.length == 2){
      return {
        type: authorzation[0],
        token: authorzation[1]
      }
   }else{
      return {
        type: null,
        token: null
      }
   }
}

非登录页面的请求服务器则拿到浏览器放在authorization的token,使用verify进行验证,并把结果返回给浏览器,浏览器根据响应结果做出相应操作。

可能产生的安全问题

  • 跨站脚本攻击(XSS)
  • 跨站请求伪造(CSRF)
  • 点击劫持(ClickJacking)
  • HTTP严格传输安全(HSTS)

参考文章, 有一些内容自己还未能理解。

练习项目地址

https://github.com/0x7A7A6572/vue-login

过程中参考的文章/视频

其他

虽然自己在实现的时候查找了大量的资料,但是后面写完再回来理清思路写博客时候却有点不知所措,简单的功能固然不难,但要完善则涉及了很多的内容,除了考虑安全方面,还有一些token续签,用户修改密码等等的情况没有考虑进去,要在短时间消化不太现实,我把这里当作一个这个方面的开始,相信在之后的实践里面能有过呢更多的理解。

— Jul 15, 2022