【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,
  )
相关推荐
_jiang2 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
web_code3 天前
webpack源码快速分析
前端·webpack·源码阅读
敲代码的彭于晏4 天前
【Nest.js 10】JWT+Redis实现登录互踢
前端·后端·nestjs
前端小王hs18 天前
Nest通用工具函数执行顺序
javascript·后端·nestjs
明远湖之鱼20 天前
从入门到入门学习NestJS
前端·后端·nestjs
吃葡萄不吐番茄皮22 天前
从零开始学 NestJS(一):为什么要学习 Nest
前端·nestjs
东方小月22 天前
Vue3+NestJS实现权限管理系统(六):接口按钮权限控制
前端·后端·nestjs
白雾茫茫丶25 天前
Nest.js 实战 (十四):如何获取客户端真实 IP
nginx·nestjs
zhuhit1 个月前
[FastDDS 源码解析(十三)发送第一条PDP消息---跨进程发送]
机器人·嵌入式·源码阅读
Spirited_Away1 个月前
Nest世界中的AOP
前端·node.js·nestjs