nestjs-实现双token无感刷新

前言

平时使用中,是否有过这样的场景,我们经常使用的 app 很久很久都不会让我们重新登录,但如果我们一个礼拜没上线,就需要重新上线了,这就是本篇文章文章要提到的 双token无感刷新

看了后,也会介绍跟单 token 的对比,会感觉,还是双token给力哈

本篇会使用 nestjs + react(umi-request) 做一个示范,让大家体会一下双 token 的魅力

双token无感刷新

双 token 就是两个 token,当用户登陆的时候,服务端会给客户端两个 token,一个token一般期限为 0.5小时(个人也是感觉太短,自己根据情况,可以适当延长1小时1天都行,越短安全性越高)

token就是令牌,带着token相当于我们拿着身份证似的,后端可以直接办理事情,客户端使用最短的 token 用来与后台交互长的token 作为 更新token的时候使用,这样半个小时后,客户端的短token过期时,就用长 token 获取新的 两个token,然后客户端拿着新的 短token 重发原来请求即可

这样单token也可以,为什么要双token?

第一个,单token等过期了就没办法续了,不过期客户端就得保存一个时间戳,在过期之前更换新的 token,但很可能也会出现类似的情况,假设期限7天,你设置5天期限更新,但第六、七天用户没更新,那么第八天就需要重新登录,用户感觉两天没上线就要重新登陆,期限短点也行,但仍然出现另外一个问题,如果你这个 token 被别人劫持拿到了,那么就可以肆意挥霍好几天了,安全性就出现问题了,如果是双 token 呢,一般短的,持续时间为 30s,别人即使拿到了,也会很快到期,还得重新想办法拿新的,这就无形之间增加了成本,双token的优势就出现了,并且无需前端设置时间校验,只需要出现 401 的时候,重新调用接口,重发请求即可,也方便后台调整过期时间

nestjs 代码

除了登录返回两个 token,刷新也是返回两个 token

话不多说直接上代码

js 复制代码
@ApiOperation({
    summary: '登陆'
})
@Public() // @UseGuards(UserGuard) @Guards()
@APIResponse(TokenDto)
@Post('login')
login(
    @Body() loginInfo: LoginDto,
) {
    return this.authService.login(loginInfo);
}

@ApiOperation({
    summary: '刷新'
})
@APIResponse(TokenDto)
@Post('refresh_token')
refreshToken(
    @ReqUser() user: User,
) {
    return this.authService.refreshToken(user);
}

generateToken(
    user: User
) {
    return {
        access_token: this.jwtService.sign({
            id: user.id
        }, {
            expiresIn: '0.003h' //1小时过期
        }), //将token返回给客户端
        refresh_token: this.jwtService.sign({
            id: user.id,
        }, {
            expiresIn: '7d' //7天过期
        })
    }
}

async login(
    loginInfo: LoginDto,
) {
    let user = await this.userRepository.findOne({
        where: {
            account: loginInfo.account,
        },
    });
    if (!user || user.password !== loginInfo.password) {
        return ResponseData.fail('用户名或者密码不正确')
    }
    return ResponseData.ok({
        user,
        ...this.generateToken(user)
    })
}

async refreshToken(
    user: User,
) {
    //获取新token
    return this.generateToken(user)
}

这样我们的后台就写好了,就等前端来测试了

前端代码

使用 umi-request 先顶一个一个全局 request 把,实际使用一般用代理,prefix一般不写,或者只写接口前缀

js 复制代码
export const request = extend({
    prefix: "http://localhost:3000/api",
    timeout: 15000,
    requestType: 'form',
});

再写个登录和刷新的接口

js 复制代码
export async function loginUser(account: string, password: string) {
    let res = await request.post('/user/login', {
        data: {
            account,
            password
        }
    })
    if (Math.floor(res.status / 200) === 2) {
        localStorage.setItem('access_token', res.data.access_token)
        localStorage.setItem('refresh_token', res.data.refresh_token)
    }
    return res
}

export async function refreshToken() {
    let res = await request.post('/user/refresh_token', {
        headers: {
            'token': localStorage.getItem('refresh_token') || ''
        } || {},
    })
    localStorage.setItem('access_token', res.data?.access_token)
    localStorage.setItem('refresh_token', res.data?.refresh_token)
    return res
}

比那些我们的请求拦截器,当 header中不存在 token 时,我们加入 token 即可(登陆时加不加都不影响,不用可以加代码)

js 复制代码
//请求request请求数据拦截器,其发生在我们写的请求之后,实际发出请求之前
request.interceptors.request.use((url, options) => {
    //假设请求钱我们需要将一些令牌统一放到header中(根据实际需要处理)
    //将token和加密后的sign放到一起
    let headers: any = options.headers
    if (!headers.token) {
        headers['token'] = localStorage.getItem('access_token')
    }
    //设置完成后,要返回我们的的参数
    return {
        url,
        options: {
            ...options,
            headers
        }
    }
})

编写我们的响应拦截器,当 401 时,我们就拦截我们的响应,这时我们就开始使用长token 直接刷新token,为什么上面要加入判断不包含刷新接口呢,为了避免我们的 长token 也过期,那时再 401 就死循环了,或者后台故障也会死循环

这里会发现我们刷新了token后,然后直接调用了我们原来的接口(这样就可以无缝衔接了),只不过 umi-request不可以直接重发,我们就直接根据类型请求我们的接口就行了

这样就完了么,并没有,这样我们的请求的 401 的接口就真的 401 失败了,前面 两个await 就是为了能够让新token获取的新内容,覆盖掉初始调用接口的response(前提是我们的接口要设置 getResponse: true),这样就会额外返回一个 interceptors 中独有的 response了,然后我们直接返回新的 reponse,用户真的无感刷新了(ps:如果失败了,也只是正常失败,而不是401,除非两个token都过期才会401)

js 复制代码
request.interceptors.response.use(async (response, options) => {
    if (response.status === 401 && !options.url.includes('/refresh_token')) {
        //为什么await,因为这样,上一个401的请求就会被阻塞,我们此时直接重新refresh,并且重新请求
        let res = await refreshToken(options)
        //处理请求,没看到直接重新请求的类
        //这里code是我专门给接口准备的罢了,就是为了区分接口,实际上使用自己的成功码即可
        if (res.code === 1) {
            if (options.method === "GET") {
                let res = await request.get(options.url, {
                    params: options.params,
                    headers: {
                        ...options.headers,
                        token: ''
                    },
                    getResponse: true
                })
                return res.response
            } else if (options.method === "POST") {
                let res = await request.post(options.url, {
                    data: options.data,
                    headers: {
                        ...options.headers,
                        token: ''
                    },
                    getResponse: true,
                })
                return res.response
            }
        }
    }
    return response;
})

看看运行效果,虽然浏览器有 401 出现,但是接口数据正常返回了,就说厉害不厉害

最后

需要的话就用起来吧,稳得一批,如果是前端,这个长 token 就别七天了,最好一两天就过期,避免别人登陆自己的账号搞事情,哈哈😂

相关推荐
小李小李不讲道理2 小时前
「Ant Design 组件库探索」四:Input组件
前端·javascript·react.js
知识分享小能手9 小时前
React学习教程,从入门到精通,React AJAX 语法知识点与案例详解(18)
前端·javascript·vue.js·学习·react.js·ajax·vue3
NeverSettle_14 小时前
React工程实践面试题深度分析2025
javascript·react.js
学前端搞口饭吃14 小时前
react reducx的使用
前端·react.js·前端框架
努力往上爬de蜗牛14 小时前
react3面试题
javascript·react.js·面试
开心不就得了14 小时前
React 进阶
前端·javascript·react.js
谢尔登14 小时前
【React】React 哲学
前端·react.js·前端框架
学前端搞口饭吃17 小时前
react context如何使用
前端·javascript·react.js
GDAL17 小时前
为什么Cesium不使用vue或者react,而是 保留 Knockout
前端·vue.js·react.js
Dragon Wu1 天前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架