React+Nest实现无感登录

目录

前言

(一)介绍

(二)具体实现

1.后端登录成功发送token

2.前端接收token存储

3.前端携带access_token发请求

 4.后端配置守卫

5.后端刷新token逻辑

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

(三)总结


前言

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

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


(一)介绍

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

登录成功后,后端发送两个token给前端:access_token 和 refresh_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: 

// 生成 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

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配置页: 

// 请求拦截
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:

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:

@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: 

  @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的响应拦截器里实现刷新

原因如下:

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

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

// 响应拦截
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里还是会有安全问题

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

React和Node.js结合可以创建一个全栈JavaScript应用,其中无感刷新通常是指通过服务器端渲染(Server-Side Rendering, SSR)配合前端的客户端路由管理来实现实时更新而无需页面完全重载。以下是一个简单的步骤概述: 1. **设置基础架构**: - 使用Express作为Node.js的web服务器框架。 - 初始化项目并安装必要的依赖,如`express`, `react`, `react-dom`, 和 `ReactDOMServer`。 2. **服务器端渲染**: - 创建一个Node.js API,例如`/api/posts`,当请求这个路由时,返回预渲染的HTML。 - 使用`ReactDOMServer.renderToString`将React组件转化为纯HTML字符串。 ```javascript app.get('/api/posts', async (req, res) => { const data = await fetchData(); // 获取数据 const html = ReactDOMServer.renderToString(<YourComponent data={data} />); res.send(html); }); ``` 3. **客户端处理**: - 在`index.html`中引入React库,并使用客户端路由库(如`react-router-dom`)。 - 当用户导航到新的URL时,从服务器获取初始渲染的数据,然后使用它来初始化React组件。 4. **状态管理和生命周期**: - 如果需要实时更新,可以使用React的`useEffect` Hook监听路由变化,当路由改变时重新渲染对应的组件。 5. **服务端事件推送**: - 可以考虑使用WebSocket或者长轮询等技术,由服务器向客户端推送新数据。当有更新发生时,通知客户端并仅更新相关部分。 6. **实现局部刷新**: - 使用React的`dangerouslySetInnerHTML`属性,在接收到服务器推送的新数据后,只更新DOM中的部分内容,而不是整个页面。 ```javascript function App() { useEffect(() => { // 接收服务器推送的数据更新 const handleUpdate = (newData) => { setPosts(newData); // 更新组件状态 }; // ...其他连接和接收更新的逻辑 }, []); return ( <div> {posts.map((post) => ( // 渲染每个帖子 ))} </div> ); } // 在接收到服务器更新时,调用handleUpdate函数 handleUpdate(dataFromServer); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值