Phoenix框架是否支持Windows身份验证?

时间:2015-10-06 15:07:29

标签: phoenix-framework

我们的网络应用程序目前在Windows和IIS上运行C#。我们在很大程度上依赖于此环境中包含的Windows身份验证方案。启用Windows身份验证后,我们可以检测已连接用户的身份,并对他们可以使用的屏幕和操作执行授权。

如果我设置Phoenix Web应用程序,是否可以根据当前的Windows登录检测连接用户的身份?如果没有,那么Windows身份验证是否易于使用?

1 个答案:

答案 0 :(得分:2)

我上周末这样做了。对的,这是可能的。您必须使用IIS的HttpPlatformHandler加载项才能使其正常工作。 HttpPlatformHandler具有forwardWindowsAuthToken配置设置,您可以使用该设置将经过身份验证的用户的Windows用户令牌从IIS转发到作为子进程运行的Phoenix应用程序。您必须使用NIF来处理令牌并获取Windows用户名或SID。正如您将从文档中注意到的那样,您需要调用CloseHandle来为每个请求释放Windows用户令牌。

(如果下面的代码不符合最佳实践,我会提前道歉。我是Elixir的新手,并且正在积极尝试学习如何编写更好的代码。这也是来自黑客会议,试图找出解决方案这个周末,所以也不一定要打磨。)

为此,我将所有内容打包到一个可以放入管道的自定义插件中(我删除了Logger语句以压缩示例的大小):

defmodule MyApp.WindowsAuthentication do
  import Plug.Conn

  require Logger

  @on_load :load_nifs

  def load_nifs do
    if match? {:win32, _}, :os.type do
      :erlang.load_nif("./priv/windows_authentication", 0)
    else
      :ok
    end
  end

  def init(options), do: options

  def call(conn, _options) do
    if match? {:win32, _}, :os.type do
      case get_req_header(conn, "x-iis-windowsauthtoken") do
        [token_handle_string] ->
          # token_handle_string is a hex string
          token_handle = String.to_integer(token_handle_string, 16)
          case do_get_windows_username(token_handle) do
            {:ok, {domain_name, username}} ->
              conn = assign(conn, :windows_user, {domain_name, username})
            error ->
              Logger.error IO.inspect(error)
          end

          do_close_handle(token_handle)
        [] ->
          Logger.debug "X-IIS-WindowsAuthToken was not present"
      end
    end

    conn
  end

  def do_get_windows_username(_token_handle) do
    raise "do_get_windows_username/1 is only available on Windows"
  end

  def do_close_handle(_handle) do
    raise "do_close_handle/1 is only available on Windows"
  end
end

NIF的C源代码如下:

#include <Windows.h>
#include <erl_nif.h>

static const char* error_atom = "error";
static const char* invalid_token_handle_atom = "invalid_token_handle";
static const char* ok_atom = "ok";
static const char* win32_error_atom = "win32_error";

#define MAX_NAME 256

static HANDLE get_user_token(ErlNifEnv *env, ERL_NIF_TERM token) {
  HANDLE token_handle;

  if (!enif_get_ulong(env, token, (unsigned long *)&token_handle)) {
    return NULL;
  }

  return token_handle;
}

static ERL_NIF_TERM make_win32_error_tuple(ErlNifEnv* env, DWORD error_code) {
  return enif_make_tuple2(
    env,
    enif_make_atom(env, error_atom),
    enif_make_ulong(env, error_code)
  );
}

static ERL_NIF_TERM make_invalid_token_handle_error(ErlNifEnv* env) {
  return enif_make_tuple2(
    env,
    enif_make_atom(env, error_atom),
    enif_make_atom(env, invalid_token_handle_atom)
  );
}

static ERL_NIF_TERM do_get_windows_username(ErlNifEnv* env, int argc, ERL_NIF_TERM argv[]) {
  HANDLE token_handle;
  DWORD token_user_length;
  PTOKEN_USER token_user;
  DWORD last_error;
  WCHAR username[MAX_NAME];
  DWORD username_length = MAX_NAME;
  WCHAR domain_name[MAX_NAME];
  DWORD domain_name_length = MAX_NAME;
  size_t converted_chars;
  char converted_username[MAX_NAME * 2];
  char converted_domain_name[MAX_NAME * 2];
  errno_t err;
  BOOL succeeded;
  SID_NAME_USE sid_name_use;

  token_handle = get_user_token(env, argv[0]);
  if (!token_handle) {
    return make_invalid_token_handle_error(env);
  }

  if (!GetTokenInformation(token_handle, TokenUser, NULL, 0, &token_user_length)) {
    last_error = GetLastError();
    if (ERROR_INSUFFICIENT_BUFFER != last_error) {
      return make_win32_error_tuple(env, last_error);
    }
  }

  token_user = (PTOKEN_USER)malloc(token_user_length);
  if (!GetTokenInformation(token_handle, TokenUser, token_user, token_user_length, &token_user_length)) {
    free(token_user);
    return make_win32_error_tuple(env, GetLastError());
  }

  succeeded = LookupAccountSidW(
    NULL,
    token_user->User.Sid,
    username,
    &username_length,
    domain_name,
    &domain_name_length,
    &sid_name_use);
  if (!succeeded) {
    free(token_user);
    return make_win32_error_tuple(env, GetLastError());
  }

  err = wcstombs_s(&converted_chars, converted_username, 512, username, username_length);
  err = wcstombs_s(&converted_chars, converted_domain_name, 512, domain_name, domain_name_length);

  free(token_user);
  return enif_make_tuple2(
    env,
    enif_make_atom(env, ok_atom),
    enif_make_tuple2(
      env,
      enif_make_string(env, converted_domain_name, ERL_NIF_LATIN1),
      enif_make_string(env, converted_username, ERL_NIF_LATIN1)
    )
  );
}

static ERL_NIF_TERM do_close_handle(ErlNifEnv* env, int argc, ERL_NIF_TERM argv[]) {
  HANDLE token_handle;

  token_handle = get_user_token(env, argv[0]);
  if (!token_handle) {
    return make_invalid_token_handle_error(env);
  }

  if (!CloseHandle(token_handle)) {
    return make_win32_error_tuple(env, GetLastError());
  }

  return enif_make_atom(env, ok_atom);
}

static ErlNifFunc nif_functions[] = {
  { "do_close_handle", 1, do_close_handle },
  { "do_get_windows_username", 1, do_get_windows_username }
};

ERL_NIF_INIT(
  Elixir.MyApp.WindowsAuthentication,
  nif_functions,
  NULL,
  NULL,
  NULL,
  NULL
)

您可以使用64位Visual Studio C ++工具编译C代码(打开x64 VS命令提示符)。我用新的VS2017工具尝试了这个。将DLL放在应用程序的priv目录中。

cl /LD /I "C:\Program Files\erl-8.2\erts-8.2\include" /DDEBUG windows_authentication.c advapi32.lib

要运行插件,请将其添加到web / router.ex中的管道中:

pipeline :browser do
  plug :accepts, ["html"]
  plug MyApp.WindowsAuthentication
  plug :fetch_session
  plug :fetch_flash
  plug :protect_from_forgery
  plug :put_secure_browser_headers
end

最终结果是conn.assigns.windows_user将包含{domain_name, username}形式的元组,该元组具有经过身份验证的用户的Windows域用户名。

注意:当我尝试这个时,我在作为IIS的子进程运行时发现了erl.exe的CPU和内存泄漏问题。我还在努力解决这个问题,以防你看到它。我发布了一个关于它的问题here

当我清理它并修复内存/ CPU问题时,我可能会在hex.pm上将其作为库发布,但是现在,这里的代码将允许您使用Phoenix进行Windows身份验证。

相关问题