2021SC@SDUSC
目录
一.引言
上一篇主要分析了Oauth授权的基础角色代码,这一篇分析中将重点分析Oauth授权。
这种授权认证的好处是OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。
二.代码分析
1.infrastructure基础类
在Authorization.schema.ts文件中通过从mongoose中导入Document并用AuthorizationDocument继承他,其中保存着用户的idempiresIn,createAt等属性,然后通过导入的SchemaFactory的createForClass()方法创建AuthorizationSchema并导出。
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { AuthorizationScope } from 'src/domain/models/authorization-scope';
@Schema({
timestamps: {
createdAt: true,
updatedAt: false,
},
})
export class AuthorizationDocument extends Document {
@Prop()
_id: string;
@Prop()
expiresIn: number;
@Prop()
createdAt: Date;
@Prop()
userId: string;
@Prop()
clientId: string;
@Prop({
type: [String],
})
scopes: AuthorizationScope[];
}
export const AuthorizationCollection = 'Authorization';
export const AuthorizationSchema = SchemaFactory.createForClass(
AuthorizationDocument,
);
MongoAuthorizationQuery类:在构造函数中定义一个AuthorizationDocument的model类型authModel,在异步方法findByCode先传入参数code,返回值是Promise,先调用authModel的findById()方法,得到一个Document类型的实例authDoc,先进行判断,如果返回的是空,则函数返回undefined,如果不为空,函数将返回authDoc中的所有属性数据。
export class MongoAuthorizationQuery implements AuthorizationQuery {
constructor(
@InjectModel(AuthorizationCollection)
private readonly authModel: Model<AuthorizationDocument>,
) {}
async findByCode(code: string): Promise<AuthorizationData> {
const authDoc = await this.authModel.findById(code);
if (!authDoc) {
return undefined;
}
return {
code,
expiresIn: authDoc.expiresIn,
createdAt: authDoc.createdAt,
userId: authDoc.userId,
clientId: authDoc.clientId,
scopes: authDoc.scopes,
};
}
}
授权存储库的实现:
MongoAuthorizationRepository类实现AuthorizationRepository接口。
nextCode()先用nanoid生成一个唯一识别码赋值给code,然后根据appConfig得到授权有效时间,最后利用两个属性创建授权码AuthorizationCode并返回。
idFromCode()给出AuthorizationCode得到AuthorizationId,其实就是从AuthorizationCode中抽取出code。
findById(),在异步方法先传入参数AuthorizationId,返回值是Promise,先调用authModel的findById()方法,得到一个Document类型的实例authDoc,先进行判断,如果返回的是空,则函数返回undefined,如果不为空,函数将authDoc转化成model类型返回。
save()方法将授权先转化为Document类型,然后再用authModel.updateOne()方法保存。
import { InjectModel } from '@nestjs/mongoose';
import { LeanDocument, Model } from 'mongoose';
import { nanoid } from 'nanoid';
import { AppConfig } from 'src/application/config/app.config';
import { Authorization } from 'src/domain/models/authorization';
import { AuthorizationCode } from 'src/domain/models/authorization-code';
import { AuthorizationId } from 'src/domain/models/authorization-id';
import { AuthorizationPayload } from 'src/domain/models/authorization-payload';
import { AuthorizationRepository } from 'src/domain/models/authorization-repository';
import {
AuthorizationCollection,
AuthorizationDocument,
} from './authorization.schema';
export class MongoAuthorizationRepository implements AuthorizationRepository {
constructor(
private appConfig: AppConfig,
@InjectModel(AuthorizationCollection)
private readonly authModel: Model<AuthorizationDocument>,
) {}
nextCode(): AuthorizationCode {
const code = nanoid(32);
const expiresIn = this.appConfig.authCodeExpiresIn;
return new AuthorizationCode(code, expiresIn, new Date());
}
idFromCode(code: AuthorizationCode): AuthorizationId {
return new AuthorizationId(code);
}
async findById(id: AuthorizationId): Promise<Authorization | undefined> {
const authDoc = await this.authModel.findById(id.toString());
if (!authDoc) {
return undefined;
}
return this.documentToModel(authDoc);
}
async save(authorization: Authorization): Promise<void> {
const authDoc = this.modelToDocument(authorization);
await this.authModel.updateOne(
{
_id: authDoc._id,
},
authDoc,
{
upsert: true,
},
);
}
}
2.创建授权
首先CreateAuthorizationCommand类包括userId,clientId,scopes三个属性。
export class CreateAuthorizationCommand implements ICommand {
constructor(
public readonly userId: string,
public readonly clientId: string,
public readonly scopes: AuthorizationScope[],
) {}
}
创建授权CreateAuthorizationHanlder类,在构造函数中注入AuthorizationRepository类型成员,用 @AuthorizationRepositoryImplement()注解AuthorizationRepository,然后execute方法先传入CreateAuthorizationCommand参数,其中包含userId,clientId,scopes,然后调用repository.nextCode()方法得到新的授权码再用传入的参数command创建一个AuthorizationPayload,用得到的code和payload创建授权authorization,然后调用repository.save()方法来保存授权,返回授权码。
@CommandHandler(CreateAuthorizationCommand)
export class CreateAuthorizationHanlder
implements ICommandHandler<CreateAuthorizationCommand>
{
constructor(
@AuthorizationRepositoryImplement()
private readonly repository: AuthorizationRepository,
) {}
async execute(
command: CreateAuthorizationCommand,
): Promise<AuthorizationCode> {
const code = this.repository.nextCode();
const payload = new AuthorizationPayload(
command.userId,
command.clientId,
command.scopes,
);
const authorization = new Authorization(code, payload);
await this.repository.save(authorization);
return code;
}
}
3.授权控制
通过 AuthorizationPayloadDto得到相关授权信息,然后执行CreateAuthorizationCommand方法创建授权,返回授权码。
@Controller()
export class AuthorizationController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@OAuthService()
async createAuthorization(
dto: AuthorizationPayloadDto,
): Promise<AuthorizationCodeDto> {
const code: AuthorizationCode = await this.commandBus.execute(
new CreateAuthorizationCommand(dto.userId, dto.clientId, dto.scopes),
);
return {
code: code.toString(),
};
}
通过授权码寻找授权信息,先执行FindAuthorizationByCodeQuery方法得到data,如果data为空,抛出异常,然后通过data.code, data.expiresIn, data.createdAt三个属性来创建AuthorizationCode对象,然后验证authcode是否合法,如果不合法抛出异常,合法则将data分解为code和payload两部分返回。
async findAuthorizationByCode(
dto: AuthorizationCodeDto,
): Promise<AuthorizationDto> {
const data: AuthorizationData = await this.queryBus.execute(
new FindAuthorizationByCodeQuery(dto.code),
);
if (!data) {
throw new AuthorizationCodeNotFoundException();
}
const authCode = new AuthorizationCode(
data.code,
data.expiresIn,
data.createdAt,
);
if (!authCode.valid) {
throw new AuthorizationCodeExpiredException();
}
return {
code: data.code,
payload: {
userId: data.userId,
clientId: data.clientId,
scopes: data.scopes,
},
};
}
4.令牌控制
登记令牌,先在构造器中注入commandBus和queryBus,在 signToken函数中注入AuthorizationCodeDto参数,然后调用this.commandBus.execute函数传入AuthorizationId(dto.code),得到SignTokenRes类型的res,返回res。
@Controller()
export class TokenController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@OAuthService()
async signToken(dto: AuthorizationCodeDto): Promise<SignTokenRes> {
const res: SignTokenRes = await this.commandBus.execute(
new SignTokenCommand(new AuthorizationId(dto.code)),
);
return res;
}
刷新令牌是传入 RefreshAccessTokenDto得到SignTokenRes然后返回res,取得访问令牌载体是是传入 AccessTokenDto得到AccessTokenPayload然后返回payload。
@OAuthService()
async refreshAccessToken(dto: RefreshAccessTokenDto): Promise<SignTokenRes> {
const res: SignTokenRes = await this.commandBus.execute(
new RefreshAccessTokenCommand(dto.refreshToken),
);
return res;
}
@OAuthService()
async getAccessTokenPayload(
dto: AccessTokenDto,
): Promise<AccessTokenPayload> {
const payload: AccessTokenPayload = await this.queryBus.execute(
new GetAccessTokenPayloadQuery(dto.accessToken),
);
return payload;
}
三.总结
本次分析先分析了基础类,实现了创建AuthorizationSchema并导出,实现了授权存储库的四个方法,之后又实现了创建授权,通过授权码寻找授权信息,登记令牌,刷新令牌,之前在基础类中就实现了对令牌的有效时间的判断,登记令牌是为了用令牌去身份认证获得服务,登记令牌需要授码,授权码需要授权获得,授权后要登记在授权存储库,需要在一段时间内刷新令牌,不然授权会失效。项目使用的框架是nestjs框架,编程语言是TypeScript。