写点什么

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

作者:XIAOJUSURVEY
  • 2024-07-05
    浙江
  • 本文字数:3155 字

    阅读完需:约 10 分钟

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

Authentication 交互流程图

源码分析

1、登录

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

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

@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 的生成和验证:

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 用于确定用户身份有效。

@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 中就会抛出错误,无法进行下一步操作,以保证数据安全。

@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,  )
复制代码


发布于: 刚刚阅读数: 5
用户头像

XIAOJUSURVEY

关注

@滴滴 github.com/didi/xiaoju-survey 2024-05-20 加入

「快速」打造「专属」问卷系统, 让调研「更轻松」

评论

发布
暂无评论
【XIAOJUSURVEY&北大】Authorization实现 - server源码阅读分析_鉴权_XIAOJUSURVEY_InfoQ写作社区