前后端 token 的使用

嗷呜, 预感又是一篇长的水文, 但是内心好激动哦

适用于前端(了解 node express 框架的人看), 想要了解后端的人看

好了, 开始废话模式

前后端 token 的使用

最近在做一个后台管理项目, 但是我一个卑微的前端我去哪里找接口的, 没有, 只好我自己写

哎, 我能有什么坏心思呢, 没有

1. 了解 JWT

利用token进行用户身份验证的流程:

  • 客户端使用用户名和密码请求登录
  • 服务端收到请求,验证用户名和密码
  • 验证成功后,服务端会签发一个token,再把这个token返回给客户端
  • 客户端收到token后可以把它存储起来,比如放到cookie中
  • 客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带
  • 服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据

*支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题

无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力

JWT就是上述流程当中token的一种具体实现方式,其全称是JSON Web Token,官网地址:https://jwt.io/

通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。

JWT 的验证证流程:

  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
  • 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串
  • 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
  • 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
  • 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
  • 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

用案例来了解, 唔, 我把 app.js 拆分成了下面的几个部分, 代码的顺序没有改变, 依次看就可以

/*
 * Start at 2022.4.30
 * Author: ximingx.
 * Github: https://github.com/ximingx
 * Csdn: https://ximingx.blog.youkuaiyun.com/
 */

// 导入 express 模块
// 创建 express 的服务器实例
const express = require('express')
const app = express()
const port = process.env.PORT || 2022;
// 跨域设置
const cors = require('cors')
app.use(cors())
// req.body 解析
app.use(express.json());
app.use(express.urlencoded({extended: false}));

1.1 安装并导入

首先需要我们去安装依赖

> yarn add jsonwebtoken express-jwt

然后导入

// 1. 安装并导入 JWT 相关的两个包, express-jwt版本推荐6.1.1, 最近他更新了, 可能会出现一些意外的问题
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

1.2 定义你自己的 secret 密钥

// 2. 定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'ximingx';

1.3 JWT字符串解析

// 5. JWT字符串解析
// 用于接收到客户端传来的请求, 验证是否含有 token
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 `req.user` 属性上
app.use(expressJWT({secret: secretKey, algorithms: ['HS256']}).unless({path: ['/login', '/register']}))

1.4 登陆后生成 token

// 登录的路由, 在这里登录成功后, 返回 token 字符串
app.post('/login', (req, res) => {
    // 哎呀,这里就是一个简单的登录测试, 没搞的很复杂
    if (!(req.body.username == 'admin' && req.body.password == "root")) {
        return res.json({
            code: 1,
            msg: '用户名或密码错误'
        })
    }
    // 3. 用户登陆成功后, 需要生成token
    // 参数1:用户的信息对象
    // 参数2:加密的秘钥
    // 参数3:配置对象,可以配置当前 token 的有效期
    const tokenStr = jwt.sign({
        username: req.body.username
    }, secretKey, {expiresIn: '1h'})
    // 返回登陆成功后返回数据
    res.json({
        status: 200,
        message: '登录成功!',
        // 4. 并通过 token 属性发送给客户端
        token: tokenStr,
    })
})

1.5 验证

// 这里需要这样请求, 否则 token 会无效报错
// axios({
//     method: 'post',
//     url: 'http://localhost:2022/adimin',
//     headers: {
//         'Authorization': 'Bearer ' + token
//     }
// })

// 可以分别用带 token 和不带 token 验证一下

// 这是一个有权限的 API 接口
app.get('/admin', function (req, res) {
    // 6. 使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
    console.log(req.user)
    // {
    //     "status": 200,
    //     "message": "获取用户信息成功!",
    //     "data": {
    //         "username": "admin",
    //         "iat": 1651242605, // 创建时间
    //         "exp": 1651246205 // token 有效期
    //     }
    // }
    res.send({
        status: 200,
        message: '获取用户信息成功!',
        data: req.user, // 要发送给客户端的用户信息
    })
})

这是不带 token 的请求

image-20220430114311749

当我们在登陆后, 获取 token

image-20220430114734717

再次进行测试, 带上 token 就可以啦

image-20220430115107833

1.6 捕获 token 过期异常

// 7:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
    // 这次错误是由 token 解析失败导致的
    if (err.name === 'UnauthorizedError') {
        return res.send({
            status: 401,
            message: '无效的token',
        })
    }
    res.send({
        status: 500,
        message: '未知的错误',
    })
})
// 端口号监听, 不重要
app.listen(port, () => {
    console.log(`Server is running at http://localhost:${port}`);
});

2. 搭建一个简单的后台服务

于是我用了 nodeexpress 框架简单的做了一个服务器

下面是 app.js 文件, 看完上面的 jwt 应该就可以很好地理解了

/*
 * Start at 2022.
 * Author: ximingx.
 * Github: https://github.com/ximingx
 * Csdn: https://ximingx.blog.youkuaiyun.com/
 */
// 导入 express 模块
// 创建 express 的服务器实例
const express = require('express')
const app = express()
const port = process.env.PORT || 3000;
// 跨域设置
const cors = require('cors')
app.use(cors())
// req.body 解析
app.use(express.json());
app.use(express.urlencoded({extended: false}));
// 加载数据库
require('./db/index');
// 5. JWT字符串解析
const expressJWT = require('express-jwt')
// 2. 定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'ximingx';
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 `req.user` 属性上
app.use(expressJWT({secret: secretKey, algorithms: ['HS256']}).unless({path: ['/api/ximingx/v1/login']}));
// 自定义模块
const {writeLog} = require('./log/log');
// 路由
app.use('/api/ximingx/v1', require('./router/index'));
// 全局捕获错误
app.use((err, req, res, next) => {
    writeLog(err);
    // 这次错误是由 token 解析失败导致的
    if (err.name === 'UnauthorizedError') {
        return res.send({
            status: 401,
            message: '无效的token',
        })
    }
    return res.status(500).send('未知的错误!');
});
// 监听端口
app.listen(3000, () => {
    console.log(`server is running at http://localhost:${port}`);
});

这是我用于登录验证的路由

// 除了 /api/ximingx/v1/login 其他的路由都需要 token
app.use(expressJWT({secret: secretKey, algorithms: ['HS256']}).unless({path: ['/api/ximingx/v1/login']}));

app.use('/api/ximingx/v1', require('./router/index'));

router.js

const express = require('express');
const router = express.Router();
// 1. 安装并导入 JWT 相关的两个包, express-jwt版本推荐6.1.1, 最近他更新了, 可能会出现一些意外的问题
const jwt = require('jsonwebtoken')
// 2. 定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'ximingx';
// 自定义模块
const {UserLogin, findUser, createUser} = require('../db/user')
const {LoginRegex} = require('../utils/regex')
const {writeLog} = require('../log/log')

router.post('/login', (req, res) => {
    if (!LoginRegex(req.body)) {
        writeLog("Login 参数不支持")
        return res.json({
            title: "register",
            code: 400,
            data: {
                info: "ximingx",
                message: "参数不支持",
            }
        })
    }

    UserLogin(req.body).then(user => {
        if (user == null) {
            return res.json({
                title: "login",
                code: 400,
                data: {
                    info: "ximingx",
                    message: "用户名或者密码错误"
                }
            })
        }
        // 3. 用户登陆成功后, 需要生成token
        // 参数1:用户的信息对象
        // 参数2:加密的秘钥
        // 参数3:配置对象,可以配置当前 token 的有效期
        const tokenStr = jwt.sign({
            username: req.body.username
        }, secretKey, {expiresIn: '1h'})
        res.json({
            title: "login",
            code: 200,
            data: {
                com: "ximingx",
                message: req.body,
                // 4. 并通过 token 属性发送给客户端
                token: tokenStr,
            }
        })
    }).catch(err => {
        writeLog(err)
        return res.json({
            title: "login",
            code: 400,
            dara: {
                info: "ximingx",
                message: "用户名或者密码错误",
            }
        })
    })
});

3. 前端登录页面

image-20220430113258699

这里我们进行请求

axios({
url: ‘http://localhost:3000/api/ximingx/v1/login’,
method: ‘POST’,
data: {
username: this.username,
password: this.password,
}
})

在登录成功后进行存储

localStorage.setItem(‘token’, res.data.data.token);

inputInfo: function () {
      let password = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,18}$/;
      let username = /^[a-zA-Z0-9]{3,12}$/;
      if (!(username.test(this.username) && password.test(this.password))) {
        return ElNotification.error('用户名或者密码不合法!');
      }
      axios({
        url: 'http://localhost:3000/api/ximingx/v1/login',
        method: 'POST',
        data: {
          username: this.username,
          password: this.password,
        }
      }).then(res => {
        if (res.data.code == 200) {
          localStorage.setItem('token', res.data.data.token);
          ElNotification({
            title: 'Success',
            message: '登录成功 请稍等...',
            type: 'success',
          });
          this.$router.push({
            path: '/home',
          })
        } else {
          ElNotification.error({
            title: '用户名或者密码错误',
            message: `请重新输入`,
          })
        }
      }).catch(res => {
        ElNotification({
          title: 'Error',
          message: '用户名或者密码错误',
          type: 'error',
        });
      })
    },

4. 导航守卫

但是呢, 即使是没有 token, 我们在 url 里输入别的页面仍然会跳转

这里就需要我们在前端 router.js 里配置, 判读有无 token, 使别人不能随便的进行别的有权限的页面的访问

// to 从哪一个路径来, from 到哪一个路径去, next 是否继续执行, 表示放行
router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    return next()
  } else {
    const token = localStorage.getItem('token')
    if (!token) {
      return next('/login')
    }
    return next()
  }
})

但是还是有问题, 别人可以自己加 token 啊

image-20220430113920401

然后我们就可以快乐的跳转了

image-20220430114018625

所以呢, 最后还应该给导航守卫还应该添加验证

### 前后端 Token 使用案例 #### ThinkPHP5 API Token 身份验证功能示例 在ThinkPHP5框架中,API的身份验证可以通过Token机制实现。服务器接收到请求时会先校验Token的有效性和合法性,只有合法有效的Token才能继续访问受保护资源[^1]。 ```php // 验证Token中间件 public function handle($request, \Closure $next) { // 获取HTTP头中的Authorization字段获取token $request->header('authorization', ''); if (empty($authHeader)) { return json(['msg' => '未提供Token'], 401); } list($type, $token) = explode(' ', $authHeader . ' '); if ($type !== 'Bearer') { return json(['msg' => '非法Token格式'], 401); } try { // 解析并验证Token有效性 JWT::parseAndValidate($token); } catch (\Exception $e) { return json(['msg' => '无效或已过期的Token'], 401); } return $next($request); } ``` 此代码片段展示了如何创建一个简单的中间件来处理进入系统的每一个请求,并检查其携带的Token是否有效。如果一切正常,则允许请求继续;反之则返回错误响应给客户端。 #### 基于Token的身份认证特点 基于Token的身份认证具有无状态和良好的可扩展性的优点。这意味着服务端不需要存储任何Session数据就能完成用户的识别工作。另外,开发者可以在Token内嵌入一些额外的信息(比如用户ID),以便后续操作使用。当Token超出设定的时间范围后,它就会自动失效,这时就需要用户提供新的凭证来进行再次登录[^2]。 #### SA-Token 安全框架下的鉴权流程 对于更复杂的应用场景而言,除了基本的身份验证外还需要考虑权限控制问题。以SA-Token为例,在完成了初步的身份确认之后,系统可以根据当前账户所持有的角色或其他条件决定该用户能否执行某些敏感动作。例如删除文章、修改密码等都需要经过严格的权限审查过程[^3]。 #### JSON Web Tokens (JWT) 作为一种轻量级的标准方法,JWT非常适合用来做跨域的身份验证以及安全的数据传输。它的主要组成部分包括头部(Header),载荷(Payload), 和签名(Signature)[^4]。下面是一个典型的JWT字符串: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adqssw5c` 其中包含了加密后的算法名称、声明集以及其他自定义参数。为了确保安全性,整个消息会被发送方用私钥签署,接收者可以用对应的公钥解密读取内容的同时也能够验证其真实性。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秦书翔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值