2021SC@SDUSC
目录
一.引言
1.传统登录方式弊端
2.OAuth 系统设计简介
? ? ? OAuth 协议概述:
二.用户登录部分代码分析
1.proto
2.application
(1)commands
(2)图灵认证系统的环境配置
(3)queries
3.domain
? ?models
4.infrastructure
从mangoDB中查询用户信息
将用户信息存储在mangoDB
三.总结
一.引言
1.传统登录方式弊端
? ? ? ?在传统应用中,如果想要拿到用户信息,第三方应用往往通过用户名 username 与密码 password 直接向用户服务器获取,此种方式将导致用户数据不安全,山东大学官方系统自前年起也通过 CAS 系统解决了这一系列问题。
2.OAuth 系统设计简介
? ? ? ? ? OAuth 是一个开放协议标准,协议标准为 RFC 6749。
? ? ? OAuth 协议概述:
? ? ? ?OAuth 分为若干种授权方式,最主要的是授权码方式与刷新令牌方式。 ? ? ? ?用户访问第三方客户端应用后,第三方客户端请求用户授权,跳转至 OAuth 系统授权端点,用户同意授权后将获得授权码,用户将 OAuth 系统提供的授权码返回给第三方客户端应用,第三方应用凭借授权码去令牌端点换取获取令牌 AccessToken 或刷新令牌 Refresh Token。之后,第三方客户端应用拿到令牌,想要获取用户信息时通过 Access Token 获取即可。因为 用户信息较为敏感,所以 Access Token 失效时间十分短暂,因此需要通过 Refresh Token在系统中刷新 Access Token 以减少系统被攻击的可能性。
二.用户登录部分代码分析
1.proto
proto部分确定客户端服务相关方法:CreateClient(创建用户),FindById(ClientById) (根据用户ID寻找用户),ValidateClient(用户授权)
syntax = "proto3";
package turing.connect.client.v1;
message CreateClientReq {
string name = 1;
string logoUri = 2;
repeated string scopes = 3;
repeated string redirectUris = 4;
}
message CreateClientRsp {
string id = 1;
string secret = 2;
}
message ClientById {
string id = 1;
}
message ClientData {
string id = 1;
string name = 2;
string logoUri = 3;
repeated string scopes = 4;
repeated string redirectUris = 5;
}
message ValidateClientReq {
string id = 1;
string secret = 2;
}
message ValidateClientRsp {
bool valid = 1;
optional string error = 2;
}
// 客户端服务
service ClientService {
rpc CreateClient(CreateClientReq) returns (CreateClientRsp);
rpc FindById(ClientById) returns (ClientData);
rpc ValidateClient(ValidateClientReq) returns (ValidateClientRsp);
}
2.application
(1)commands
实现@nestjs/cqrs中的ICommand接口
创建用户结构体并export:结构体的属性是只读的,已经写死,防止被更改
import { ICommand } from '@nestjs/cqrs';
import { AuthScope } from 'src/domain/models/auth-scope';
export class CreateClientCommand implements ICommand {
constructor(
public readonly name: string,
public readonly logoUri: string,
public readonly scopes: AuthScope[],
public readonly redirectUris: string[],
) {}
}
返回前台送来的用户ID和用户密码创建用户并且异步操作
@CommandHandler(CreateClientCommand)
export class CreateClientHandler
implements ICommandHandler<CreateClientCommand>
{
constructor(
@ClientRepositoryImplement()
private readonly repository: ClientRepository,
) {}
async execute(command: CreateClientCommand): Promise<CreateClientResult> {
const id = this.repository.nextId();
const client = new Client({
id,
secret: '',
name: command.name,
logoUri: command.logoUri,
scopes: command.scopes,
redirectUris: command.redirectUris,
});
const secret = client.newSecret();
client.create();
await this.repository.save(client);
client.commit();
return {
id,
secret,
};
}
}
进行用户授权:
(1)先创建用户授权结构体再export
export class ValidateClientCommand implements ICommand {
constructor(public readonly id: ClientId, public readonly secret: string) {}
}
?(2)进行用户授权确认:
@CommandHandler(ValidateClientCommand)
export class ValidateClientHandler
implements ICommandHandler<ValidateClientCommand>
{
constructor(
@ClientRepositoryImplement()
private readonly repository: ClientRepository,
) {}
async execute(command: ValidateClientCommand): Promise<void> {
const client = await this.repository.findById(command.id);
if (!client) {
throw new ClientNotFoundException();
}
if (!client.validate(command.secret)) {
throw new GrpcException(status.INVALID_ARGUMENT, '客户端密钥错误');
}
return;
}
}
如果用户在库中找不到,抛出异常ClientNotFoundException
import { GrpcException } from '@sdu-turing/microservices';
import { status } from 'grpc';
export class ClientNotFoundException extends GrpcException {
constructor() {
super(status.NOT_FOUND, '客户端不存在');
}
}
如果用户密钥错误,抛出异常
import { GrpcException } from '@sdu-turing/microservices';
import { status } from 'grpc';
export class InvalidArgumentException extends GrpcException {
constructor() {
super(status.INVALID_ARGUMENT, '参数非法');
}
}
(2)图灵认证系统的环境配置
import { IsEnum, IsString, IsUrl } from 'class-validator';
import { Env } from '@sdu-turing/config';
export enum NodeEnvironment {
Development = 'development',
Production = 'production',
Test = 'test',
Provision = 'provision',
}
export class AppConfigSchema {
@IsEnum(NodeEnvironment)
NODE_ENV: NodeEnvironment;
@IsString()
MONGO_URI: string;
}
export class AppConfig {
mongoUri: string;
nodeEnv: NodeEnvironment;
constructor(@Env() env: AppConfigSchema) {
this.nodeEnv = env.NODE_ENV;
this.mongoUri = env.MONGO_URI;
}
}
(3)queries
用户数据接口
import { AuthScope } from 'src/domain/models/auth-scope';
export interface ClientData {
id: string;
name: string;
logoUri: string;
scopes: AuthScope[];
redirectUris: string[];
}
通过IDquery找到用户
import { IQuery } from '@nestjs/cqrs';
export class FindClientByIdQuery implements IQuery {
constructor(public readonly clientId: string) {}
}
?将{?IQueryHandler,?QueryHandler?} 通过?'@nestjs/cqrs'导出
通过this.clientQuery.findById返回clientData
nestjs/cqrs简介:CQRS的核心除了Command与Query的分离,还有Controller层与Handler层的解耦。以往的MVC架构中,Controller层会实例化Service,比如UserService,CommentService。Service实例提供了数据库操作逻辑。 这就是Controller与Service的紧耦合。 NestJS的CQRS框架通过QueryBus/CommandBus(.net的CQRS框架中称为Mediator)实现了Controller与事件处理服务的解耦。
@QueryHandler(FindClientByIdQuery)
export class FindClientByIdHandler
implements IQueryHandler<FindClientByIdQuery>
{
constructor(
@ClientQueryImplement()
private readonly clientQuery: ClientQuery,
) {}
async execute(query: FindClientByIdQuery): Promise<ClientData | undefined> {
const clientData = await this.clientQuery.findById(query.clientId);
return clientData;
}
}
3.domain
? ?models
用户属性的model
import { AuthScope } from './auth-scope';
import { ClientId } from './client-id';
export interface ClientProperties {
id: ClientId;
name: string;
secret: string;
logoUri: string;
scopes: AuthScope[];
redirectUris: string[];
}
整个用户的model,包括各种用户方法。
import { AuthScope } from './auth-scope';
import { ClientId } from './client-id';
import { ClientProperties } from './client-properties';
import * as SHA256 from 'sha256';
import { nanoid } from 'nanoid';
import { AggregateRoot } from '@nestjs/cqrs';
import { ClientCreatedEvent } from '../events/client-created.event';
export class Client extends AggregateRoot {
private _id: ClientId;
private _name: string;
private _secret: string;
private _logoUri: string;
private _scopes: AuthScope[];
private _redirectUris: string[];
constructor(props: ClientProperties) {
super();
this.assignProps(props);
}
get asProps(): ClientProperties {
return {
id: this._id,
name: this._name,
secret: this._secret,
logoUri: this._logoUri,
scopes: this._scopes,
redirectUris: this._redirectUris,
};
}
private assignProps(props: ClientProperties): this {
this._id = props.id;
this._name = props.name;
this._secret = props.secret;
this._logoUri = props.logoUri;
this._redirectUris = props.redirectUris;
this._scopes = props.scopes;
return this;
}
create() {
this.apply(new ClientCreatedEvent(this.asProps));
}
newSecret() {
const secret = nanoid(32);
this._secret = this.hashSecret(secret);
return secret;
}
validate(secret: string) {
return this._secret === this.hashSecret(secret);
}
private hashSecret(plain: string) {
return SHA256(plain);
}
}
4.infrastructure
从mangoDB中查询用户信息
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { ClientData } from 'src/application/queries/client-data.interface';
import { ClientQuery } from 'src/application/queries/client.query';
import { ClientCollection, ClientDocument } from './client.schema';
export class MongoClientQuery implements ClientQuery {
constructor(
@InjectModel(ClientCollection)
private readonly clientModel: Model<ClientDocument>,
) {}
async findById(id: string): Promise<ClientData | undefined> {
const client = await this.clientModel.findById(id);
if (!client) {
return undefined;
}
return {
id: client._id,
name: client.name,
logoUri: client.logoUri,
scopes: client.scopes,
redirectUris: client.redirectUris,
};
}
}
将用户信息存储在mangoDB
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { nanoid } from 'nanoid';
import { Client } from 'src/domain/models/client';
import { ClientId } from 'src/domain/models/client-id';
import { ClientRepository } from 'src/domain/models/client-repository';
import { ClientCollection, ClientDocument } from './client.schema';
export class MongoClientRepository implements ClientRepository {
constructor(
@InjectModel(ClientCollection)
private readonly clientModel: Model<ClientDocument>,
) {}
nextId(): ClientId {
return new ClientId(nanoid(24));
}
async findById(id: ClientId): Promise<Client | undefined> {
const clientDoc = await this.clientModel.findById(id.toString());
if (!clientDoc) {
return undefined;
}
return this.documentToModel(clientDoc);
}
async save(client: Client): Promise<void> {
const clientDoc = this.modelToDocument(client);
await this.clientModel.updateOne(
{
_id: clientDoc._id,
},
clientDoc,
{
upsert: true,
},
);
}
private documentToModel(document: ClientDocument) {
const props = document.toObject();
return new Client({
...props,
id: new ClientId(props._id),
});
}
private modelToDocument(model: Client) {
const props = model.asProps;
const document = new this.clientModel({
...props,
_id: props.id.toString(),
});
return document;
}
}
三.总结
? ? ? ?本周主要通过学习SDU信息门户代码的图灵式的Oauth登录,学习掌握了Typescript语言,我之前从未接触typescript语言,现在已经略微学习到了基本的语法,通过分析了项目代码,更加深入地掌握理解了typescript,并且我也学习了一部分地nestJS,掌握了docker的使用方式以及如何使用docker来部署项目或者pull官方软件,还学会了写自己的docker-compose.yaml文件,对了,go语言的基本用法我也基本掌握了。虽然学习了很多新的知识,但感觉还有很多东西需要学习,学得越多,越感觉自己知识地浅陋。希望以后通过和队友的交流和自己的学习能学更多的知识。
|