【XIAOJUSURVEY&北大】Authorization实现 - server源码阅读分析

导读:本专栏主要分享同学们在XIAOJUSURVEY&北大开源实践课程的学习成果。

详细介绍请查看:【XIAOJUSURVEY& 北大】2024 滴滴开源 XIAOJUSURVEY 北大软微开源实践课

作者:colmon46

Authentication交互流程图

源码分析

1、登录

src/modules/auth/controllers/auth.controller.ts

登录controller实现:接受用户request,处理未注册、用户名密码不一致等错误,并调用JWT生成服务。

typescript 复制代码
@Post('/login')
  @HttpCode(200)
  async login(
    @Body()
    userInfo: {
      username: string;
      password: string;
      captchaId: string;
      captcha: string;
    },
  ) {
    const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
      captcha: userInfo.captcha,
      id: userInfo.captchaId,
    }); // 比较输入的验证码和数据库中与id对应的验证码是否一致(小写)

    if (!isCorrect) {
      throw new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT);
    }

    const username = await this.userService.getUserByUsername(
      userInfo.username,
    ); 
    /* 根据username检查数据库中是否存在当前用户。在数据库中执行的是:
        const user = await this.userRepository.findOne({
          where: {
            username: username,
            'curStatus.status': {
              $ne: RECORD_STATUS.REMOVED,
            },
          },
        });
              特别地,需要排除状态为REMOVED的用户。在删除时,数据项的状态会被设置为REMOVED。
    */
    if (!username) {
      throw new HttpException(
        '账号未注册,请进行注册',
        EXCEPTION_CODE.USER_NOT_EXISTS,
      );
    }

    const user = await this.userService.getUser({
      username: userInfo.username,
      password: userInfo.password,
    }); // 对比用户名和密码是否对应。在数据库中,密码使用hash256加密。
    if (user === null) {
      throw new HttpException(
        '用户名或密码错误',
        EXCEPTION_CODE.USER_PASSWORD_WRONG,
      );
    }
    let token;
    try {
      token = await this.authService.generateToken(
        {
          username: user.username,
          _id: user._id.toString(),
        }, // 根据用户名和id生成JWT
        {
          secret: this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),  // 服务端定义的JWT密钥
          expiresIn: this.configService.get<string>(
            'XIAOJU_SURVEY_JWT_EXPIRES_IN',  // JWT过期时间。超过这个时间后,用户的登录失效。
          ),
        },
      );
      // 验证过的验证码要删掉,防止被别人保存重复调用
      this.captchaService.deleteCaptcha(userInfo.captchaId);
    } catch (error) {
      throw new Error(
        'generateToken erro:' +
          error.message +
          this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET') +
          this.configService.get<string>('XIAOJU_SURVEY_JWT_EXPIRES_IN'),
      );
    }

    return {
      code: 200,
      data: {
        token,
        username: user.username,
      },
    };
  }

2、JWT

src/modules/auth/services/auth/service.ts

负责JWT生成和验证。

JWT(JSON Web Token)是一种用于在不同服务之间安全传输信息的开放标准。服务器完成认证后,生成一个JWT传回给用户,在后续的通信中,用户一律需要携带JWT作为其身份验证,服务端无需保存任何其他的session信息,只需验证JWT是否有效即可完成鉴权。

JWT的组成为:

  • header(头部)

    alg:表示签名的算法。默认是HS256。

    typ:JWT的类型统一为JWT

  • payload(负载)

    iss:签发人

    exp:过期时间

    sub:主题

    aud:受众

    nbf:生效时间

    iat:签发时间

    jti:编号

    以上是可以选用的官方字段。负载部分还可以定义私有字段。

  • signature(签名)

    为了防止用户篡改数据,服务器会通过指定一个密钥(secret),使用header中指定的签名算法,根据header+payload+secret生成签名。

这三个部分通过Base64URL算法转为字符串之后,用.作为分割,拼接为一个完整的字符串。可以注意到,虽然设置了签名防止数据篡改,但是JWT本身没有加密过程,因此不能在负载中放置隐私信息。

使用jsonwebtoken,可以简单地完成JWT的生成和验证:

typescript 复制代码
3async generateToken(
    { _id, username }: { _id: string; username: string },
    { secret, expiresIn }: { secret: string; expiresIn: string },
  ) {
    return sign({ _id, username }, secret, {
      expiresIn, 
    });
  }
    /* 生成JWT
    export function sign(
        payload: string | Buffer | object,
        secretOrPrivateKey: Secret,
        options?: SignOptions,
    ): string;
    */
  async verifyToken(token: string) {
    let decoded;
    try {
      decoded = verify(
        token,
        this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
      );
    } catch (err) {
      throw new Error('用户凭证错误');
    } // 根据JWT验证token。如果超过设定的过期时间,验证无法通过。
    const user = await this.userService.getUserByUsername(decoded.username);
    if (!user) {
      throw new Error('用户不存在');
    } // 解码token负载中的username,确定用户没有注销账户,但无需再次验证密码。
    return user;
  }

3、guards

src/guards/authentication.guard.ts

Guard是Nestjs中的一种拦截器机制,用于在进入controller之前执行某些逻辑,可以用于身份验证、授权、数据校验等场景。在设定Guard时,需要完成一个实现了 CanActivate 接口的类,该接口输入请求的上下文信息,判断请求是否可以继续。在Authentication Guard中,Guard用于确定用户身份有效。

typescript 复制代码
@Injectable()
export class Authentication implements CanActivate {
  constructor(private readonly authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest(); // 获取http请求
    const token = request.headers.authorization?.split(' ')[1]; 
        /* 请求头部为:
                headers: {'Authorization': `Bearer ${token}`}
           空格分割得到JWT。
        */
    if (!token) {
      throw new AuthenticationException('请登录');
    }  // 处理没有JWT的情况
      
    try {
      const user = await this.authService.verifyToken(token); // 验证JWT,获取user
      request.user = user;
      return true;
    } catch (error) {
      throw new AuthenticationException(error?.message || '用户凭证错误');
    }
  }
}

src/modules/survey/controllers/survey.controller.ts

以获取问卷内容的controller为例:使用@UseGuards(Authentication)装饰器,在controller级别执行用户鉴权。在实际执行获取问卷之前,以用户请求作为上下文,先执行Authentication中的canActivate方法,如果用户登陆状态无效,在Guard中就会抛出错误,无法进行下一步操作,以保证数据安全。

less 复制代码
@Get('/getSurvey')
  @HttpCode(200)
  @UseGuards(SurveyGuard)
  @SetMetadata('surveyId', 'query.surveyId')
  @SetMetadata('surveyPermission', [
    SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
    SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
    SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
  ])
  @UseGuards(Authentication)
  async getSurvey(
    @Query()
    queryInfo: {
      surveyId: string;
    },
    @Request()
    req,
  )
相关推荐
无双_Joney9 小时前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
麻辣小蜗牛2 天前
以 NestJS 为原型看懂 Node.js 框架设计:Module
nestjs
Tans53 天前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Tans56 天前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
irving同学462389 天前
TypeORM 列装饰器完整总结
前端·后端·nestjs
Tans510 天前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
Wang's Blog10 天前
Nestjs框架: 基于策略的权限控制(ACL)与数据权限设计
nestjs·rbac·acl
三十_10 天前
【NestJS】构建可复用的数据存储模块 - 动态模块
前端·后端·nestjs
凡小烦10 天前
LeakCanary源码解析
源码阅读·leakcanary
SuperheroMan8246610 天前
部署时报错:Type 'string' is not assignable to type 'never'(Prisma 关联字段问题)
nestjs