使用 Java EE Security,我正在一个简单的应用程序中尝试自定义表单身份验证。
这些是相关文件(问题的描述位于文件下方):
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;
}
}
}
登录.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>
登录.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
。下面是在执行时以调试模式运行的应用程序的图像,以显示这一点:
结果,浏览器地址栏没有更新为真实的url(start.xhtml),因为上面代码中的
AuthenticationStatus.SUCCESS
分支没有被执行:
我以为我做错了什么,但后来我决定从 @ArjanTijms 克隆这个使用相同身份验证逻辑的简单应用程序:
https://github.com/rieckpil/blog-tutorials/tree/master/jsf-simple-login-with-java-ee-security-api
他的申请在这篇文章中有解释:
https://rieckpil.de/howto-simple-form-based-authentication-for-jsf-2-3-with-java-ee-8-security-api/
结果与我的应用程序相同:执行了
AuthenticationStatus.SEND_CONTINUE
分支而不是 AuthenticationStatus.SUCCESS
分支:
地址栏也没有更新:
那么,这是怎么回事?这是应用程序中的问题吗,Wildfly,这是正确的行为吗(如果是这样,
AuthenticationStatus.SUCCESS
枚举有什么用,什么时候会使用它?)?我只是想能够自己执行重定向。
更新1
供您参考,在
executeUserAuthentication()
之后,用户已通过身份验证:((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).getUserPrincipal()
不为空。
应用程序中的一些其他文件:
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
。到现在我还没弄清楚原因。
所以,我暂时决定通过 hack 来解决
AuthenticationStatus.SEND_CONTINUE
被返回的情况。所以,基本上,我所做的就是手动重定向到 start 页面。黑客如下:
web.xml(将欢迎页面更改为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");
}
}
这并不优雅,但暂时解决了我的问题。希望有一天你们中的一些人知道我的应用程序到底会发生什么,并可以给我一些提示。
AuthenticationStatus 主要由 HttpAuthenticationMechanism 用作返回值,以指示身份验证过程的结果(状态)。
SEND_CONTINUE :调用了身份验证机制,并启动了与调用者的多步骤身份验证对话(例如,调用者已重定向到登录页面)。简单地说身份验证“正在进行中”。收到此状态时,调用应用程序代码(如果有)不应写入响应。 Java文档
你的 web.xml 是什么样的?
您添加了安全限制吗?
示例:
<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 中
jaspitest是Wildfly/Jboss中使用HTTP认证机制接口的安全域
<?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>
我刚刚测试了您链接的应用程序并按预期工作
您可以在 web.xml 上配置会话超时(以分钟为单位)
<session-config>
<session-timeout>
1
</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>false</secure>
</cookie-config>
</session-config>
我在 Payara 6 上的 Jakarta EE 10 应用程序中也有完全相同的情况,但仍然不明白何时以及为什么会收到 AuthenticationStatus.SEND_CONTINUE...我只注意到注销后的一件事
ExternalContext ec = fc.getExternalContext(); ((HttpServletRequest) ec.getRequest()).logout();
新的登录请求带有 AuthenticationStatus.SUCCESS