SDU信息门户(2)图灵认证授权子系统:用户登录

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

创建并导出CreateClientCommand类:构造器属性包括name,logoUri,scopes,redirectUris;

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[],
  ) {}
}

        使用@CommandHandler装饰器让类 CreateClientHandler实现 ICommandHandler,在构造器中创建一个 ClientRepository对象,将execute方法传入的commend中的用户ID用nextId函数表示为唯一标识符然后根据command创建一个新的对象,用newSecret函数产生密码,create函数创建用户,save函数过滤器保存用户,然后提交用户,最后返回创建用户的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)对用户输入的id和密码存储的类,实现了ICommand接口。

export class ValidateClientCommand implements ICommand {
  constructor(public readonly id: ClientId, public readonly secret: string) {}
}

 (2)在构造器中注入ClientRepository类的对象,然后在execute函数中:先用this.repository.findById(command.id)判断是否存在该用户,若错误,报错,然后再用client.validate(command.secret)判断用户密码输入是否正确,若错误,报错,都正确就返回。

@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, '客户端不存在');
  }
}

如果用户密钥错误,抛出异常GrpcException

if (!client.validate(command.secret)) {
      throw new GrpcException(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[];
}

保存用户ID的类

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与事件处理服务的解耦。

先在构造器中注入 ClientQuery对象,然后通过this.clientQuery.findById函数寻找用户并返回用户数据。

@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

ClientProperties的model包含了用户id,name,secret等属性。

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[];
}

client的model类继承自AggregateRoot,是一组关联对象,我们将其视为用于数据更改的单元。每个聚合有一个根和边界。边界定义聚合内的内容。根是聚合中包含的单个特定实体。类中包括各种用户方法。

这里解释一下导入的'sha256':SHA-2,名称来自于安全散列算法2(英语:Secure Hash Algorithm 2)的缩写,一种密码散列函数算法标准,说白了,它就是一个哈希函数。

导入的nanoid库和uuid库一样都可以生成uuid,但是nanoid相比uuid要更轻量级,uuid就是通用唯一识别码(Universally Unique Identifier)的缩写。

assignProps方法将接收到的ClientProperties对象的每一个属性赋值给client中对应的属性,asProps返回对象中的属性。

newsecret方法先用nanoid生成一个唯一识别码,然后用hashsecret加密后赋值到this._secret中,返回的是唯一标识符secret。

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);
  }
}

 创建ClientRepository接口,包括next ID,findbyId,save方法。

import { Inject } from '@nestjs/common';
import { Client } from './client';
import { ClientId } from './client-id';

export interface ClientRepository {
  nextId(): ClientId;
  findById(id: ClientId): Promise<Client | undefined>;
  save(client: Client): Promise<void>;
}

 

4.infrastructure基础类

导入schema注解,ClientDocument继承自Document类,然后导出clientschema。

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { AuthScope } from 'src/domain/models/auth-scope';

@Schema()
export class ClientDocument extends Document {
  @Prop()
  _id: string;

  @Prop()
  secret: string;

  @Prop()
  logoUri: string;

  @Prop()
  name: string;

  @Prop({
    type: [String],
  })
  scopes: AuthScope[];

  @Prop({
    type: [String],
  })
  redirectUris: string[];
}

export const ClientCollection = 'Client';

export const ClientSchema = SchemaFactory.createForClass(ClientDocument);

从mangoDB中查询用户信息

使用@InjectModel(ClientCollection)注解clientModel,构造器constructor的参数列表接受clientModel,findById函数通过传入的id调用this.clientModel.findById(id)函数,失败就返回undefined,成功就返回具体的client属性数据。

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

MongoClientRepository类实现了 ClientRepository接口,首先还是使用@InjectModel(ClientCollection)注解clientModel,构造器constructor的参数列表接受clientModel,nextId是返回一个24位的唯一标识符,documentToModel类和ModeldToocument类实现了client的schema的document对象和model对象的相互转化。findById()函数,失败就返回undefined,成功就返回具体的clientdocument对象,save方法将client过滤并保存下来。

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语言的基本用法我也基本掌握了。虽然学习了很多新的知识,但感觉还有很多东西需要学习,学得越多,越感觉自己知识地浅陋。希望以后通过和队友的交流和自己的学习能学更多的知识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值