React+Nest实现无感登录

目录

前言

(一)介绍

(二)具体实现

1.后端登录成功发送token

2.前端接收token存储

3.前端携带access_token发请求

4.后端配置守卫

5.后端刷新token逻辑

6.前端响应拦截器实现刷新

(三)总结


前言

给自己的项目做登录功能,正好丰富一下经验,决定用一下无感登录

我大概参考了这位大佬的文章:https://www.cnblogs.com/sunyan97/p/17887134.html


(一)介绍

无感登录,就是无感刷新token

登录成功后,后端发送两个token给前端:access_tokenrefresh_token

access_token有效时间很短,一般为30min,refresh_token有效时间较长,大概是7days

  1. access_token用于带到请求头进行权限请求
  2. 当后端检测到过期时,通知前端acess_token已过期(401)
  3. 前端携带refresh_token发起刷新请求,后端根据refresh_token发送新的access_token
  4. 如果refresh_token也过期,就返回401给前端,通知前端退出登录

为什么不采用前端定时器定时发起刷新请求?

定时刷新对浏览器性能消耗过大;用到了再刷新,将刷新决定权交给后端,节省资源;

如何存储?

access_token可以存储在storage里,refresh_token尽量存储的保密一点,比如httpOnly-token等(没试过这个

(二)具体实现

前端采用react、后端采用nest,代码可能有部分删减,并不完整

1.后端登录成功发送token

使用jwt签发token

pnpm add @nestjs/jwt

在auth.controller.ts:

javascript 复制代码
// 生成 token
  async login(user: any) {
    // 临时token
    const access_token = this.jwtService.sign({
        email: user.email,
        sub: user.userId,
      },{
        expiresIn: '1m',
      },
    );
    // 刷新token
    const refresh_token = this.jwtService.sign({
        sub: user.userId,
      },{
        expiresIn: '10m',
      },
    );
    // 无感登录
    return {
      access_token,
      refresh_token,
    };
  }

2.前端接收token存储

这里用到了ahooks的useRequest发起请求

为了简单省事,access_token采用localStorage存储,refresh_token采用cookie存储

pnpm add ahooks js-cookie

javascript 复制代码
const navigate = useNavigate();
// 发起注册请求
const { run:signUpRun,loading:signUpLoading } = useRequest(
    (params)=>{
        return register(params)
    },
    {
        manual:true,
        onSuccess(res:any,params:any) {
            toast({
                title: "登录成功!",
                duration: 1000,
            })
            // 保存token
            saveToken(res.data)
    
            // 跳转到主页
            navigate(`/dashboard`, { replace: false })
        },
        onError(err:any) {} 
    }
});

const saveToken = ({ access_token, refresh_token }: { access_token: string; refresh_token: string }) => {
    localStorage.setItem('access_token', access_token);
    Cookies.set('refresh_token', refresh_token);
};

3.前端携带access_token发请求

在service配置页:

javascript 复制代码
// 请求拦截
this.instance.interceptors.request.use(
    config => {
        // 携带access_token
        const access_token =localStorage.getItem('access_token');
        if (access_token) {
            config.headers.Authorization = `Bearer ${access_token}`;
        }
        return config;
    },
    err => {
        return Promise.reject(err);
    }
);

4.后端配置守卫

配置guard自动检测传来的请求头内的access_token是否过期,过期自动返回401

在guard/login.guard.ts:

javascript 复制代码
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Observable } from 'rxjs';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class LoginGuard implements CanActivate {
  @Inject(JwtService)
  private jwtService: JwtService;
  
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest();

    const authorization = request.headers.authorization;
    if (!authorization) {
      throw new UnauthorizedException('用户未登录');
    }

    try {
      console.log('authorization', authorization);
      
      const token = authorization.split(' ')[1];
      const data = this.jwtService.verify(token);
      console.log('data', data);
      
      return true;
    } catch (e) {
      throw new UnauthorizedException('token失效,请重新登录');
    }
  }
}

写一个接口使用一下guard

在user.controller.ts:

javascript 复制代码
@UseGuards(LoginGuard)
  @Get('getUserByEmail')
  async getUserByEmail(@Query('email') email: string) {
    const {password,...result} =  await this.userService.findByEmail(email);
    return result
  }

当access_token有效的时候,可以直接返回数据,如果失效就返回401

下面开始实现 **"无感刷新"**的功能

5.后端刷新token逻辑

用jwtService.verify验证refresh_token是否过期,过期返回401,没过期返回新的access_token

在auth.controller.ts:

javascript 复制代码
  @Post('refresh')
  async refresh(@Body('refresh_token') refreshToken: string) {
    try {
      const data = this.jwtService.verify(refreshToken);

      const user = await this.userService.findById(data.sub);
      // 重新签发access_token
      const access_token = this.jwtService.sign(
        { sub: user.id,email: user.email, },
        { expiresIn: '30m' },
      );

      return {
        access_token,
      };
    } catch (error) {
      throw new UnauthorizedException('token已失效,请重新登录');
    }
  }

6.前端响应拦截器实现刷新

在axios的响应拦截器里实现刷新

原因如下:

请求响应后判断请求路径是否为需带权限请求路径,并判断状态码,满足要求就发起自主发起刷新请求,可以实现无感刷新;

如果不在响应拦截器里设置刷新请求,那么每次写一个带权限的请求,都需要在处理函数里增加判断权限的操作,非常复杂;而在响应器里设置,只需要配置相关路径(需要前后端规范

javascript 复制代码
// 响应拦截
this.instance.interceptors.response.use(
    (response) => response.data,
    async (err) => {
        let { data, config } = err.response;
            if (data.statusCode === 401 && config.url.includes("/user/getUserByEmail")) {
                const res:any = await refresh({ refresh_token: getRefreshToken() });             
                if (res.statusCode === 200) {
                    saveAccessToken(res.data.access_token);
                    // 重新发起请求
                    return this.instance(config);
                } else {
                    alert("登录过期,请重新登录");
                    removeToken()
                    window.location.href = '/auth/login'
                    return Promise.reject(res.data);
                }
              } else {
                return Promise.reject(err);
              }
            }
        );

测试测试:

无感登录!欧了!


(三)总结

无感登录大概就是这样,不过我没有对token进行加密,直接存在cookie里还是会有安全问题

用户权限的路由守卫还没写,后面补上,挥挥~

相关推荐
mCell12 小时前
GSAP ScrollTrigger 详解
前端·javascript·动效
gnip12 小时前
Node.js 子进程:child_process
前端·javascript
你的人类朋友13 小时前
说说签名与验签
后端
databook13 小时前
Manim实现脉冲闪烁特效
后端·python·动效
excel15 小时前
为什么在 Three.js 中平面能产生“起伏效果”?
前端
excel16 小时前
Node.js 断言与测试框架示例对比
前端
canonical_entropy17 小时前
AI时代,我们还需要低代码吗?—— 一场关于模型、演化与软件未来的深度问答
后端·低代码·aigc
天蓝色的鱼鱼17 小时前
前端开发者的组件设计之痛:为什么我的组件总是难以维护?
前端·react.js
codingandsleeping17 小时前
使用orval自动拉取swagger文档并生成ts接口
前端·javascript
颜如玉18 小时前
HikariCP:Dead code elimination优化
后端·性能优化·源码