Authenticating Website Members as Users in CKFinde

2019-04-30 19:53发布

问题:

Before beginning this question, I should point out that my knowledge of ASP.NET & C# is pretty much nil.

I'm in the process of trying to integrate the ASP.NET version of CKFinder v3 into a site built in a different language and all is going well so far; I have everything setup as I want it and it's working when I grant unrestricted access to CKF but I'm stuck at the point now of trying to restrict access to it by authenticating only certain members of my site to use it. All the pages that CKFinder appears on on my site are only accessible by those certain members but I need an extra level of security if, for example, anyone figures out the direct path to my "ckfinder.html" file.

In the ASP version of CKFinder, I simply added this line in the function that checks my member's privileges, where isEditor was a boolean whose value was assigned per member based on information from my database:

session("accessckf")=isEditor

and then edited the CheckAuthentication() function in CKFinder's "config.asp" file to read:

function CheckAuthentication()
    CheckAuthentication=session("accessckf")
end function

Reading through this "Howto", authentication seems to be much more complex in v3 but, after a lot of trial and error and some help from Lesiman, I created this C# file, which is located in my CKF directory:

<%@page codepage="65001" debug="true" language="c#" lcid="6153"%>
<%@import namespace="CKSource.CKFinder.Connector.Core"%>
<%@import namespace="CKSource.CKFinder.Connector.Core.Authentication"%>
<%@import namespace="CKSource.CKFinder.Connector.Core.Builders"%>
<%@import namespace="CKSource.CKFinder.Connector.Host.Owin"%>
<%@import namespace="Owin"%>
<%@import namespace="System.Data.Odbc"%>
<%@import namespace="System.Threading"%>
<%@import namespace="System.Threading.Tasks"%>
<script runat="server">
    public void Configuration(IAppBuilder appBuilder){
        var connectorBuilder=ConfigureConnector();
        var connector=connectorBuilder.Build(new OwinConnectorFactory());
        appBuilder.Map("/path/to/connector",builder=>builder.UseConnector(connector));
    }
    public ConnectorBuilder ConfigureConnector(){
        var connectorBuilder=new ConnectorBuilder();
        connectorBuilder.SetAuthenticator(new MyAuthenticator());
        return connectorBuilder;
    }
    public class MyAuthenticator:IAuthenticator{
        public Task<IUser> AuthenticateAsync(ICommandRequest commandRequest,CancellationToken cancellationToken){
            var domain=HttpContext.Current.Request.Url.Host;
            var cookie=HttpContext.Current.Request.Cookies[urlDomain];
            var password="";
            var username="";
            var user=new User(false,null);
            if (cookie!=null){
                if (cookie["username"]!=null)
                    username=cookie["username"];
                if (cookie["password"]!=null)
                    password=cookie["password"];
                if(username!=""&&password!=""){
                    var connection=new OdbcConnection("database=[database];driver=MySQL;pwd=[pwd];server=[server];uid=[uid];");
                    connection.Open();
                    OdbcDataReader records=new OdbcCommand("SELECT ISEDITOR FROM MEMBERS WHERE USERNAME='"+username+"' AND PASSWORD='"+password+"'",connection).ExecuteReader();
                    if(records.HasRows){
                        records.Read();
                        bool isEditor=records.GetString(0)=="1";
                        var roles="member";
                        if(isEditor)
                            roles="editor,member";
                            user=new User(isEditor,roles.Split(','));
                        }
                        records.Close();
                        connection.Close();
                }
            }
            return Task.FromResult((IUser)user);
        }
    }
</script>

Loading that page produces no errors (which doesn't necessarily mean it's working as trying to write anything to screen from within the public class doesn't work, for some reason) so now I'm at the stage of somehow checking that file for authentication.

Originally, I had tried loading it via XMLHttp from within my function that checks membership privileges for the site but, as I suspected and as Lesmian confirmed, that wouldn't work. After more trial & error, I added code to check website member privileges to the C# file, which leads me to where I am now: stuck!

What do I need to edit within CKFinder in order to have it use this custom file to check whether or not a user is authenticated?

回答1:

First you'll need a connector between the ASP's Session and CKFinder's .Net authenticator. Here's an example that serializes ASP Session contents into JSON.

Put the connector.asp into a publicly accessible location. http://myaspwebsite.com/connector.asp for example.

connector.asp

<%@Language=VBScript CodePage=65001%>
<% Option Explicit %>
<!--#include file="JSON.asp"-->
<%
' obtain JSON.asp from https://github.com/tugrul/aspjson/archive/master.zip

' just for testing, must be removed in the production environment
Session("isEditor") = True
Session("isMember") = True

' only local requests allowed
' instead of local and remote ip comparison, a secret key can be used
If Request.ServerVariables("LOCAL_ADDR") <> Request.ServerVariables("REMOTE_ADDR") Then
    Response.Status = "403 Forbidden"
    Response.End
End If

Response.ContentType = "application/json"
Response.Charset = "utf-8"

Dim JSONObject, Key
Set JSONObject = jsObject()

For Each Key In Session.Contents
    If Not IsObject(Session.Contents(Key)) Then 'skip the objects cannot be serialized
        JSONObject(Key) = Session.Contents(Key)
    End If
Next

JSONObject.Flush
%>

CKFinder 3.3.0 comes with a default connector which can be found in /ckfinder/bin/CKSource.CKFinder.Connector.WebApp.dll, remove it.

Examine the following program and remember to replace builder.Map("/connector", SetupConnector); and new Uri("http://myaspwebsite.com/connector.asp"); with your own values.

It simply authenticates the users by checking ASP Session varaibles isEditor and isMember via connector.asp and finally claims the roles editor , member or none.

I assume that you have configured the roles editor and member in the web.config.

Then put the Shaggy.cs into /ckfinder/App_Code. Create App_Code directory if not exist. .Net files in this folder will be compiled on the fly.

For more information have a look at Shared Code Folders in ASP.NET Web Projects

Shaggy.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Newtonsoft.Json.Linq;
using Owin;

[assembly: Microsoft.Owin.OwinStartup(typeof(CKSource.CKFinder.Connector.Shaggy.Startup))]
namespace CKSource.CKFinder.Connector.Shaggy
{
    using FileSystem.Local;
    using FileSystem.Dropbox;
    using Core;
    using Core.Authentication;
    using Config;
    using Core.Builders;
    using Core.Logs;
    using Host.Owin;
    using Logs.NLog;
    using KeyValue.EntityFramework;

    public class Startup
    {
        public void Configuration(IAppBuilder builder)
        {
            LoggerManager.LoggerAdapterFactory = new NLogLoggerAdapterFactory();

            RegisterFileSystems();

            builder.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "ApplicationCookie",
                AuthenticationMode = AuthenticationMode.Active
            });

            //replace connector path with yours
            builder.Map("/connector", SetupConnector);
        }

        private static void RegisterFileSystems()
        {
            FileSystemFactory.RegisterFileSystem<LocalStorage>();
            FileSystemFactory.RegisterFileSystem<DropboxStorage>();
        }

        private static void SetupConnector(IAppBuilder builder)
        {
            var keyValueStoreProvider = new EntityFrameworkKeyValueStoreProvider("CacheConnectionString");
            var authenticator = new ShaggysAuthenticator();

            var connectorFactory = new OwinConnectorFactory();
            var connectorBuilder = new ConnectorBuilder();
            var connector = connectorBuilder
                .LoadConfig()
                .SetAuthenticator(authenticator)
                .SetRequestConfiguration(
                    (request, config) =>
                    {
                        config.LoadConfig();
                        config.SetKeyValueStoreProvider(keyValueStoreProvider);
                    })
                .Build(connectorFactory);

            builder.UseConnector(connector);
        }
    }

    public class ShaggysAuthenticator : IAuthenticator
    {
        // this method makes an http request on the background to gather ASP's all session contents and returns a JSON object
        // if the request contains ASP's session cookie(s)
        private static JObject GetAspSessionState(ICommandRequest requestContext)
        {
            // building Cookie header with ASP's session cookies
            var aspSessionCookies = string.Join(";",
                requestContext.Cookies.Where(cookie => cookie.Key.StartsWith("ASPSESSIONID"))
                    .Select(cookie => string.Join("=", cookie.Key, cookie.Value)));

            if (aspSessionCookies.Length == 0)
            {
                // logs can be found in /ckfinder/App_Data/logs
                LoggerManager.GetLoggerForCurrentClass().Info("No ASP session cookie found");
                // don't make an extra request to the connector.asp, there's no session initiated
                return new JObject();
            }

            //replace this URL with your connector.asp's
            var publicAspSessionConnectorUrl = new Uri("http://myaspwebsite.com/connector.asp");
            var localSafeAspSessionConnectorUrl = new UriBuilder(publicAspSessionConnectorUrl) { Host = requestContext.LocalIpAddress };

            using (var wCli = new WebClient())
                try
                {
                    wCli.Headers.Add(HttpRequestHeader.Cookie, aspSessionCookies);
                    wCli.Headers.Add(HttpRequestHeader.Host, publicAspSessionConnectorUrl.Host);
                    return JObject.Parse(wCli.DownloadString(localSafeAspSessionConnectorUrl.Uri));
                }
                catch (Exception ex) // returning an empty JObject object in any fault
                {
                    // logs can be found in /ckfinder/App_Data/logs
                    LoggerManager.GetLoggerForCurrentClass().Error(ex);
                    return new JObject();
                }
        }

        public Task<IUser> AuthenticateAsync(ICommandRequest commandRequest, CancellationToken cancellationToken)
        {
            var aspSessionState = GetAspSessionState(commandRequest);

            var roles = new List<string>();
            var isEditor = aspSessionState.GetNullSafeValue("isEditor", false);
            var isMember = aspSessionState.GetNullSafeValue("isMember", false);

            if (isEditor) roles.Add("editor");
            if (isMember) roles.Add("member");

            var isAuthenticated = isEditor || isMember;
            var user = new User(isAuthenticated, roles);
            return Task.FromResult((IUser)user);
        }
    }

    public static class JObjectExtensions
    {
        // an extension method to help case insensitive lookups with a default value to get avoid NullReferenceException
        public static T GetNullSafeValue<T>(this JObject jobj, string key, T defaultValue = default(T))
        {
            dynamic val = jobj.GetValue(key, StringComparison.OrdinalIgnoreCase);
            if (val == null) return defaultValue;
            return (T)val;
        }
    }
}

Now you should have a working CKFinder connector. Change the logic in the method AuthenticateAsync if you need and see how CKFinder handles your Classic ASP membership management.



回答2:

Did you setup your custom authentication provider with ConnectorBuilder?

public ConnectorBuilder ConfigureConnector()
{
   var connectorBuilder = new ConnectorBuilder();
   connectorBuilder.SetAuthenticator(new MyAuthenticator());

   return connectorBuilder;
}

You can find full example here: http://docs.cksource.com/ckfinder3-net/configuration_by_code.html.

UPDATE

Additionally you should register ConnectorBuilder inside Startup class to add it to request pipeline:

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

All this is from a documentation link I've provided before.