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 就别七天了,最好一两天就过期,避免别人登陆自己的账号搞事情,哈哈😂

相关推荐
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
老码沉思录1 小时前
React Native 全栈开发实战班 - 第四部分:用户界面进阶之动画效果实现
react native·react.js·ui
奔跑草-8 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
林太白14 小时前
❤React-React 组件通讯
前端·javascript·react.js
豆华15 小时前
React 中 为什么多个 JSX 标签需要被一个父元素包裹?
前端·react.js·前端框架
前端熊猫15 小时前
React第一个项目
前端·javascript·react.js
练习两年半的工程师15 小时前
使用React和Vite构建一个AirBnb Experiences克隆网站
前端·react.js·前端框架
林太白15 小时前
❤React-JSX语法认识和使用
前端·react.js·前端框架
女生也可以敲代码15 小时前
react中如何在一张图片上加一个灰色蒙层,并添加事件?
前端·react.js·前端框架