nest access_token、refresh_token实现用户登录及无感刷新

思路
  • 用户登录 返回给前端 ACCESS_TOKEN(1h)、REFRESH_TOKEN(7d)、和 ACCESS_TOKEN 的失效时间(1h)
  • 其他的接口请求时 携带 ACCESS_TOKEN
  • Axios处理:前端根据 ACCESS_TOKEN 的失效时间判断过期或者即将过期, 或者接口返回用户校验失败401的错误,前端向后端发送刷新 REFRESH_TOKEN 的接口刷新 ACCESS_TOKEN、REFRESH_TOKEN
  • 后端nestjs:authGuard拦截接口,判断是否需要accessToken校验, 如果accessToken校验失败,返回401,token失效,前端接收401错误后发送refresh的接口刷新 ACCESS_TOKEN、REFRESH_TOKEN
  • nestjs refresh接口: 接收请求后 判断REFRESH_TOKEN是否存在
    • 不存在直接返回401错误
    • 若REFRESH_TOKEN,判断和redis中存储的REFRESH_TOKEN是否一致,不一致返回401
    • 解析REFRESH_TOKEN,若解析token失败,返回401
    • 获取用户id,调用userService获取用户信息
    • 重新生成ACCESS_TOKEN、REFRESH_TOKEN,并结合用户信息返回给客户端
axios 响应拦截实现接口无感刷新

401错误根据后台封装情况,是直接接口报错返回status:401 或者 接口请求成功 在data再返回code: 401

customAxios.interceptors.response.use(
  (response) => {
    const { data, config, status } = response
    const { code } = data
    const { expiresOut } = getCurrentUser()
    const flag = expiresOut - Date.now() / 1000 < 60 * 10 // 判断过期时间距离accessToken过期时间10分钟之内
    const pathFlag = !noTokenList.includes(location.pathname) // 不需要token的接口列表

    if (pathFlag && (flag || code === 401 || status === 401)) { // status === 401 为多余项 非200的会被catch拦截
      if (!isRefreshing) {
        dealRefresh()
      }
      const retryOriginalRequest = new Promise((resolve) => {
        addSubscriber(() => {
          // 将当前接口放到重新请求的队列中
          resolve(customAxios(config))
        })
      })
      return retryOriginalRequest
    }
    if (location.pathname !== '/user/login' && code === 401) {
      // token失效
      // location.replace('/user/login')
    }
    return response.data
  },
  (error) => {
    const { response } = error
    const { status, data, config } = response
    if (
      location.pathname !== '/user/login' &&
      (+status === 401 || +data.code === 401)
    ) {
      // token失效
      if (!isRefreshing) {
        dealRefresh()
      }

      const retryOriginalRequest = new Promise((resolve) => {
        addSubscriber(() => {
          resolve(customAxios(config))
        })
      })
      return retryOriginalRequest
    }
    // location.replace('/user/login')
    return Promise.reject(error)
  }
)
// 是否正在刷新的标记
export let isRefreshing = false
//重试队列
export let requests: any[] = []
// 发布订阅  执行请求池中的接口
export function onAccessTokenFetched() {
  requests.forEach((callback: any) => {
    callback && callback()
  })
  requests = [] // 清空重试队列
}
// 发布订阅  将未完成的接口放入请求池
export function addSubscriber(callback: any) {
  requests.push(callback)
}
export const refreshAxios = axios.create({}) // 新建一个axios实例

// 刷新token的处理
export const dealRefresh = async () => {
  isRefreshing = true // 正在刷新token
  const url = `/api/sign/refresh?refreshToken=${getRefresh()}` // 自行处理 token刷新的接口路径
  refreshAxios
    .get(url)
    .then(async (refresData) => {
      const data = refresData.data.data
      // 更新用户信息 处理localStorage的token信息
      const updateUser = JSON.parse(localStorage.getItem('currentUser') || '{}')
      updateUser.expiresOut = data.expiresOut
      updateUser.accessToken = data.accessToken
      await localStorage.setItem('token', data.accessToken)
      await localStorage.setItem('refresh', data.refreshToken)
      await localStorage.setItem('currentUser', JSON.stringify(updateUser))
      isRefreshing = false

      onAccessTokenFetched() // 处理重试队列
    })
    .catch((err) => {
      const { response } = err
      const { data } = response
      const { code } = data
	
      if (+code === 401) { // refreshToken过期或者无效 跳转登录页面
        // 401 跳登录页
        const { pathname } = location
        pathname !== '/user/login' && location.replace('/user/login')
      }
    })
}
nestjs 处理
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { decrypt } from '@src/utils/secret';
import { verify } from 'jsonwebtoken';
import { REDIS_DB_0 } from '@src/modules/redis/redis';
import { Reflector } from '@nestjs/core';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();

    const noAuth = this.reflector.get<boolean>('no-auth', context.getHandler());
    if (noAuth) {
      return true;
    }

    const accessToken =
      context.switchToRpc().getData().headers['access-token'] ||
      request.headers['access-token'] ||
      request.body.token ||
      request.query.token ||
      request.params.token;

    if (accessToken) {
      let userInfo, access_token;
      try {
        access_token = decrypt(accessToken);
        userInfo = verify(access_token, process.env.TOKEN_SECRET);
      } catch {
        throw new UnauthorizedException('token失效');
      }

      const REDIS_REFRESH_TOKEN = await REDIS_DB_0.get(userInfo.id);
      if (accessToken !== REDIS_REFRESH_TOKEN) {
      	// 判断接收的accessToken是否和redis保存的一致
        throw new UnauthorizedException('token失效');
      }

      try {
        const decodeAccessToken: any = verify(
          access_token,
          process.env.TOKEN_SECRET,
        );
        // 解析access_token信息并存放到request.headers中
        request.headers.user = decodeAccessToken;
      } catch (err) {
        const { expiredAt } = err;
        throw new UnauthorizedException(
          `token已于${new Date(+expiredAt).toLocaleString()}失效`,
        );
      }
    } else {
      throw new UnauthorizedException('token失效');
    }

    return true;
  }
}
import {
  Body,
  Controller,
  Get,
  Post,
  Query,
  Request,
  UnauthorizedException,
} from '@nestjs/common';
import { decrypt, encrypt } from '@src/utils/secret';
import { UserDto } from '../user/user.dto';
import { LoginService } from './login.service';
import { LoginRes } from './types';
import { verify, sign, JwtPayload } from 'jsonwebtoken';
import { REDIS_DB_0, REDIS_DB_1 } from '../redis/redis';
import { UserService } from '../user/user.service';
import { instanceToPlain } from 'class-transformer';
import { NoAuth } from '@src/utils/tools';

interface JwtParams extends JwtPayload {
  id?: string;
  type?: string;
}

@Controller('sign')
export class LoginController {
  constructor(
    private readonly loginService: LoginService,
    private readonly userService: UserService,
  ) {}

  @NoAuth()
  @Post('login')
  async login(@Body() userDto: UserDto): Promise<LoginRes> {
    const user = await this.userService.findUser(userDto);
    if (!user) {
      throw new BadRequestException(`用户${userDto.name}未注册~`);
    }

    const isPwdCorrect = checkPassword(userDto.password, user.password);
    if (!isPwdCorrect) {
      throw new BadRequestException(`账号密码输入错误, 请修改后重试~`);
    }

    const userPlain = instanceToPlain(user);
    const accessToken = sign(userPlain, process.env.TOKEN_SECRET, {
      expiresIn: 60 * 60,
    });
    const refreshToken = sign(
      { type: 'refresh', id: userPlain.id },
      process.env.TOKEN_SECRET,
      {
        expiresIn: '7d',
      },
    );

    const nToken = encrypt(accessToken);
    const nRefreshToken = encrypt(refreshToken);

    await REDIS_DB_0.set(userPlain.id, nToken);
    await REDIS_DB_1.set(userPlain.id, nRefreshToken);

    return {
      ...user,
      expiresOut: ~~((Date.now() + 60 * 60 * 1000) / 1000),
      accessToken: nToken,
      refreshToken: nRefreshToken,
    };
  }

  @NoAuth()
  @Get('refresh')
  async refresh(
    @Request() request,
    @Query('refreshToken') refreshToken: string,
  ): Promise<any> {
    if (refreshToken) {
      try {
      	// 解密token
        const refresh_token = await decrypt(refreshToken);
 	 	// 处理jwt信息
        const decodeRefreshToken: string | JwtParams = await verify(
          refresh_token,
          process.env.TOKEN_SECRET,
        );

        const { id } = decodeRefreshToken as JwtParams;

        const REDIS_REFRESH_TOKEN = await REDIS_DB_1.get(id); // 获取redis中储存的refreshToken

        if (refreshToken !== REDIS_REFRESH_TOKEN) {
       	  // 判断接收的refreshToken是否和redis保存的一致
          throw new UnauthorizedException();
        }
        // 获取用户信息
        const userInfo = await this.userService.findUserById({ id });
		// 生成新的ACCESS_TOKEN、REFRESH_TOKEN
        const newAccessToken = sign(
          instanceToPlain(userInfo),
          process.env.TOKEN_SECRET,
          {
            expiresIn: 60 * 60,
          },
        );

        const newRefreshToken = sign(
          { type: 'refresh', id: userInfo.id },
          process.env.TOKEN_SECRET,
          {
            expiresIn: '7d',
          },
        );

        const nToken = encrypt(newAccessToken);
        const nRefreshToken = encrypt(newRefreshToken);

        await REDIS_DB_0.set(userInfo.id, nToken);
        await REDIS_DB_1.set(userInfo.id, nRefreshToken);

        return {
          ...userInfo,
          expiresOut: ~~((Date.now() + 60 * 60 * 1000) / 1000),
          accessToken: nToken,
          refreshToken: nRefreshToken,
        };
      } catch (err) {
        throw new UnauthorizedException();
      }
    } else {
      throw new UnauthorizedException();
    }
  }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值