2021SC@SDUSC
目录
一.引言
认证授权系统的一个重要的部分就是用户,用户部分需要完成的功能包括用户安全登入登出,用户创建,用户更新,用户upsert操作,从学生队列中寻找用户,根据Id寻找用户。接下来让我们分析相关代码。
二.代码分析
1.用户环境相关配置
导出类 UserConfigSchema ,类中使用@IsEnum装饰器来判断属性NODE_ENV是否为Enum,判断MONGO_URI是否为String类型等等。在 UserConfig类的构造器中注入UserConfigSchema类型参数,使用@ENV装饰器,将变量env中相关属性赋值给类的私有属性。
import { IsEnum, IsString, IsUrl } from 'class-validator';
import { NodeEnvironment } from './node-environment.enum';
import { Env } from '@sdu-turing/config';
export class UserConfigSchema {
@IsEnum(NodeEnvironment)
NODE_ENV: NodeEnvironment;
@IsString()
MONGO_URI: string;
@IsString()
TURING_APP_ID: string;
@IsUrl()
TURING_LOGIN_URL: string;
}
export class UserConfig {
mongoUri: string;
turingAppId: string;
turingLoginUrl: string;
nodeEnv: NodeEnvironment;
constructor(@Env() env: UserConfigSchema) {
this.mongoUri = env.MONGO_URI;
this.turingAppId = env.TURING_APP_ID;
this.turingLoginUrl = env.TURING_LOGIN_URL;
}
}
2.用户登录
用户登录命令类结构
export class SignInCommand {
constructor(
public readonly userId: UserId,
public readonly password: string,
) {}
}
在构造器中注入用户存储库repository,异步方法execute中将SignInCommand注入,然后通过用户存储库repository的findById方法来根据command中userId来查找user,如果用户Id不存在于用户存储库中,抛出无法找到用户异常,找到用户的话,再调用得到的用户user的validatePassword方法,密码错误抛出异常,正确则调用用户的SignIn方法,然后在用户存储库中保存用户repository.save(user),最后提交用户。
@CommandHandler(SignInCommand)
export class SignInHandler implements ICommandHandler<SignInCommand> {
constructor(
@UserRepositoryImplement()
private readonly repository: UserRepository,
) {}
async execute(command: SignInCommand): Promise<void> {
const { userId, password } = command;
const user = await this.repository.findById(userId);
if (!user) {
throw new UserNotFoundException();
}
if (!user.validatePassword(password)) {
throw new GrpcException(status.INVALID_ARGUMENT, '用户密码错误');
}
user.signIn();
await this.repository.save(user);
user.commit();
}
}
3.用户登出
用户登出只需用户Id即可
export class SignOutCommand {
constructor(public readonly userId: UserId) {}}
在构造器中注入用户存储库repository,异步方法execute中将SignOutCommand注入,然后通过用户存储库repository的findById方法来根据command中userId来查找user,如果用户Id不存在于用户存储库中,抛出无法找到用户异常,找到用户的话,将用户登出,然后保存并提交用户即可。
@CommandHandler(SignOutCommand)
export class SignOutHandler implements ICommandHandler<SignOutCommand> {
constructor(
@UserRepositoryImplement()
private readonly repository: UserRepository,
) {}
async execute(command: SignOutCommand): Promise<void> {
const user = await this.repository.findById(command.userId);
if (!user) {
throw new UserNotFoundException();
}
user.signOut();
await this.repository.save(user);
user.commit();
}
}
4.更新用户命令类结构
更新用户的命令类需要用户Id和需要更新的用户参数。
export class UpsertUserCommand {
constructor(
public readonly userId: UserId,
public readonly changes: UpdateUserParams,
) {}
}
在构造器中注入用户存储库repository,异步方法execute中将SignOutCommand注入UpsertUserCommand,然后通过用户存储库repository的findById方法来根据command中userId来查找user,如果用户不存在,用更新的数据创建用户并且返回,如果用户存在,用需要更新的数据更新用户然后save函数保存用户并提交用户。
@CommandHandler(UpsertUserCommand)
export class UpsertUserHandler implements ICommandHandler<UpsertUserCommand> {
constructor(
@UserRepositoryImplement()
private readonly repository: UserRepository,
) {}
async execute(command: UpsertUserCommand): Promise<void> {
const user = await this.repository.findById(command.userId);
if (!user) {
// 用户不存在,创建数据
return this.createUser({
...command.changes,
casId: command.userId,
status: UserStatus.SignedOut,
updatedAt: new Date(),
});
}
// 用户存在,更新数据
user.updateData(command.changes);
await this.repository.save(user);
user.commit();
}
5.用户存储库
用@Injectable()装饰器的类MongoUserRepository实现了UserRepository接口,构造器中注入userModel,用户存储库的方法包括findById根据Id寻找用户,根据传入的UserId,通过userModel的findById方法得到userDoc实例,判断是否存在userDoc,不存在返回undefined,存在则返回Model类型的userDoc。保存用户的方法save将传入的user转化为Document类型用uerModel.updateOne方法保存在用户存储器中。
@Injectable()
export class MongoUserRepository implements UserRepository {
constructor(
@InjectModel(UserCollection)
private readonly userModel: Model<UserDocument>,
) {}
async findById(userId: UserId): Promise<User | undefined> {
const userDoc = await this.userModel.findById(userId.toString());
if (!userDoc) {
return undefined;
}
return this.documentToModel(userDoc);
}
async save(user: User): Promise<void> {
const userDoc = this.modelToDocument(user);
await this.userModel.updateOne(
{
_id: userDoc._id,
},
userDoc,
{
upsert: true,
},
);
}
6.通过Id查询用户
构造器中注入用户查询UserQuery类变量,在异步函数execute中传入参数FindUserByIdQuery类型的变量query,通过userQuery的findById函数,以用户Id作为参数得到用户data返回data。
@QueryHandler(FindUserByIdQuery)
export class FindUserByIdHandler implements IQueryHandler<FindUserByIdQuery> {
constructor(@UserQueryImplement() private readonly userQuery: UserQuery) {}
async execute(query: FindUserByIdQuery): Promise<UserData> {
const data = await this.userQuery.findById(query.casId);
return data;
}
7.学生中查询用户
构造器中注入用户查询UserQuery类变量,在异步函数execute中传入参数FindUserFromSduQuery类型的变量query,通过userQuery的findFromSdu函数,以用户Id和password作为参数得到用户user并返回。
@QueryHandler(FindUserFromSduQuery)
export class FindUserFromSduHandler
implements IQueryHandler<FindUserFromSduQuery>
{
constructor(@UserQueryImplement() private readonly userQuery: UserQuery) {}
async execute(query: FindUserFromSduQuery): Promise<UserData> {
const user = await this.userQuery.findFromSdu(query.casId, query.password);
return user;
}
}
8.确定用户身份
向异步函数findFromSdu传入Id和password,将userConfig中Url和appId赋值给相关变量,调用this.httpService.get函数,传入loginUrl和参数体,参数体中包括Id,password,设备类型,软件Id,得到对象再调用toPromise(),然后验证得到的data是不是success,如果不是就报错'学工号或密码错误',然后根据data.extraInfo.type判断是不是'xs',role就是学生,不是就是教师,然后返回包括各种属性casId,name,role,grade,workplace,campus, major,type的用户数据。
async findFromSdu(casId: string, password: string): Promise<UserData> {
const loginUrl = this.userConfig.turingLoginUrl;
const appId = this.userConfig.turingAppId;
const data = await this.httpService
.get(loginUrl, {
params: {
userId: casId,
passWord: password,
deviceType: 'android',
appId: appId,
},
})
.toPromise()
.then(
(resp) => resp.data,
() => {
throw new TuringServiceException();
},
);
if (data.result !== 'success') {
throw new GrpcException(status.INVALID_ARGUMENT, '学工号或密码错误');
}
const userJson = data.extraInfo.info;
const role =
data.extraInfo.type === 'xs' ? UserRole.Student : UserRole.Teacher;
const name = data.user?.userName || '未知';
return {
casId,
name,
role,
grade: parseInt(userJson.sznj || '1901', 10),
workplace: userJson.dwmc || '未知',
campus: userJson.xqmc || '未知',
major: userJson.zymc || '未知',
type: userJson.xslx,
};
}
三.总结
分析了这么久的图灵认证授权,在SDU信息门户的user中终于使用上了,完成了用户安全登入登出,用户创建,用户更新,用户upsert操作,从学生队列中寻找用户,根据Id寻找用户,具体的实现是通过UserCommand中命令类和UserQueries中查询类来实现的。该子功能所使用的框架是nestjs,还涉及到了mongoose的Schema模块以及Grpc的远程函数调用的使用。通过分析,更加深入了解了nestjs和Typescript的相关知识。