Khi đăng nhập thành công, SecurityContext.authenticate () trả về AuthenticationStatus.SEND_CONTINUE thay vì AuthenticationStatus.SUCCESS

Nov 09 2020
  • Jakarta EE 8
  • Wildfly 21
  • Java 11

Sử dụng Java EE Security , tôi đang thử xác thực biểu mẫu tùy chỉnh trong một ứng dụng đơn giản.

Đây là các tệp có liên quan (mô tả sự cố nằm bên dưới tệp):

CustomFormAuthenticationConfig.java

package br.dev.authentication.view;

import javax.enterprise.context.ApplicationScoped;
import javax.faces.annotation.FacesConfig;
import javax.security.enterprise.authentication.mechanism.http.CustomFormAuthenticationMechanismDefinition;
import javax.security.enterprise.authentication.mechanism.http.LoginToContinue;

@CustomFormAuthenticationMechanismDefinition(
    loginToContinue = @LoginToContinue(
        loginPage = "/login.xhtml",
        useForwardToLogin = false,
        errorPage = ""
    )
)
@FacesConfig
@ApplicationScoped
public class CustomFormAuthenticationConfig
{
}

UserAuthenticator.java

package br.dev.authentication.view;

import java.util.Set;

import javax.enterprise.context.ApplicationScoped;
import javax.security.enterprise.credential.Credential;
import javax.security.enterprise.credential.UsernamePasswordCredential;
import javax.security.enterprise.identitystore.CredentialValidationResult;
import javax.security.enterprise.identitystore.IdentityStore;

@ApplicationScoped
public class UserAuthenticator implements IdentityStore
{
    @Override
    public CredentialValidationResult validate(Credential credencial)
    {
        var userCredentials = (UsernamePasswordCredential) credencial;
        var userName = userCredentials.getCaller();
        var password = userCredentials.getPasswordAsString();

        if (userName.equals("1") && password.equals("1"))
        {
            return new CredentialValidationResult(userName, Set.of("USER"));
        }
        else
        {
            return CredentialValidationResult.INVALID_RESULT;
        }
    }
}

login.xhtml

<ui:composition template="/templates/layout.xhtml"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:au="http://dev.br/authentication">

    <ui:define name="title">
        Login
    </ui:define>

    <ui:define name="content">
        <au:errors />
        <div id="fields">
            <h:outputLabel value="User name:" for="userName" />
            <h:inputText id="userName" value="#{login.userName}" />

            <h:outputLabel value="Password:" for="password" />
            <h:inputSecret id="password" value="#{login.password}" />

            <h:commandButton value="Enter" action="#{login.authenticateUser}" />
        </div>
    </ui:define>
</ui:composition>

Login.java

package br.dev.authentication.view;

import java.io.IOException;

import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.security.enterprise.AuthenticationStatus;
import javax.security.enterprise.SecurityContext;
import javax.security.enterprise.authentication.mechanism.http.AuthenticationParameters;
import javax.security.enterprise.credential.UsernamePasswordCredential;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;

@RequestScoped
@Named
public class Login
{
    private String _userName;
    private String _password;

    @Inject
    private FacesContext _facesContext;
    
    @Inject
    private ExternalContext _externalContext;

    @Inject
    private SecurityContext _securityContext;
    
    @NotBlank(message = "User name is required.")
    public String getUserName()
    {
        return _userName;
    }
    
    public void setUserName(String userName)
    {
        _userName = userName;
    }
    
    @NotBlank(message = "Password is required.")
    public String getPassword()
    {
        return _password;
    }
    
    public void setPassword(String password)
    {
        _password = password;
    }
    
    public void authenticateUser() throws IOException
    {
        // After a successful login (username and password correct),
        // executeUserAuthentication() returns AuthenticationStatus.SEND_CONTINUE, 
        // and not AuthenticationStatus.SUCCESS.
        // Why?
        // As a result, the code in the AuthenticationStatus.SUCCESS branch above is not executed.
        AuthenticationStatus result = executeUserAuthentication();
        if (result == AuthenticationStatus.SUCCESS)
        {
            _externalContext.redirect(_externalContext.getRequestContextPath() + "/start.xhtml");
        }
        else if (result == AuthenticationStatus.SEND_CONTINUE)
        {
            _facesContext.responseComplete();
        }
        else if (result == AuthenticationStatus.SEND_FAILURE)
        {
            _facesContext.addMessage(null, new FacesMessage(
                FacesMessage.SEVERITY_ERROR, "Invalid user name and/or password.", null));
        }
    }
    
    private AuthenticationStatus executeUserAuthentication()
    {
        return _securityContext.authenticate(
            (HttpServletRequest) _externalContext.getRequest(),
            (HttpServletResponse) _externalContext.getResponse(),
            AuthenticationParameters.withParams().credential(
                new UsernamePasswordCredential(_userName, _password))
        );
    }   
}

Vấn đề là, sau khi đăng nhập thành công (tên người dùng và mật khẩu chính xác), như bạn đã thấy trong các nhận xét trong lớp Đăng nhập ở trên, phương thức này executeUserAuthentication()sẽ trả về AuthenticationStatus.SEND_CONTINUEthay vì AuthenticationStatus.SUCCESS. Dưới đây là hình ảnh của ứng dụng đang chạy ở chế độ gỡ lỗi trong thời điểm thực thi để hiển thị điều này:

Do đó, thanh địa chỉ của trình duyệt không được cập nhật với url thực ( start.xhtml ) vì AuthenticationStatus.SUCCESSnhánh trong mã trên không được thực thi:

Tôi nghĩ rằng tôi đã làm sai điều gì đó, nhưng sau đó tôi quyết định sao chép ứng dụng đơn giản này từ @ArjanTijms sử dụng cùng một logic xác thực:

https://github.com/rieckpil/blog-tutorials/tree/master/jsf-simple-login-with-java-ee-security-api

Ứng dụng của anh ấy được giải thích trong bài đăng này:

https://rieckpil.de/howto-simple-form-based-authentication-for-jsf-2-3-with-java-ee-8-security-api/

Và kết quả cũng giống như ứng dụng của tôi: AuthenticationStatus.SEND_CONTINUEnhánh được thực thi thay vì AuthenticationStatus.SUCCESSnhánh:

Cũng không có thanh địa chỉ cập nhật:

Vậy, chuyện gì đang xảy ra ở đây? Đây có phải là một vấn đề trong các ứng dụng, Wildfly, đây có phải là hành vi chính xác không (nếu vậy, AuthenticationStatus.SUCCESSenum sử dụng là gì , khi nào nó sẽ được sử dụng?)? Tôi chỉ muốn có thể tự mình thực hiện chuyển hướng.

CẬP NHẬT 1

Đối với thông tin của bạn, sau khi executeUserAuthentication()người dùng đã được xác thực: ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getUserPrincipal()không rỗng.

Một số tệp khác trong ứng dụng:

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
    <display-name>autenticacao-visao</display-name>

    <!-- ========== JSF ========== -->

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    
    <context-param>
        <param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
        <param-value>true</param-value>
    </context-param>
    
    <!-- ========== Security ========== -->
    
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>restrict</web-resource-name>
            <url-pattern>/app/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>USER</role-name>
        </auth-constraint>
    </security-constraint>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>allowed</web-resource-name>
            <url-pattern>/app/resources/*</url-pattern>
        </web-resource-collection>
    </security-constraint>

    <security-role>
        <role-name>USER</role-name>
    </security-role>

    <!-- ========== Resources ========== -->
    
    <context-param>
        <param-name>dev.br.RESOURCES</param-name>
        <param-value>resources</param-value>
    </context-param>

    <!-- ========== Start page ========== -->
  
    <welcome-file-list>
        <welcome-file>app/start.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

jboss-app.xml

<?xml version="1.0" encoding="UTF-8"?>
<jboss-app>
    <security-domain>jaspitest</security-domain>
</jboss-app>

start.xhtml

<ui:composition template="/templates/layout.xhtml"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html">

    <ui:define name="title">
        Início
    </ui:define>
    
    <ui:define name="content">
        #{start.loggedInMessage}
        <br />
        <h:commandButton value="Execute" />
    </ui:define>
</ui:composition>

Start.java

package br.dev.authentication.view;

import java.security.Principal;

import javax.enterprise.context.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;

@RequestScoped
@Named
public class Start
{
    @Inject
    private ExternalContext _externalContext;
    
    public String getLoggedInMessage()
    {
        Principal user = ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getUserPrincipal();
        if (user != null)
        {
            return "Logged in: " + user.getName();
        }
        else
        {
            return "Not logged in";
        }
    }
}

CẬP NHẬT 2

Sau nhiều lần kiểm tra, tôi nhận thấy (ít nhất là trong ứng dụng của tôi) đôi khi SecurityContext.authenticate()trả về AuthenticationStatus.SEND_CONTINUEvà đôi khi quay lại AuthenticationStatus.SUCCESS. Đến bây giờ tôi vẫn chưa tìm ra nguyên nhân tại sao.

Vì vậy, hiện tại, tôi quyết định giải quyết vấn đề bằng cách hack cho những trường hợp AuthenticationStatus.SEND_CONTINUEsẽ được trả lại. Vì vậy, về cơ bản, những gì tôi đang làm là chuyển hướng đến trang bắt đầu theo cách thủ công. Dưới đây là bản hack:

web.xml (đã thay đổi trang chào mừng thành redirectortostart.xhtml )

<welcome-file-list>
    <welcome-file>app/redirectortostart.xhtml</welcome-file>
</welcome-file-list>

redirectortostart.xhtml

<ui:composition template="/templates/layout.xhtml"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:f="http://xmlns.jcp.org/jsf/core">

    <ui:define name="metadados">
        <f:event type="preRenderView" listener="#{redirectorToStart.goToStart}" />
    </ui:define>
</ui:composition>

RedirectorToStart.java

package br.dev.authentication.view;

import java.io.IOException;

import javax.enterprise.context.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.inject.Inject;
import javax.inject.Named;

@RequestScoped
@Named
public class RedirectorToStart
{
    @Inject
    private ExternalContext _externalContext;

    public void goToStart() throws IOException
    {
        _externalContext.redirect(_externalContext.getRequestContextPath() + "/app/start.xhtml");
    }
}

Nó không thanh lịch, nhưng hiện tại nó giải quyết được vấn đề của tôi. Hy vọng một ngày nào đó một số bạn có manh mối về những gì thực sự có thể xảy ra với ứng dụng của tôi và có thể đưa ra gợi ý cho tôi.

Trả lời

LuisGalaz Nov 09 2020 at 14:15

AuthenticationStatus được sử dụng như một giá trị trả về chủ yếu bởi HttpAuthenticationMechanism để chỉ ra kết quả (trạng thái) của quá trình xác thực.

SEND_CONTINUE: Cơ chế xác thực đã được gọi và hộp thoại xác thực nhiều bước với người gọi đã được bắt đầu (ví dụ: người gọi đã được chuyển hướng đến trang đăng nhập). Nói đơn giản là xác thực là "đang được tiến hành". Mã ứng dụng gọi điện (nếu có) không được ghi vào phản hồi khi nhận được trạng thái này. Javadocs

Web.xml của bạn trông như thế nào?

bạn đã thêm các ràng buộc bảo mật chưa?

Thí dụ :

  <security-constraint>
        <web-resource-collection>
            <web-resource-name>Application pages</web-resource-name>
            <url-pattern>/app/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>ADMIN</role-name>
            <role-name>USER</role-name>
        </auth-constraint>
    </security-constraint>

    <security-role>
        <role-name>USER</role-name>
    </security-role>
    <security-role>
        <role-name>ADMIN</role-name>
    </security-role>

bạn có jboss-web.xml không? trong src / main / webapp / WEB-INF

jaspitest là miền bảo mật để sử dụng Giao diện cơ chế xác thực HTTP trong Wildfly / Jboss

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web version="8.0" xmlns="http://www.jboss.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/schema/jbossas/jboss-web_8_0.xsd">
  <context-root/>
  <security-domain>jaspitest</security-domain>
</jboss-web>

Tôi vừa thử nghiệm ứng dụng mà bạn đã liên kết và hoạt động như mong đợi

bạn có thể định cấu hình thời gian chờ của phiên (tính bằng phút) trên web.xml

<session-config>
    <session-timeout>
        1
    </session-timeout>
    <cookie-config>
        <http-only>true</http-only>
        <secure>false</secure>
    </cookie-config>
</session-config>