本文介绍了成功登录后,SecurityContext.authenticate()返回AuthenticationStatus.SEND_CONTINUE而不是AuthenticationStatus.SUCCESS的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

  • 雅加达EE 8
  • Wildfly 21
  • Java 11

使用 Java EE安全性,我正在尝试在一个简单的应用程序中进行自定义表单身份验证.

Using Java EE Security, I'm trying custom form authentication in a simple application.

以下是相关文件(问题的描述在文件下面):

These are the relevant files (the description of the problem is below the files):

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))
        );
    }   
}

问题在于,成功登录后(用户名和密码正确),如您在上面的 Login 类中的注释中所看到的,方法executeUserAuthentication()返回AuthenticationStatus.SEND_CONTINUE而不是AuthenticationStatus.SUCCESS.下面是在执行时正在调试模式下运行的应用程序的图像,以显示此信息:

The problem is that, after a successful login (user name and password correct), as you saw in the comments in Login class above, the method executeUserAuthentication() returns AuthenticationStatus.SEND_CONTINUE instead of AuthenticationStatus.SUCCESS. Below is an image of the application running in debug mode in the moment of execution to show this:

结果是,浏览器地址栏未使用真实网址( start.xhtml )更新,因为未执行上述代码中的AuthenticationStatus.SUCCESS分支:

As a result, the browser address bar is not updated with the real url (start.xhtml) because the AuthenticationStatus.SUCCESS branch in the code above was not executed:

我以为我做错了事,但是后来我决定从@ArjanTijms克隆一个使用相同身份验证逻辑的简单应用程序:

I thought I was doing something wrong, but then I decided to clone this simple application from @ArjanTijms that uses the same authentication logic:

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

他的申请在这篇文章中进行了解释:

His application is explained in this post:

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

结果与我的应用程序相同:执行AuthenticationStatus.SEND_CONTINUE分支而不是AuthenticationStatus.SUCCESS分支:

And the results were the same of my application: the AuthenticationStatus.SEND_CONTINUE branch was executed instead of the AuthenticationStatus.SUCCESS branch:

也没有更新的地址栏:

那么,这是怎么回事?这是Wildfly应用程序中的问题吗,这是正确的行为吗(如果是这样,AuthenticationStatus.SUCCESS枚举的用途是什么,何时使用?)?我只想能够自己执行重定向.

So, what's going on here? Is this a problem in the applications, Wildfly, is this correct behaviour (if so, what's the use of the AuthenticationStatus.SUCCESS enum, when will it be used?)? I just want to be able to execute the redirect by myself.

更新1

为供参考,executeUserAuthentication()用户已经通过身份验证:((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getUserPrincipal()不为空.

For your information, after the executeUserAuthentication() the user is already authenticated: ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getUserPrincipal() is not null.

应用程序中的其他一些文件:

Some other files in the application:

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";
        }
    }
}

更新2

经过大量测试,我注意到(至少在我的应用程序中)有时SecurityContext.authenticate()返回AuthenticationStatus.SEND_CONTINUE,有时返回AuthenticationStatus.SUCCESS.到目前为止,我还没有找到原因.

After lots of tests, I noticed (at least in my application) that sometimes SecurityContext.authenticate() returns AuthenticationStatus.SEND_CONTINUE and sometimes returns AuthenticationStatus.SUCCESS. Until now, I haven't figured out the reason why.

因此,暂时,我决定通过黑客破解来解决AuthenticationStatus.SEND_CONTINUE将要返回的情况.因此,基本上,我正在做的是手动重定向到 start 页面. hack如下:

So, for the time being, I decided to solve the problem with a hack for the cases when AuthenticationStatus.SEND_CONTINUE would be returned. So, basically, what I'm doing is redirecting to the start page manually. The hack is below:

web.xml (将欢迎页面更改为 redirectortostart.xhtml )

web.xml (changed the welcome page to 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");
    }
}

它并不优雅,但暂时可以解决我的问题.希望有一天你们中的一些人对我的应用程序可能真正发生的事情有所了解,并能给我一些提示.

It's not elegant, but for the time being it solves my problem. Hope someday some of you have a clue about what really could be happening with my application and could give a hint to me.

推荐答案

HttpStaticMechanism主要将AuthenticationStatus用作返回值,以指示认证过程的结果(状态).

The AuthenticationStatus is used as a return value by primarily the HttpAuthenticationMechanism to indicate the result (status) of the authentication process.

SEND_CONTINUE:已调用身份验证机制,并且已启动与调用方的多步身份验证对话框(例如,已将调用方重定向到登录页面).简单地说,认证是进行中".收到此状态时,不应将调用应用程序代码(如果有)写入响应. Javadocs

SEND_CONTINUE : The authentication mechanism was called and a multi-step authentication dialog with the caller has been started (for instance, the caller has been redirected to a login page). Simply said authentication is "in progress". Calling application code (if any) should not write to the response when this status is received.Javadocs

您的web.xml看起来如何?

How does your web.xml looks like?

您添加了安全限制吗?

示例:

  <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>

您是否拥有jboss-web.xml?在src/main/webapp/WEB-INF

have you the jboss-web.xml? in the src/main/webapp/WEB-INF

jaspitest是在Wildfly/Jboss中使用HTTP身份验证机制接口的安全域

jaspitest is the security domain to use HTTP Authentication Mechanism Interface in 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>

我刚刚测试了您链接并按预期工作的应用程序

I just tested the application that you linked and work as Expected

您可以在web.xml上配置会话超时(以分钟为单位)

you can configure the session timeout (in minutes) on the web.xml

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

这篇关于成功登录后,SecurityContext.authenticate()返回AuthenticationStatus.SEND_CONTINUE而不是AuthenticationStatus.SUCCESS的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

10-25 00:36