Используйте Friend для аутентификации и авторизации в одностраничном веб-приложении Clojure

Я пытаюсь интегрировать аутентификацию и авторизацию Friend в одностраничное веб-приложение Clojure/Compojure.

У меня есть форма входа, поддерживаемая контроллером Angular, и этот контроллер использует AJAX для аутентификации имени пользователя и пароля в веб-приложении и получения аутентифицированной записи пользователя. Из-за этого мне не нужно поведение по умолчанию, обеспечиваемое входом в систему на основе формы Friend — я в основном хочу полагаться на коды состояния HTTP и мне не нужны какие-либо перенаправления страниц Friend.

Например, выполнение неаутентифицированного запроса должно просто возвращать код состояния 401 и не должно перенаправлять на «/login». У меня эта часть работает, указав пользовательский «: unauthenticated-handler» при настройке Friend (код приведен ниже).

При успешном входе мне просто нужен код состояния 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», при успешном входе в систему создается перенаправление страницы — я хочу предотвратить это перенаправление, поэтому я установил значение false. Однако это единственное изменение приводит к ошибке 404 и неудачному входу в систему.

Таким образом, помимо карты аутентификации Friend, мне нужно каким-то образом вернуть здесь код состояния 200, а также я хочу вернуть «запись пользователя» в теле ответа, чтобы клиентское приложение могло адаптировать пользовательский интерфейс в зависимости от роли пользователя (я уже есть запросы/ответы JSON, упакованные и работающие).

Поэтому я думаю, что мне нужен эквивалент этого, когда я вызываю функцию Friend "make-auth":

{:status 200 :body user-record}

Однако похоже, что у меня может быть либо карта аутентификации, либо ответ, но не оба вместе.

Можно ли этого добиться с помощью Friend, и если да, то как?


person caprica    schedule 28.11.2013    source источник
comment
Возможно, это связано с: github.com/cemerick/friend/issues/83, что подразумевает что в настоящее время это невозможно и требует исправления для Friend.   -  person caprica    schedule 30.11.2013
comment
да, похоже, это так.   -  person ponzao    schedule 30.11.2013


Ответы (2)


Вам нужно, чтобы :redirect-on-auth? было false, и вам нужно обернуть свой ответ в карту ответов {:status 200 :body (workflows/make-auth...)}. Обратите внимание, что вам, вероятно, потребуется сериализовать свое тело в String или что-то еще, что можно обработать.

person ponzao    schedule 28.11.2013
comment
На первый взгляд это выглядело так, как будто это работает, но хотя он правильно возвращает запись пользователя в формате JSON клиенту вместе с кодом состояния 200, аутентифицированный сеанс по-прежнему не создается. Когда я пытаюсь получить доступ к защищенному ресурсу, это не удается. - person caprica; 29.11.2013
comment
@caprica, жаль это слышать. Не могли бы вы привести немного больше кода в качестве примера? Меня особенно интересует ваше промежуточное программное обеспечение. - person ponzao; 29.11.2013
comment
@caprica, не могли бы вы попробовать удалить session/wrap-session, handler/site уже оборачивает ваш обработчик в сеанс, и это может вызвать вашу проблему. - person ponzao; 29.11.2013
comment
Удаление сеанса/обертывания не имело значения - я все еще терплю неудачу при последующем доступе к аутентифицированной ссылке. На самом деле я добавил session/wrap-session только для того, чтобы попробовать что-то, когда что-то не работает. - person caprica; 29.11.2013
comment
@caprica Я прочитал все ваши связанные сообщения и проблему на github. Вы, наконец, нашли обходной путь для этого? Не могли бы вы поделиться им с нами? заранее спасибо - person Jaime Agudo; 20.01.2014
comment
Это не работает из-за ошибки или отсутствующей функции/поведения, упомянутых в другом ответе. В моем собственном приложении я подделал это, заставив первоначальный AJAX-запрос клиента игнорировать перенаправление страницы при успешной аутентификации, а затем сделать последующий запрос для получения ролей. Не идеально, но работает. - person caprica; 21.01.2014

Я создал рабочее решение вашей проблемы. Ключ в том, чтобы убедиться, что вы возвращаете статус, тело и объединяете учетные данные аутентификации с ответом. Итак, вы хотите вернуться

{:status 200 :body {:message "Hello World"} 
 :session {:cemerick.friend/identity {...}} ...}

Проблема с приведенным выше решением заключается в том, что оно объединяет результаты сеанса в тело, а не в сам ответ на звонок. Суть

(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)))))))
person rileyfreeman    schedule 10.05.2014