如何在Nestjs中刷新令牌。

23
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtPayload } from './model/jwt-payload.model';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'secretKey',
    });
  }

  async validate(payload: JwtPayload) {
    const user = await this.authService.validateUser(payload);
    if (!user) {
      throw new UnauthorizedException();
    }
    return true;
  }
}

PassportStrategy 会从请求中提取出 Token。我不知道如何捕获 Token 过期或无效时的错误。我的目的是,如果由于 Token 过期而导致错误,则需要刷新 Token。否则,执行其他操作。

2个回答

30

在自定义身份验证守卫的canActivate方法中处理刷新令牌的实现。

如果访问令牌过期,则将使用刷新令牌来获取新的访问令牌。 在此过程中,还会更新刷新令牌。

如果两个令牌都无效,则将清除cookie。

@Injectable()
export class CustomAuthGuard extends AuthGuard('jwt') {
  private logger = new Logger(CustomAuthGuard.name);

  constructor(
    private readonly authService: AuthService,
    private readonly userService: UserService,
  ) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();

    try {
      const accessToken = ExtractJwt.fromExtractors([cookieExtractor])(request);
      if (!accessToken)
        throw new UnauthorizedException('Access token is not set');

      const isValidAccessToken = this.authService.validateToken(accessToken);
      if (isValidAccessToken) return this.activate(context);

      const refreshToken = request.cookies[REFRESH_TOKEN_COOKIE_NAME];
      if (!refreshToken)
        throw new UnauthorizedException('Refresh token is not set');
      const isValidRefreshToken = this.authService.validateToken(refreshToken);
      if (!isValidRefreshToken)
        throw new UnauthorizedException('Refresh token is not valid');

      const user = await this.userService.getByRefreshToken(refreshToken);
      const {
        accessToken: newAccessToken,
        refreshToken: newRefreshToken,
      } = this.authService.createTokens(user.id);

      await this.userService.updateRefreshToken(user.id, newRefreshToken);

      request.cookies[ACCESS_TOKEN_COOKIE_NAME] = newAccessToken;
      request.cookies[REFRESH_TOKEN_COOKIE_NAME] = newRefreshToken;

      response.cookie(ACCESS_TOKEN_COOKIE_NAME, newAccessToken, COOKIE_OPTIONS);
      response.cookie(
        REFRESH_TOKEN_COOKIE_NAME,
        newRefreshToken,
        COOKIE_OPTIONS,
      );

      return this.activate(context);
    } catch (err) {
      this.logger.error(err.message);
      response.clearCookie(ACCESS_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
      response.clearCookie(REFRESH_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
      return false;
    }
  }

  async activate(context: ExecutionContext): Promise<boolean> {
    return super.canActivate(context) as Promise<boolean>;
  }

  handleRequest(err, user) {
    if (err || !user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}

将用户与请求连接是在JwtStrategy类中的validate方法中完成的,如果访问令牌有效,则会调用该方法。

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    readonly configService: ConfigService,
    private readonly userService: UserService,
  ) {
    super({
      jwtFromRequest: cookieExtractor,
      ignoreExpiration: false,
      secretOrKey: configService.get('jwt.secret'),
    });
  }

  async validate({ id }): Promise<User> {
    const user = await this.userService.get(id);
    if (!user) {
      throw new UnauthorizedException();
    }

    return user;
  }
}

自定义cookie提取器示例

export const cookieExtractor = (request: Request): string | null => {
  let token = null;
  if (request && request.signedCookies) {
    token = request.signedCookies[ACCESS_TOKEN_COOKIE_NAME];
  }
  return token;
};

这只是一个请求,不会将用户与请求绑定。 - glasspill
将用户附加到请求是在JwtStrategy类的validate方法中完成的,我为此添加了一个示例。 - Željko Šević
cookieExtractors来自哪里? - rony
cookieExtractor是一个自定义的提取器,示例已经更新。 - Željko Šević
使用此解决方案,您需要在每个请求中发送refreshToken。那么使用refreshToken的意义何在呢?目的正是为了避免太频繁地共享它。 - Paul Serre
@PaulSerre 如果RToken已经来自数据库而不是cookie,该怎么办? - alucard

16

您可以创建自己的身份验证并覆盖请求处理程序,而不是使用内置的AuthGuard

@Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {

  handleRequest(err, user, info: Error) {
    if (info instanceof TokenExpiredError) {
      // do stuff when token is expired
      console.log('token expired');
    }
    return user;
  }

}

根据您想要做什么,您还可以覆盖canActivate方法,该方法可以访问请求对象。请查看AuthGuard源代码


2
我不明白。使用这种方法,我应该如何将新的访问令牌传递给客户端?您能详细说明一下吗? - Albert
@Albert,这真的取决于您的具体要求,但也许这个帖子会有所帮助。 - Kim Kern
2
令牌过期错误从哪里产生? - rony

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接