CKFinder 3 – ASP.NET 连接器文档
操作指南

如果您没有找到您要寻找的答案,请向我们发送您的问题:http://cksource.com/contact

实现身份验证器

默认情况下,CKFinder 会拒绝所有人访问其界面。

要添加您的身份验证器,请实现 IAuthenticator 接口,并在 ConnectorBuilder.SetAuthenticator 方法中设置它。

身份验证器应确定执行请求的用户是否可以访问 CKFinder,并且应为该用户分配角色。

最简单的实现可能如下所示

using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
public class MyAuthenticator : IAuthenticator
{
/*
* 尽管此方法是异步的,但它将针对每个请求被调用
* 并且不建议在其中进行耗时的调用。
*/
public Task<IUser> AuthenticateAsync(ICommandRequest commandRequest, CancellationToken cancellationToken)
{
/*
* 可以安全地假设 IPrincipal 是 ClaimsPrincipal。
*/
var claimsPrincipal = commandRequest.Principal as ClaimsPrincipal;
/*
* 从 claimsPrincipal 中提取角色名称。
*/
var roles = claimsPrincipal?.Claims?.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToArray();
/*
* 强烈建议以某种方式更改此操作,以仅允许某些用户访问 CKFinder。
* 例如,您可以检查 commandRequest.RemoteIPAddress 以限制仅允许您的本地网络访问。
*/
var isAuthenticated = true;
/*
* 创建并返回用户。
*/
var user = new User(isAuthenticated, roles);
return Task.FromResult((IUser)user);
}
}

每个实例使用不同的文件夹

如果您使用多个 CKFinder 实例,则可以使用不同的 id 属性并将它们传递给服务器连接器请求。

CKFinder.start( { id: 'instanceNo1', pass: 'id' } );
CKFinder.start( { id: 'instanceNo2', pass: 'id' } );

在连接器端,您可以在传递给 SetRequestConfiguration 的操作内部使用 request.QueryParameters["id"].FirstOrDefault() 获取当前实例的名称,并将其用于动态配置修改。这样,您可以使每个实例使用自己的根文件夹作为本地文件系统后端。

connectorBuilder
.SetRequestConfiguration(
(request, config) =>
{
var instanceId = request.QueryParameters["id"].FirstOrDefault() ?? string.Empty;
var root = GetRootByInstanceId(instanceId);
var baseUrl = GetBaseUrlByInstanceId(instanceId);
config.AddProxyBackend("default", new LocalStorage(root));
});

出于安全原因,您应该避免在目录路径中直接使用实例名称,并使用某种白名单。上面配置示例中使用的 GetRootByInstanceId() 方法可能如下所示

private static string GetRootByInstanceId(string instanceId)
{
var pathMap = new Dictionary<string, string>
{
{ "instanceNo1", @"C:\Files\No1" },
{ "instanceNo2", @"C:\Files\No2" }
};
string root;
if (pathMap.TryGetValue(instanceId, out root))
{
return root;
}
throw new CustomErrorException("Invalid instance Id");
}

每个用户使用私有文件夹

要为用户创建单独的目录,您需要创建一个简单的机制来将当前用户映射到相应的目录路径。

在构建目录路径时,您应该记住以下内容,这些内容可能会导致路径遍历攻击

  • 不要泄露任何敏感信息。
  • 不要使用任何不安全的数据。

在此示例中,使用当前用户名的 sha1 哈希值。

注意:在为用户创建私有目录时,您还应该记住内部设置,例如缩略图和键值存储提供程序,它们也应该分开。

connectorBuilder.SetRequestConfiguration(
(request, config) =>
{
var userName = request.Principal?.Identity?.Name;
if (userName != null)
{
var sha = new SHA1CryptoServiceProvider();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(userName));
var folderName = BitConverter.ToString(hash).Replace("-", string.Empty);
config.AddResourceType("private", builder => builder.SetBackend("default", folderName));
config.SetThumbnailBackend("default", $"App_Data/{folderName}");
config.SetKeyValueStoreProvider(new EntityFrameworkKeyValueStoreProvider(
"CacheConnectionString", string.Empty, folderName));
}
})

磁盘配额

在此示例中,假设您已使用 IsQuotaAvailable() 方法实现了您自己的检查用户存储配额的逻辑。您可以将此逻辑附加到 CKFinder 中您要检查的命令的 命令前事件(在检查配额的情况下:例如 FileUploadCopyFilesImageResizeCreateFolder 等命令)。

有关实现此功能的源代码,请参阅 DiskQuota 插件示例

记录用户操作

在此示例中,目标是创建一个用于记录用户操作的插件。这可以通过使用 事件 系统来实现。为了便于此示例,让我们假设所有与 中间事件 相对应的用户操作都应记录在案。为此,需要创建简单的事件监听器并将它们附加到应记录的事件。

有关实现此功能的完整源代码,请参阅 UserActionsLogger 插件示例

如果插件已正确注册,您应该在日志文件中看到类似于以下内容的输出。

2016-02-24 11:24:40.0063 | INFO | dummyUser1 - Folder create: Files://folder/
2016-02-24 11:24:51.5327 | INFO | dummyUser1 - File upload: Files://folder/image.jpg
2016-02-24 11:25:10.1064 | INFO | dummyUser1 - File rename: Files://folder/image.jpg -> Files://folder/image2.jpg
2016-02-24 11:25:25.7100 | INFO | dummyUser1 - File move: Files://document.txt -> Files://folder/document.txt
2016-02-24 11:25:43.9000 | INFO | dummyUser1 - File copy: Files://folder/image.jpg -> Files://image.jpg
2016-02-24 11:25:49.6668 | INFO | dummyUser1 - File delete: Files://folder/image.jpg

有关特定事件传递的事件对象参数类型的更多详细信息,请参阅 事件 部分。

自定义命令

此示例展示了一个简单的命令插件,它返回有关文件的基本信息。

有关实现此功能的完整源代码,请参阅 GetFileInfo 插件示例

如果启用了此插件,您可以调用额外的 GetFileInfo 命令,该命令返回有关文件的某些非常基本的信息,例如大小和上次修改时间戳。此行为可以轻松地更改为返回有关文件的任何其他信息(例如,图像的 EXIF 数据或 mp3 文件的 ID3 标签)。

GetFileInfo

说明返回有关文件的基本信息。
方法GET
示例请求获取位于 Files 资源类型 sub1 目录中的 foo.png 文件的基本信息。
/ckfinder/connector?command=GetFileInfo&type=Files&currentFolder=/sub1/&fileName=foo.png
示例响应
{
"name":"foo.png",
"createDate":"20160128084240",
"updateDate":"20160128084240",
"size":27511,
"mimeType":"image/png"
}

有关命令的更多详细信息,请参阅 CKFinder ASP.NET 连接器文档的 命令 部分。

将资源类型指向现有文件夹

资源类型文件夹可以使用 SetBackend 方法在 ConnectorBuilder 中定义的 SetRequestConfiguration 操作执行期间定义,也可以使用 folder 配置选项定义(请参阅 资源类型)。定义的目录相对于后端的根目录。

考虑以下文件夹结构

rootDir
└── dir1
└── dir2
└── dir3

其中 rootDir 是为名为 default 的后端定义的根目录。

只需将/作为第二个参数传递给SetBackend方法,即可将资源类型附加到根文件夹。

配置
.AddResourceType("Files", resourceBuilder =>
resourceBuilder.SetBackend("default", "/"));

或者,通过将/值提供给folder配置选项

<resourceType name="Files" folder="/" backend="default" />

通过以上配置,您将在 CKFinder 中看到以下文件夹树

文件
└── dir1
└── dir2
└── dir3

您可以将资源类型指向任何子文件夹,如下所示

配置
.AddResourceType("Files", resourceBuilder =>
resourceBuilder.SetBackend("default", "/dir1"));

使用folder选项

<resourceType name="Files" folder="/dir1" backend="default" />
文件
└── dir2
└── dir3

或指向更深的子文件夹

配置
.AddResourceType("Files", resourceBuilder =>
resourceBuilder.SetBackend("default", "/dir1/dir2"));
<resourceType name="Files" folder="/dir1/dir2" backend="default" />
文件
└── dir3

在不转换为应用程序或在 WebMatrix 中使用 Zip 包

在不转换为应用程序或在 WebMatrix 中使用.zip包的最简单方法是将.zip存档内容提取到一个空的站点中,并省略根ckfinder文件夹。

接下来打开Web.config文件并更改

<add key="ckfinderRoute" value="/connector" />

<add key="ckfinderRoute" value="/ckfinder/connector" />

<appSettings />部分中。

CKFinder 和 Classic ASP

CKFinder 3.x 不支持 Classic ASP,但是,可以通过自定义的Authenticator类和经典 ASP 应用程序中的附加 ASP 脚本,获取 CKFinder 的用户身份验证数据。

附加的 ASP 脚本应返回包含用户身份验证数据的 JSON 数据。它应该放在一个公开可见的位置。它可能看起来像这样

<%@Language=VBScript CodePage=65001%>
<% Option Explicit %>
<%
' 出于安全原因,只允许本地请求。
' 如果需要远程请求,则应使用某种密钥或证书。
If Request.ServerVariables("LOCAL_ADDR") <> Request.ServerVariables("REMOTE_ADDR") Then
Response.Status = "403 Forbidden"
Response.End
End If
Dim isAuthenticated
Dim roles
' 如果用户被允许访问 CKFinder,则将 True 分配给 isAuthenticated。
isAuthenticated = False
' 将用户角色分配给 roles 数组。
' 例如
' roles = Array("Administrator", "Manager")
Dim quotedRoles
ReDim quotedRoles(uBound(roles))
Dim role
Dim index
index = 0
For Each role In roles
quotedRoles(index) = """" & role & """"
index = index + 1
Next
Response.ContentType = "application/json"
Response.Charset = "utf-8"
Response.Write "{ ""isAuthenticated"": "
Response.Write """" & isAuthenticated & """"
Response.Write ", ""roles"": [ "
Response.Write Join(quotedRoles, ", ")
Response.Write " ] }"
%>

自定义的Authenticator类可能看起来像这样

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class ClassicAspAuthenticator : IAuthenticator
{
private readonly string _classicAspConnectorUrl;
public ClassicAspAuthenticator(string classicAspConnectorUrl)
{
_classicAspConnectorUrl = classicAspConnectorUrl;
}
public async Task<IUser> AuthenticateAsync(ICommandRequest commandRequest, CancellationToken cancellationToken)
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(_classicAspConnectorUrl, cancellationToken);
var json = await response.Content.ReadAsStringAsync();
return json.FromJson<User>();
}
}

最后一步是在ConnectorBuilder.SetAuthenticator方法中传递ClassicAspAuthenticator实例

var authenticator = new ClassicAspAuthenticator("http://url/to/the/additional/classic/asp/script.asp");
var connectorBuilder = new ConnectorBuilder();
connectorBuilder.SetAuthenticator(authenticator);

混合多个 Owin 中间件

当您想将 CKFinder 中间件与其他中间件混合使用时,您可以通过路由映射来实现。

public void Configuration(IAppBuilder appBuilder)
{
var connectorBuilder = ConfigureConnector();
var connector = connectorBuilder.Build(new OwinConnectorFactory());
appBuilder.Map("/CKFinder/connector", builder => builder.UseConnector(connector));
appBuilder.Map("/anotherMiddleware", builder => builder.UseAnotherMiddleware());
}

有关 Owin 路由映射的更多信息,请参阅MSDN 上的 AppBuilder 类参考

有关与现有应用程序集成的更多信息,请参阅在现有应用程序中集成

添加对自定义存储的支持

可以通过实现IFileSystem接口,添加对自定义文件系统的支持。

此接口的大多数成员是不言自明的,但是有四个方法需要一些额外的说明

Task<FolderListResult> GetFolderInfosAsync(string path, CancellationToken cancellationToken);
Task<FolderListResult> GetFolderInfosAsync(IFolderListContinuation folderListContinuation, CancellationToken cancellationToken);
Task<FileListResult> GetFileInfosAsync(string path, CancellationToken cancellationToken);
Task<FileListResult> GetFileInfosAsync(IFileListContinuation fileListContinuation, CancellationToken cancellationToken);

这四个成员负责列出文件夹和文件。假设使用路径作为参数的调用始终是第一个请求,后续调用使用延续对象。这些延续对象是游标,应该由文件系统的实现内部处理。

示例适配器支持在数据库中存储。

出于本教程的目的,让我们假设文件将存储在一个数据库表中,该表由下面显示的 SQL 模式表示

SQL Server

CREATE TABLE [dbo].[DatabaseNodes](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Path] [nvarchar](max) NULL,
[Type] [int] NOT NULL,
[Contents] [varbinary](max) NULL,
[Size] [int] NOT NULL,
[MimeType] [nvarchar](max) NULL,
[Timestamp] [datetime] NOT NULL,
CONSTRAINT [PK_dbo.DatabaseNodes] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

实现自定义存储

在 CKFinder 3 ASP.NET 连接器中添加自定义存储的第一步是创建IFileSystem的实现。此接口定义了与给定文件系统通信所需的所有方法——例如写入、读取或删除文件。

看看IFileSystem的自定义实现,它需要将文件保存到具有假设模式的数据库表中。DatabaseStorage类使用EntityFramework与数据库通信。下面介绍DatabaseStorage类的实例化。

var databaseStorage = new DatabaseStorage("Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;");

为<tt>web.config</tt>配置注册自定义适配器

要为静态web.config配置注册自定义适配器,您必须定义如何创建此适配器。这在静态FileSystemFactory类中完成。对于DatabaseStorage类,它只需要连接字符串参数,这可以通过以下示例完成

FileSystemFactory.RegisterFileSystem("local", options => new DatabaseStorage(options["connectionString"]));

请参阅DatabaseStorage 示例,了解实现此功能的完整源代码。

每个请求设置许可证详细信息

要按请求设置许可证详细信息,需要在代码中动态更改连接器配置(请参阅通过代码进行配置)。

许可证详细信息可以在传递给connectorBuilder.SetRequestConfiguration()方法的回调中按请求更改,如以下示例所示

var connector = connectorBuilder
.LoadConfig()
.SetRequestConfiguration(
(request, config) =>
{
config.LoadConfig();
// 设置要在当前请求中使用的 licenseName 和 licenseKey。
connectorBuilder.licenseProvider.SetLicense(licenseName, licenseKey);
})
.Build(connectorFactory);

在 Amazon S3 适配器中定义自定义 S3 客户端

要为 Amazon S3 适配器定义自定义 S3 客户端,请扩展默认的IFileSystem类并覆盖createClient()工厂方法,如下所示。

public class CustomS3Storage : AmazonStorage
{
public CustomS3Storage() : base("bucket-name")
{
}
protected override AmazonS3Client createClient()
{
BasicAWSCredentials credentials = new BasicAWSCredentials("key", "secret");
AmazonS3Config config = new AmazonS3Config();
config.RegionEndpoint = RegionEndpoint.GetBySystemName("region-name");
config.SignatureVersion = "4";
return new AmazonS3Client(credentials, config);
}
}

然后您可以在连接器中注册新的存储类型

connectorBuilder
.LoadConfig()
.SetRequestConfiguration(
(request, config) =>
{
config.LoadConfig();
config.AddBackend("s3", new CustomS3Storage());
config.AddResourceType("S3 Resorce Type", resourceBuilder => {
resourceBuilder.SetBackend("s3", "");
resourceBuilder.SetLazyLoaded(true);
});
}

保护公开访问的文件夹

在集成 CKFinder 时,您通常希望让用户访问上传的文件,以便他们可以将图像或文件链接插入编辑的内容中。 这可以通过两种方式完成

  • 您可以将 CKFinder 配置为使用 Proxy 命令通过连接器提供所有文件。
  • 您可以使该文件夹公开访问,以便所有文件都通过 Web 服务器提供。

如果您依赖 Web 服务器来提供使用 CKFinder 上传的文件,则应采取其他步骤来确保以安全的方式提供这些文件。

假设您已将 CKFinder 配置为允许上传 .avi 文件。

即使 .avi 文件随后使用有效的 Content-Type: video/x-msvideo 标头提供,某些浏览器也可能忽略此信息并对原始文件内容执行其他检查。 如果在文件内容中检测到任何类似 HTML 的数据,浏览器可能会决定忽略有关内容类型的信息,并将提供的内容视为普通网页。 此行为称为 "内容嗅探"(也称为“媒体类型嗅探”或“MIME 嗅探”),在某些情况下,它可能导致安全问题(例如,它可能为 跨站点脚本 (XSS) 攻击 打开大门)。

为了避免内容嗅探,您应该确保您的服务器在从公开可用的文件夹提供文件时,将 X-Content-Type-Options: nosniff 标头添加到所有 HTTP 响应中。 X-Content-Type-Options 响应 HTTP 标头是服务器使用的标记,用于指示 Content-Type 标头设置的 MIME 类型不应更改,并且应遵循。 因此,浏览器不会对接收到的内容执行任何内容嗅探。

Microsoft IIS

对于 Microsoft IIS 服务器,您可以在 web.config 文件中启用 X-Content-Type-Options 标头

<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Content-Type-Options"/>
<add name="X-Content-Type-Options" value="nosniff"/>
</customHeaders>
</httpProtocol>
</system.webServer>

Apache

如果您使用 Apache Web 服务器,可以使用 mod_headers 添加自定义 HTTP 响应标头。 确保 mod_headers 模块已启用,并在公开可访问的文件夹的根目录中创建(或修改)以下 .htaccess 文件(例如 userfiles/.htaccess

Header set X-Content-Type-Options "nosniff"

Nginx

如果您使用 Nginx,可以为每个位置定义自定义 HTTP 响应标头

location /userfiles {
add_header X-Content-Type-Options nosniff;
}