导读:本专栏主要分享同学们在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,
)