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里还是会有安全问题

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

相关推荐
Lee川3 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
七八星天4 小时前
C#代码设计与设计模式
后端
Ticnix4 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人4 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
砍材农夫4 小时前
threadlocal
后端
twl4 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人4 小时前
vue3使用jsx语法详解
前端·vue.js
神奇小汤圆4 小时前
告别手写HTTP请求!Spring Feign 调用原理深度拆解:从源码到实战,一篇搞懂
后端
天蓝色的鱼鱼4 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端