您现在的位置是:首页 >技术交流 >DelphiMVCFrameWork 源码分析(三)网站首页技术交流

DelphiMVCFrameWork 源码分析(三)

看那山瞧那水 2024-06-17 10:43:18
简介DelphiMVCFrameWork 源码分析(三)

中间件(MiddleWare)

文档中是这样说的:

Middleware is a powerful and flexible layer within DMVCFramework. Middleware allows you to
write SOLID code and separate the processing or each request into smaller steps to be invoked during the request handling. Middleware is the perfect layer for cross cutting concerns or to control HTTP requests/response.

中间件是DMVCFramework中一个强大而灵活的层。中间件允许您编写符合"SOLID"原则的代码,并将处理或请求分成更小的步骤,以便在调用请求期间进行处理。中间件是全局关注或控制HTTP的请求/响应的理想层。

  • 中间件的使用场景:
  • 支持CORS(跨域资源共享)
  • JWT 授权
  • HTTP基础授权
  • 其它形式的授权
  • URL重定向(如果你没用Apache或IIS的Webservice的URL重定向功能)
  • 反向代理(Reverse proxy)
  • 处理 ETag HTTP header
  • 日志Log
  • 缓存
  • 其它等等....

贴一个DelphiMVCFrameWork的中间件的时序图:

框架的中间件是通过事件机制来实现的,有4种情况:

  1. OnBeforeRouting
  2. ONBeforeControllerAction
  3. OnAfterControllerAction
  4. OnAfterRouting

中间件根据需要可以实现其中的一个或多个 。

中间件是通过接口机制来实现的:

基础单元MVCFramework.pas 声明了接口:

  IMVCMiddleware = interface
    ['{3278183A-124A-4214-AB4E-94CA4C22450D}']
    procedure OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
    procedure OnBeforeControllerAction(AContext: TWebContext;
      const AControllerQualifiedClassName: string; const AActionName: string;
      var AHandled: Boolean);
    procedure OnAfterControllerAction(AContext: TWebContext;
      const AControllerQualifiedClassName: string; const AActionName: string;
      const AHandled: Boolean);
    procedure OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
  end;

中间件是由引擎(MVCEngine)管理的。中间件和控制器的一个主要区别是:中间件是单例模式(所有请求共用一个实例),控制器是每个请求一个实例,所以引擎在添加这二者时,中间件是添加实例,控制器是添加类型。

FMVC.AddController(TPublicController);  //添加控制器类名
FMVC.AddController(TPrivateController);  //添加控制器类名
//Other controllers...
FMVC.AddMiddleware(TMyMiddleware.Create());  //添加中间件实例

看一个基本DEMO:(..samplesasicdemo_server, ..samplesasicdemo_vclclient)

服务端的WebModule代码:

unit WebModuleUnit1;

interface

uses System.SysUtils,
  System.Classes,
  Web.HTTPApp,
  MVCFramework;

type
  TWebModule1 = class(TWebModule)
    procedure WebModuleCreate(Sender: TObject);
    procedure WebModuleDestroy(Sender: TObject);

  private
    MVC: TMVCEngine;

  public
    { Public declarations }
  end;

var
  WebModuleClass: TComponentClass = TWebModule1;

implementation

{$R *.dfm}


uses
  App1MainControllerU,
  MVCFramework.Commons,
  MVCFramework.Middleware.StaticFiles;

procedure TWebModule1.WebModuleCreate(Sender: TObject);
begin
  MVC := TMVCEngine.Create(Self,
    procedure(Config: TMVCConfig)
    begin
      Config[TMVCConfigKey.ViewPath] := '.wwwpublic_html';
    end);

  // Web files
  MVC.AddMiddleware(TMVCStaticFilesMiddleware.Create('/app', '.wwwpublic_html'));

  // Image files
  MVC.AddMiddleware(TMVCStaticFilesMiddleware.Create('/images', '.wwwpublic_images', 'database.png'));

  MVC.AddController(TApp1MainController);
end;

procedure TWebModule1.WebModuleDestroy(Sender: TObject);
begin
  MVC.free;
end;

end.

这里添加了2个处理静态文件的中间件,一个是静态网页,一个是图片。

看下这个中间件的代码:

// ***************************************************************************
//
// Delphi MVC Framework
//
// Copyright (c) 2010-2023 Daniele Teti and the DMVCFramework Team
//
// https://github.com/danieleteti/delphimvcframework
//
// Collaborators on this file:
// Jo鉶 Ant鬾io Duarte (https://github.com/joaoduarte19)
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// *************************************************************************** }

unit MVCFramework.Middleware.StaticFiles;

interface

uses
  MVCFramework,
  MVCFramework.Commons,
  System.Generics.Collections;

type
  TMVCStaticFilesDefaults = class sealed
  public const
    /// <summary>
    /// URL segment that represents the path to static files
    /// </summary>
    STATIC_FILES_PATH = '/static';

    /// <summary>
    /// Physical path of the root folder that contains the static files
    /// </summary>
    DOCUMENT_ROOT = '.www';

    /// <summary>
    /// Default static file
    /// </summary>
    INDEX_DOCUMENT = 'index.html';

    /// <summary>
    /// Charset of static files
    /// </summary>
    STATIC_FILES_CONTENT_CHARSET = TMVCConstants.DEFAULT_CONTENT_CHARSET;
  end;

  TMVCStaticFileRulesProc = reference to procedure(const Context: TWebContext; var PathInfo: String; var Handled: Boolean);
  TMVCStaticFileMediaTypesCustomizer = reference to procedure(const MediaTypes: TMVCStringDictionary);
  TMVCStaticFilesMiddleware = class(TInterfacedObject, IMVCMiddleware)
  private
    fSanityCheckOK: Boolean;
    fMediaTypes: TMVCStringDictionary;
    fStaticFilesPath: string;
    fDocumentRoot: string;
    fIndexDocument: string;
    fStaticFilesCharset: string;
    fSPAWebAppSupport: Boolean;
    fRules: TMVCStaticFileRulesProc;
    procedure AddMediaTypes;
    // function IsStaticFileRequest(const APathInfo: string; out AFileName: string;
    // out AIsDirectoryTraversalAttach: Boolean): Boolean;
    function SendStaticFileIfPresent(const AContext: TWebContext; const AFileName: string): Boolean;
    procedure DoSanityCheck;
  public
    constructor Create(
      const AStaticFilesPath: string = TMVCStaticFilesDefaults.STATIC_FILES_PATH;
      const ADocumentRoot: string = TMVCStaticFilesDefaults.DOCUMENT_ROOT;
      const AIndexDocument: string = TMVCStaticFilesDefaults.INDEX_DOCUMENT;
      const ASPAWebAppSupport: Boolean = True;
      const AStaticFilesCharset: string = TMVCStaticFilesDefaults.STATIC_FILES_CONTENT_CHARSET;
      const ARules: TMVCStaticFileRulesProc = nil;
      const AMediaTypesCustomizer: TMVCStaticFileMediaTypesCustomizer = nil);
    destructor Destroy; override;

    procedure OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
    procedure OnBeforeControllerAction(AContext: TWebContext; const AControllerQualifiedClassName: string;
      const AActionName: string; var AHandled: Boolean);

    procedure OnAfterControllerAction(AContext: TWebContext;
      const AControllerQualifiedClassName: string; const AActionName: string;
      const AHandled: Boolean);

    procedure OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
  end;

implementation

uses
  MVCFramework.Logger,
  System.SysUtils,
  System.NetEncoding,
  System.IOUtils,
  System.Classes;

{ TMVCStaticFilesMiddleware }

procedure TMVCStaticFilesMiddleware.AddMediaTypes;
begin
  fMediaTypes.Add('.html', TMVCMediaType.TEXT_HTML);
  fMediaTypes.Add('.htm', TMVCMediaType.TEXT_HTML);
  fMediaTypes.Add('.txt', TMVCMediaType.TEXT_PLAIN);
  fMediaTypes.Add('.text', TMVCMediaType.TEXT_PLAIN);
  fMediaTypes.Add('.csv', TMVCMediaType.TEXT_CSV);
  fMediaTypes.Add('.css', TMVCMediaType.TEXT_CSS);
  fMediaTypes.Add('.js', TMVCMediaType.TEXT_JAVASCRIPT);
  fMediaTypes.Add('.json', TMVCMediaType.APPLICATION_JSON);
  fMediaTypes.Add('.jpg', TMVCMediaType.IMAGE_JPEG);
  fMediaTypes.Add('.jpeg', TMVCMediaType.IMAGE_JPEG);
  fMediaTypes.Add('.jpe', TMVCMediaType.IMAGE_JPEG);
  fMediaTypes.Add('.png', TMVCMediaType.IMAGE_PNG);
  fMediaTypes.Add('.ico', TMVCMediaType.IMAGE_X_ICON);
  fMediaTypes.Add('.appcache', TMVCMediaType.TEXT_CACHEMANIFEST);
  fMediaTypes.Add('.svg', TMVCMediaType.IMAGE_SVG_XML);
  fMediaTypes.Add('.xml', TMVCMediaType.TEXT_XML);
  fMediaTypes.Add('.pdf', TMVCMediaType.APPLICATION_PDF);
  fMediaTypes.Add('.svgz', TMVCMediaType.IMAGE_SVG_XML);
  fMediaTypes.Add('.gif', TMVCMediaType.IMAGE_GIF);
end;

constructor TMVCStaticFilesMiddleware.Create(
      const AStaticFilesPath: string;
      const ADocumentRoot: string;
      const AIndexDocument: string;
      const ASPAWebAppSupport: Boolean;
      const AStaticFilesCharset: string;
      const ARules: TMVCStaticFileRulesProc;
      const AMediaTypesCustomizer: TMVCStaticFileMediaTypesCustomizer);
begin
  inherited Create;
  fSanityCheckOK := False;
  fStaticFilesPath := AStaticFilesPath.Trim;
  if not fStaticFilesPath.EndsWith('/') then
    fStaticFilesPath := fStaticFilesPath + '/';

  if TDirectory.Exists(ADocumentRoot) then
  begin
    fDocumentRoot := TPath.GetFullPath(ADocumentRoot);
  end
  else
  begin
    fDocumentRoot := TPath.Combine(AppPath, ADocumentRoot);
  end;
  fIndexDocument := AIndexDocument;
  fStaticFilesCharset := AStaticFilesCharset;
  fSPAWebAppSupport := ASPAWebAppSupport;
  fMediaTypes := TMVCStringDictionary.Create;
  fRules := ARules;
  AddMediaTypes;
  if Assigned(AMediaTypesCustomizer) then
  begin
    AMediaTypesCustomizer(fMediaTypes);
  end;
end;

destructor TMVCStaticFilesMiddleware.Destroy;
begin
  fMediaTypes.Free;

  inherited Destroy;
end;

procedure TMVCStaticFilesMiddleware.DoSanityCheck;
begin
  if not fStaticFilesPath.StartsWith('/') then
  begin
    raise EMVCException.Create('StaticFilePath must begin with "/" and cannot be empty');
  end;
  if not TDirectory.Exists(fDocumentRoot) then
  begin
    raise EMVCException.CreateFmt('TMVCStaticFilesMiddleware Error: DocumentRoot [%s] is not a valid directory', [fDocumentRoot]);
  end;
  fSanityCheckOK := True;
end;

// function TMVCStaticFilesMiddleware.IsStaticFileRequest(const APathInfo: string; out AFileName: string;
// out AIsDirectoryTraversalAttach: Boolean): Boolean;
// begin
// Result := TMVCStaticContents.IsStaticFile(fDocumentRoot, APathInfo, AFileName,
// AIsDirectoryTraversalAttach);
// end;

procedure TMVCStaticFilesMiddleware.OnAfterControllerAction(AContext: TWebContext;
      const AControllerQualifiedClassName: string; const AActionName: string;
      const AHandled: Boolean);
begin
  // do nothing
end;

procedure TMVCStaticFilesMiddleware.OnAfterRouting(AContext: TWebContext; const AHandled: Boolean);
begin
  // do nothing
end;

procedure TMVCStaticFilesMiddleware.OnBeforeControllerAction(AContext: TWebContext; const AControllerQualifiedClassName,
  AActionName: string; var AHandled: Boolean);
begin
  // do nothing
end;

procedure TMVCStaticFilesMiddleware.OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
var
  lPathInfo: string;
  lFileName: string;
  lIsDirectoryTraversalAttach: Boolean;
  lFullPathInfo: string;
  lRealFileName: string;
  lAllow: Boolean;
begin
//  if not fSanityCheckOK then
//  begin
//    DoSanityCheck;
//  end;

  lPathInfo := AContext.Request.PathInfo;

  if not lPathInfo.StartsWith(fStaticFilesPath, True) then
  begin
    { In case of folder request without the trailing "/" }
    if not lPathInfo.EndsWith('/') then
    begin
      lPathInfo := lPathInfo + '/';
      if not lPathInfo.StartsWith(fStaticFilesPath, True) then
      begin
        AHandled := False;
        Exit;
      end;
    end
    else
    begin
      AHandled := False;
      Exit;
    end;
  end;

  if Assigned(fRules) then
  begin
    lAllow := True;
    fRules(AContext, lPathInfo, lAllow);
    if not lAllow then
    begin
      AHandled := True;
      Exit;
    end;
  end;

  // calculate the actual requested path
  if lPathInfo.StartsWith(fStaticFilesPath, True) then
  begin
    lPathInfo := lPathInfo.Remove(0, fStaticFilesPath.Length);
  end;
  lPathInfo := lPathInfo.Replace('/', PathDelim, [rfReplaceAll]);
  if lPathInfo.StartsWith(PathDelim) then
  begin
    lPathInfo := lPathInfo.Remove(0, 1);
  end;
  lFullPathInfo := TPath.Combine(fDocumentRoot, lPathInfo);

  { Now the actual requested path is in lFullPathInfo }

  if not fSanityCheckOK then
  begin
    DoSanityCheck;
  end;

  if TMVCStaticContents.IsStaticFile(fDocumentRoot, lPathInfo, lRealFileName,
    lIsDirectoryTraversalAttach) then
  begin
    // check if it's a direct file request
    // lIsFileRequest := TMVCStaticContents.IsStaticFile(fDocumentRoot, lPathInfo, lRealFileName,
    // lIsDirectoryTraversalAttach);
    if lIsDirectoryTraversalAttach then
    begin
      AContext.Response.StatusCode := HTTP_STATUS.NotFound;
      AHandled := True;
      Exit;
    end;

    AHandled := SendStaticFileIfPresent(AContext, lRealFileName);
    if AHandled then
    begin
      Exit;
    end;
  end;

  // check if a directory request
  if TDirectory.Exists(lFullPathInfo) then
  begin
    if not AContext.Request.PathInfo.EndsWith('/') then
    begin
      AContext.Response.StatusCode := HTTP_STATUS.MovedPermanently;
      AContext.Response.CustomHeaders.Values['Location'] := AContext.Request.PathInfo + '/';
      AHandled := True;
      Exit;
    end;

    if not fIndexDocument.IsEmpty then
    begin
      AHandled := SendStaticFileIfPresent(AContext, TPath.Combine(lFullPathInfo, fIndexDocument));
      Exit;
    end;
  end;

  // if SPA support is enabled, return the first index.html found in the path.
  // This allows to host multiple SPA application in subfolders
  if (not AHandled) and fSPAWebAppSupport and (not fIndexDocument.IsEmpty) then
  begin
    while (not lFullPathInfo.IsEmpty) and (not TDirectory.Exists(lFullPathInfo)) do
    begin
      lFullPathInfo := TDirectory.GetParent(lFullPathInfo);
    end;
    lFileName := TPath.GetFullPath(TPath.Combine(lFullPathInfo, fIndexDocument));
    AHandled := SendStaticFileIfPresent(AContext, lFileName);
  end;
end;

function TMVCStaticFilesMiddleware.SendStaticFileIfPresent(const AContext: TWebContext;
  const AFileName: string): Boolean;
var
  lContentType: string;
begin
  Result := False;
  if TFile.Exists(AFileName) then
  begin
    if fMediaTypes.TryGetValue(LowerCase(ExtractFileExt(AFileName)), lContentType) then
    begin
      lContentType := BuildContentType(lContentType, fStaticFilesCharset);
    end
    else
    begin
      lContentType := BuildContentType(TMVCMediaType.APPLICATION_OCTETSTREAM, '');
    end;
    TMVCStaticContents.SendFile(AFileName, lContentType, AContext);
    Result := True;
    Log(TLogLevel.levDebug, AContext.Request.HTTPMethodAsString + ':' +
      AContext.Request.PathInfo + ' [' + AContext.Request.ClientIp + '] -> ' +
      ClassName + ' - ' + IntToStr(AContext.Response.StatusCode) + ' ' +
      AContext.Response.ReasonString);
  end;
end;

end.

可以看到静态文件处理中间件只要处理OnBeforeRouting()事件,其它事件为空。

静态文件有许多种类型,这里把常用的以后缀标志文件类型进行了归类。对文件路径进行检查,如果有自定义的规则,则进行规则处理等。

中间件的架构比较简单,但是实现中间件的功能就不那么容易了。要清楚中间件是要完成什么功能,理清中间件的流程,中间件的使用场景。可以看框架自带的中间件的代码,是如何实现的。

框架自带了常用的中间件:

  •  MVCFramework.Middleware.Compression.pas   // 压缩
  • •MVCFramework.Middleware.ActiveRecord.pas   //ORM
  •  MVCFramework.Middleware.CORS.pas          //CORS
  •  MVCFramework.Middleware.JWT.pas             //JWT Token授权
  •  MVCFramework.Middleware.Analytics.pas     //Use Log save Data 
  • •MVCFramework.Middleware.Authentication.pas   //授权
  •  MVCFramework.Middleware.Authentication.RoleBasedAuthHandler.pas  //基于角色的授权
  • MVCFramework.Middleware.Redirect   //重定向
  • MVCFramework.Middleware.ETag   //ETag Http header
  • •MVCFramework.Middleware.SecurityHeaders.pas   //加密头
  •  MVCFramework.Middleware.StaticFiles.pas   //静态文件
  •  MVCFramework.Middleware.Swagger.pas  //文档生成
  •  MVCFramework.Middleware.Trace.pas   //Debug Utils Tools

关于中间件,就简单介绍这些,这里不是分析。

框架里的JWT Token 登录授权处理中间件比较简单,不支持验证码处理,签名方法也只支持一种HS(HS256,HS384,HS512),不支持RS(RS256, RS384, RS512),ES(ES256,ES384,ES512),PS(PS256,PS384,PS512)等,实际运用中还得扩充。

以后写一个支持验证码的注册登录中间件后再做一个详细分析。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。