在一个单页的Clojure Web应用程序中,使用Friend进行身份验证和授权。

9
我正在尝试将好友身份验证和授权集成到Clojure/Compojure单页面Web应用程序中。
我有一个由Angular控制器支持的登录表单,该控制器使用AJAX针对Web应用程序验证用户名和密码,并获取已认证的用户记录。因此,我不希望使用Friend基于表单的默认行为 - 我基本上希望依赖HTTP状态代码,并且我不希望任何Friend页面重定向。
例如,进行未经身份验证的请求应仅返回401状态代码,而不应重定向到“/login”。通过在配置Friend时指定自定义“:unauthenticated-handler”来实现此部分(包含在下面的代码中)。
成功登录后,我只想要一个200状态代码,而不是重定向到最初请求的页面。这是我无法让其工作的部分。
我根据各种示例编写了一个基于自定义Friend身份验证工作流程的代码(我的Clojure技能目前处于初学者水平)。
(defn my-auth
  [& {:keys [credential-fn]}]
    (routes
      (GET "/logout" req
        (friend/logout* {:status 200}))
      (POST "/login" {{:keys [username password]} :params}
        (if-let [user-record (-> username credential-fn)]
          (if
            (and
              [user-record password]
              (creds/bcrypt-verify password (:password user-record)))
            (let [user-record (dissoc user-record :password)]
              (workflows/make-auth user-record {:cemerick.friend/workflow :my-auth :cemerick.friend/redirect-on-auth? true}))
            {:status 401})
          {:status 401}))))

这里是我的带有中间件声明的处理程序:

(def app
  (-> 
    (handler/site
      (friend/authenticate app-routes
        {:credential-fn (partial creds/bcrypt-credential-fn my-credential-fn)
         :unauthenticated-handler unauthenticated
         :workflows [(my-auth :credential-fn my-credential-fn)]}))
    (session/wrap-session)
    (params/wrap-keyword-params)
    (json/wrap-json-body)
    (json/wrap-json-response {:pretty true})))

还有上面引用的额外处理函数:

(defn unauthenticated [v]
  {:status 401 :body "Unauthenticated"})

最后,还需要一个额外的路由片段来测试身份验证:

(GET "/auth" req
  (friend/authenticated (str "You have successfully authenticated as "
    (friend/current-authentication))))

这个功能基本上可用,几乎满足我的需求。

因为在“make-auth”中“redirect-on-auth?”的值为true,所以在成功登录后会生成一个页面重定向 - 我想阻止该重定向,因此将该值设置为false。然而,这个单一的更改导致了404和登录失败。

因此,除了Friend身份验证映射之外,我还需要以某种方式返回200状态代码,并且我希望在响应正文中返回“user-record”,以便客户端应用程序可以根据用户角色调整UI(我已经包装并使用JSON请求/响应)。

因此,当我调用Friend“make-auth”函数时,我认为我需要相应的等效物:

{:status 200 :body user-record}

然而,似乎我只能获取验证映射或响应,而不能同时获取两者。

如果可以的话,Friend如何实现这一点?


也许这与此相关:https://github.com/cemerick/friend/issues/83,这意味着目前不可能,并且需要修复Friend。 - caprica
是的,看起来就是这样。 - ponzao
2个回答

1
我已经为您的问题构建了一个可行的解决方案。关键是确保您返回状态、正文,并将身份验证凭据合并到响应中。因此,您需要返回:
{:status 200 :body {:message "Hello World"} 
 :session {:cemerick.friend/identity {...}} ...}

上述解决方案的问题在于它将会话结果合并到正文中,而不是合并到Ring响应本身中。Gist
(defn- form-ajax-response 
  "Creates an Ajax Request.
   Authenticates User by merging authentication
   into response along with status and body allowing
   for ajax response."
  [status body resp auth content-type]
  (let [auth-resp (friend/merge-authentication (dissoc resp :body-params) auth)
        formatter (case content-type 
                    "application/edn" noir.response/edn
                    noir.response/json)]
    (merge auth-resp {:status status} (formatter {:body body}))))

(def not-nil? (complement nil?))

(defn form-or-ajax-login 
  "Friend Workflow for Handling forms or ajax requests posted to 
   '/login'.  When a form is posted, the request will initiate a redirect
   on login When the post is performed via ajax. A response will be sent
   with success and redirect information"
  [& {:keys [login-uri credential-fn login-failure-handler redirect-on-auth?] :as ajax-config
      :or {redirect-on-auth? true}}]
  (fn [{:keys [uri request-method content-type params] :as request}]
    (when (and (= :post request-method)
               (= uri (gets :login-uri ajax-config (::friend/auth-config request))))
      (let [creds {:username (:username params) 
                   :password (:password params)}
            {:keys [username password]} creds
            ajax? (not-nil? (ajax-request-type content-type))
            cred-fn (gets :credential-fn ajax-config (::friend/auth-config request))]
        (when-let [user-record (and username password (cred-fn creds))]
          (let [user-auth (workflow/make-auth user-record {::friend/workflow :form-or-ajax
                                                           ::friend/redirect-on-auth? false})]
            (case ajax?
              true (form-ajax-response 202 {:Tom "Peterman"} request user-auth content-type)
              user-auth)))))))

1
你需要将:redirect-on-auth?设置为false,并将响应包装在响应映射{:status 200 :body (workflows/make-auth...)}中。请注意,你可能需要将响应体序列化为String或其他可处理的格式。

一开始看起来好像可以工作,但是虽然它能够正确地将用户记录以JSON格式返回给客户端,并且返回200状态码,但是仍然没有创建已认证的会话。当我尝试访问受保护的资源时,它会失败。 - caprica
@caprica,很抱歉听到这个消息。你能提供一些代码作为例子吗?我对你的中间件特别感兴趣。 - ponzao
@caprica,你可以尝试移除 session/wrap-session,因为 handler/site 已经在会话中包装了你的处理程序,这可能导致问题。 - ponzao
删除session/wrap-session并没有任何影响 - 当我随后访问经过身份验证的链接时,我仍然失败。实际上,当事情不顺利时,我只是添加了session/wrap-session来尝试一些东西。 - caprica
@caprica,我读了你所有相关的帖子和在Github上的问题。你最终找到解决方法了吗?你介意与我们分享一下吗?提前感谢。 - Jaime Agudo
1
它无法正常工作,因为其他答案中提到的错误或缺失的功能/行为。在我的应用程序中,我通过让初始客户端AJAX请求忽略成功身份验证后的页面重定向,然后进行后续请求以获取角色来模拟它。虽然不是理想的解决方案,但它可以工作。 - caprica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接