# 基于 Glassfish 表单的身份验证示例 > 原文: [https://javatutorial.net/glassfish-form-based-authentication-example](https://javatutorial.net/glassfish-form-based-authentication-example) 在本教程中,我将向您展示如何使用内置的 Glassfish 身份验证机制通过用户登录创建基于 Web 的应用程序。 ![](img/a9a02b91242a5747befea43ffbcac8ca.jpg) 如今,身份验证已广泛用于所有类型的 Web 应用程序中。 从网上商店,低谷社交媒体和要求用户登录的其他服务。Glassfish 带有集成的身份验证和授权机制,可让用户登录并维护应用程序中的会话。 我们将利用此功能并构建和应用程序,以允许用户注册和登录以访问授权的内容。 最后,我将向您展示如何创建注销功能以允许用户终止会话。 这是一个完整的 Java EE 教程,它要求 DB 设置,Glassfish 配置,身份验证后端逻辑的开发以及创建前端部分。 我将使用 Glassfish 4,MySQL 数据库和 JSF 2.2 来构建前端。 该代码也可以在 Payara 4 Server 和其他数据库(MSSQL,Oracle 等)上运行,而无需进行任何修改。 ## 项目描述 我们将创建一个简单的 Web 应用程序,其中包含一个注册表单,一个登录表单和一个私人页面。 用户登录后即可访问该私人页面。 表单中的所有字段都将根据错误的用户输入进行验证。 ## 创建数据库表 在继续之前,请确保已使用数据库配置了 Glassfish 服务器。 如果尚未执行此操作,请先阅读[如何使用 MySQL 配置 Glassfish 4](https://javatutorial.net/configure-glassfish-mysql)。 当然,您可以使用您喜欢的任何其他数据库来学习本教程。 您不仅限于 MySQL。 您可以在多个位置存储用户凭据,例如数据库,文件,LDAP 等。在本教程中,我们将使用数据库存储用户名和密码。 在 Glassfish 中,这称为 JDBC 安全领域。 创建安全领域需要两张表 - 第一个用于存储用户凭据,第二个用于将特定用户映射到角色。 Glassfish 并未预定义角色,这意味着我们可以创建自己的角色 - 管理员,用户,主持人,查看者等。为了使本教程尽可能简短,我将只实现一个角色:用户 第一个表称为`users`,具有以下字段: * `email` – 唯一的主要字段。 我们将使用用户的电子邮件地址作为用户名,如果您愿意,可以使用简单的旧用户名 * `password` – SHA-256 编码的用户密码。 切勿将用户密码存储为纯文本格式 - 不好😉 * `name` – 用户名 Glassfishe 的安全领域只有用户名和密码字段为必填项。 我在此处添加了名称,以说明您可以在同一表单中为用户添加其他信息 ```java CREATE TABLE `users` ( `email` varchar(255) NOT NULL, `password` varchar(64) NOT NULL, `name` varchar(30) NOT NULL, PRIMARY KEY (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` `user_groups`表具有 2 个必填字段: * `email` - 这是用户名 * `groupname` – 存储用户的角色,例如管理员,用户,主持人,查看者等。 ```java CREATE TABLE `user_groups` ( `email` VARCHAR(255) NOT NULL, `groupname` VARCHAR(32) NOT NULL, PRIMARY KEY (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ## 项目结构 在开始实现项目的后端部分之前,我想向您展示项目结构。 这样,您将知道所有文件所在的位置 ![](img/7a9e5e0c8bc0b6c412ed33d495753d4d.jpg) 您可以在 GitHub 上找到完整的源代码,网址为 [https://github.com/JavaTutorialNetwork/Tutorials/tree/master/GlassfishFormBasedAuthentication](https://github.com/JavaTutorialNetwork/Tutorials/tree/master/GlassfishFormBasedAuthentication) ## 项目依赖关系(`pom.xml`文件) 这是纯 Java EE 7 的实现,除了 Java 企业 API 本身之外,我们在项目中不需要其他依赖项。 该 API 已包含在 Glassfish 中,因此您应将依赖项标记为`provided` ```java 4.0.0 net.javatutorial.tutorials GlassfishFormBasedAuthentication 1 war UTF-8 javax javaee-api 7.0 provided authexample org.apache.maven.plugins maven-war-plugin 2.3 src/main/webapp/WEB-INF/web.xml org.apache.maven.plugins maven-compiler-plugin 3.1 1.8 1.8 ``` ## 用户和组实体 在数据库中添加表`users`和`user_groups`后,必须为这两个表创建 JPA 实体。 实体会将数据库表映射到 Java 对象,因此您可以使用实体对象中的 getter 和 setter 方法轻松地修改或获取数据库。 使用`@Entity`注解将 Java 类标记为实体。 该类还必须实现`Serializable`接口。 这是**用户实体**的代码 ```java package net.javatutorial.tutorials.gfauthexample.entity; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; @Entity @NamedQueries({ @NamedQuery(name = "findUserById", query = "SELECT u FROM User u WHERE u.email = :email") }) @Table(name="users") public class User implements Serializable { private static final long serialVersionUID = -5892169641074303723L; @Id @Column(name="email", nullable=false, length=255) private String email; @Column(name="password", nullable=false, length=64) private String password; @Column(name="name", nullable=false, length=30) private String name; public User(){} public User(String email, String password, String name) { this.email = email; this.password = password; this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ``` 在第 14 行,定义了`NamedQuery "findUserById"`。 我们稍后将在项目中使用此查询来: * 验证注册期间提供的电子邮件是否尚未用于其他注册 * 登录后显示用户名和电子邮件 在第 11 行,用户实体通过`@Table`注释映射到数据库表`users` 在第 18 行,`@Id`注释用于将“电子邮件”表示为主字段 `@Column`注解用于将字段从 Java 类映射到表中的字段 **团体实体** ```java package net.javatutorial.tutorials.gfauthexample.entity; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name="user_groups") public class Group implements Serializable { private static final long serialVersionUID = 1528447384986169065L; public static final String USERS_GROUP = "users"; @Id @Column(name="email", nullable=false, length=255) private String email; @Column(name="groupname", nullable=false, length=32) private String groupname; public Group() {} public Group(String email, String groupname) { this.email = email; this.groupname = groupname; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getGroupname() { return groupname; } public void setGroupname(String groupname) { this.groupname = groupname; } } ``` 字段`groupname`用于给定用户角色。 该表是一个映射表,用户 ID 映射到某个角色。 为了使此示例简单,在本教程中创建的所有帐户都将具有“用户”角色。 您可以根据需要添加更多角色。 角色不是预定义的,您可以随意命名。 角色之间的区别在于实现。 例如,如果您具有“管理员”角色,则将要赋予它比“用户”更多的特权。 您必须以编程方式限制一个角色,并以编程方式为另一个角色提供更多选择,并在您自己的代码中进行管理。 ## `persistence.xml`文件 ```java org.eclipse.persistence.jpa.PersistenceProvider jdbc/tutorialsDS net.javatutorial.tutorials.gfauthexample.entity.User net.javatutorial.tutorials.gfauthexample.entity.Group ``` 在此文件中,我们指定持久层属性。 首先,我们创建一个命名的持久性单元`tutorialsPU`。 我们将在 EJB 中使用此持久性单元为`EntityManager`创建隔离的`PersistenceContext`。 其次,我们指定将用于此持久性单元的数据源。 数据源在 Glassfish 管理控制台中配置(有关更多详细信息,请阅读[如何使用 MySQL 配置 Glassfish 4](https://javatutorial.net/configure-glassfish-mysql))。 第三,我们列出要包含在持久性单元中的实体类。 在我们的例子中,这是我们在上一步中创建的`User`和`Group`类。 我还包括了两个其他有用的属性。 您可以根据需要将其注释掉: 如果数据库表不存在,此属性将生成数据库表 ```java ``` 部署应用程序时,此属性将擦除现有表,并根据您的实体类创建新的(空)表。 您可以将其用于开发 ```java ``` ## 编写业务层 – `UserEJB` 在这一步中,我们将编写一个 Enterprise Java Bean(EJB),以在数据库中插入新用户并对其进行查询。 EJB 通常用作持久层(数据库实体)和表示层(托管 Bean 和 JSF 页面)之间的中介。 ```java package net.javatutorial.tutorials.gfauthexample.ejb; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import net.javatutorial.tutorials.gfauthexample.entity.Group; import net.javatutorial.tutorials.gfauthexample.entity.User; import net.javatutorial.tutorials.gfauthexample.utils.AuthenticationUtils; @Stateless public class UserEJB { @PersistenceContext(unitName="tutorialsPU") private EntityManager em; public User createUser(User user) { try { user.setPassword(AuthenticationUtils.encodeSHA256(user.getPassword())); } catch (Exception e) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, e); e.printStackTrace(); } Group group = new Group(); group.setEmail(user.getEmail()); group.setGroupname(Group.USERS_GROUP); em.persist(user); em.persist(group); return user; } public User findUserById(String id) { TypedQuery query = em.createNamedQuery("findUserById", User.class); query.setParameter("email", id); User user = null; try { user = query.getSingleResult(); } catch (Exception e) { // getSingleResult throws NoResultException in case there is no user in DB // ignore exception and return NULL for user instead } return user; } } ``` 请注意,第 16 行的 Bean 被注释为`@Stateless`。 在第 19 行,`EntityManager`指向`"tutorialsPU"`作为持久性单元,全部通过`@PersistenceContext`注解完成。 我们在`persistence.xml`文件中指定了持久性单元 `createUser`方法首先将纯文本密码编码为 SHA-256 字符串,然后将两个对象(用户和组)存储到数据库中。 如果在数据库中找不到该用户,则`findUserById`方法将返回一个`User`对象或`null` 请注意此处`try-catch`的用法。 如果未找到具有给定 ID(电子邮件)的用户,则对`query.getSingleResult()`的调用将引发`NoResultException`。 我们将忽略该异常,而是返回一个`null`用户。 稍后,我们将在托管 Bean 中处理`null`。 实用程序将密码编码为 SHA-256 的方法 ```java package net.javatutorial.tutorials.gfauthexample.utils; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.xml.bind.DatatypeConverter; public final class AuthenticationUtils { /** * Returns SHA-256 encoded string * @param password - the string to be encoded * @return SHA-256 encoded string * @throws UnsupportedEncodingException if UTF-8 is not supported by the system * @throws NoSuchAlgorithmException if SHA-256 is not supported by the system */ public static String encodeSHA256(String password) throws UnsupportedEncodingException, NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(password.getBytes("UTF-8")); byte[] digest = md.digest(); return DatatypeConverter.printBase64Binary(digest).toString(); } } ``` ## 配置 Glassfish 安全领域 我们需要在 Glassfish 中创建和配置一个新的 JDBC 安全领域。 在`localhost:4848`打开 Glassfish 管理控制台。 打开“配置 -> 服务器配置 -> 安全 -> 领域”,然后单击“新建…”按钮以创建新的安全领域。 ![Glassfish server configuration security realms](img/08e1e625a22c339aab716879c3da9e95.jpg) Glassfish 服务器配置安全领域 ![Create new JDBC security realm](img/d3712488e8b862c4928b28f0c919e95f.jpg) 创建新的 JDBC 安全领域 填写以下字段: * `Name ` – `jdbc-realm` * `Class Name` – `com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm` * `JAAS Context` – `jdbcRealm` * `JNDI` – `jdbc/tutorialsDS` * `User Table` – 用户 * `User Name Column` – 电子邮件 * `Password Column` - 密码 * `Group Table` – user_groups * `Group Table User Name Column` – 电子邮件 * `Group Name Column` – 组名 * `Password Encryption Algorithm` - 无(我们通过编程方式对密码进行加密) * `Digest Algorithm` – SHA-256 * `Encoding` – Base64 ## `glassfish-web.xml`文件 ```java users users Keep a copy of the generated servlet class' java code. ``` 我们必须在`glassfish-web.xml`文件中指定`security-role-mapping`,并添加角色名称和组名称 ## `web.xml`文件 ```java Form Based Auth Example javax.faces.PROJECT_STAGE Development javax.faces.DEFAULT_SUFFIX .xhtml Faces Servlet javax.faces.webapp.FacesServlet 1 Faces Servlet *.xhtml 300 signin.xhtml FORM jdbc-realm /signin.xhtml /signin.xhtml users Restricted to users Restricted Access /user/* users NONE ``` `web.xml`部署描述符也需要进行一些更改。 我们将在此处配置安全规则。 首先,我们配置会话超时。 这是使用户会话保持活动状态的时间。 当会话超时时,用户不必再次登录。 该值以分钟为单位。 300 表示用户会话将持续 5 个小时。 其次,我们将`welcome-file`设置为`signin.xhtml`。 当用户打开应用程序时,将显示登录屏幕。 在``中设置以下内容: * `auth-method`是`FORM` – 用户使用 Web 表单输入用户名和密码 * `realm-name`是`jdbc-realm` – 这是我们在上一步中配置的安全领域的名称 * `form-login-page`和`form-error-page`是带有登录表单的 xhtml 文件和在发生登录错误时显示的 xhtml 文件(我们都指向`signin.xhtml`,因为在此文件中进行了错误处理) 我们只有一个安全角色–用户 在``标签中,我们为特定用户角色定义了受限资源。 换句话说 – `user`目录中的每个文件都需要登录才能访问。 我们将在其中放置私有 XHTML 文件。 ## 创建展示层 ![Registration form](img/0c3808c455e96f9b6cc17ae1a64b8184.jpg) 注册表单 ### 注册表单页面 在注册表单中,我们要求提供用户名,电子邮件地址和密码。 我们还要求用户确认密码。 我们将验证电子邮件,密码和确认密码字段中的用户输入。 首先让我们看一下 JSF XHTML 文件和`ManagedBean` ```java register

Create new account

Name: E-Mail: Password: Confirm password:


``` 注册视图 ```java package net.javatutorial.tutorials.gfauthexample.managedbeans; import java.io.Serializable; import java.util.logging.Logger; import javax.faces.application.FacesMessage; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.context.FacesContext; import javax.faces.event.ComponentSystemEvent; import javax.inject.Inject; import net.javatutorial.tutorials.gfauthexample.ejb.UserEJB; import net.javatutorial.tutorials.gfauthexample.entity.User; @ManagedBean @SessionScoped public class RegisterView implements Serializable { private static final long serialVersionUID = 1685823449195612778L; private static Logger log = Logger.getLogger(RegisterView.class.getName()); @Inject private UserEJB userEJB; private String name; private String email; private String password; private String confirmPassword; public void validatePassword(ComponentSystemEvent event) { FacesContext facesContext = FacesContext.getCurrentInstance(); UIComponent components = event.getComponent(); // get password UIInput uiInputPassword = (UIInput) components.findComponent("password"); String password = uiInputPassword.getLocalValue() == null ? "" : uiInputPassword.getLocalValue().toString(); String passwordId = uiInputPassword.getClientId(); // get confirm password UIInput uiInputConfirmPassword = (UIInput) components.findComponent("confirmpassword"); String confirmPassword = uiInputConfirmPassword.getLocalValue() == null ? "" : uiInputConfirmPassword.getLocalValue().toString(); // Let required="true" do its job. if (password.isEmpty() || confirmPassword.isEmpty()) { return; } if (!password.equals(confirmPassword)) { FacesMessage msg = new FacesMessage("Confirm password does not match password"); msg.setSeverity(FacesMessage.SEVERITY_ERROR); facesContext.addMessage(passwordId, msg); facesContext.renderResponse(); } if (userEJB.findUserById(email) != null) { FacesMessage msg = new FacesMessage("User with this e-mail already exists"); msg.setSeverity(FacesMessage.SEVERITY_ERROR); facesContext.addMessage(passwordId, msg); facesContext.renderResponse(); } } public String register() { User user = new User(email, password, name); userEJB.createUser(user); log.info("New user created with e-mail: " + email + " and name: " + name); return "regdone"; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getConfirmPassword() { return confirmPassword; } public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; } } ``` ### 用户输入验证 验证规则: * 所有字段均为必填项(不允许使用任何空字段) * 合法的邮件地址 * 密码必须至少包含 1 个小写字符,1 个大写字符,1 个数字 * 密码长度必须在 6 到 20 个字符之间 * 确认密码必须等于密码 * 用户电子邮件应该是唯一的(不用于其他注册) ![User input form validation](img/0df8a4058f1876c8fda83831a5785af2.jpg) 用户输入表单验证 有 3 种可能的方法来验证 JSF 2 中的字段。我将演示全部 3 种方法。 **使用`JSFvalidateRegex`** 我们使用此方法验证密码字段。 我们要做的就是插入具有给定正则表达式模式的`f:validateRegex`标签。 如果您想了解更多有关正则表达式的信息,请阅读[基本 Java 正则表达式](https://javatutorial.net/basic-java-regular-expressions)教程 ```java ``` 我们还在`validatorMessage`属性内的 JSF 文件中指定了错误消息 **在系统事件(`PostValidateEvent`)中验证** 确认密码字段通过名为`PostValidateEvent`的系统事件进行验证。 我们在 XHTML 文件中注册了一个新事件: ```java ``` 用户单击“注册”按钮时,将触发`RegisterView` bean 中的`public void validatePassword(ComponentSystemEvent event)`方法 **使用自定义验证程序类** 对于电子邮件验证,我们使用一个自定义类,该类实现`javax.faces.validator.Validator`接口 `Validator`类必须实现一个称为`validate`的方法 ```java public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException ``` 在此方法中,我们根据正则表达式模式验证用户输入,如果该模式不正确,我们将抛出`ValidatorException` ```java package net.javatutorial.tutorials.gfauthexample.validators; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.validator.FacesValidator; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; @FacesValidator("emailValidator") public class EmailValidator implements Validator { private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-]+(\\." + "[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*" + "(\\.[A-Za-z]{2,})$"; private Pattern pattern; private Matcher matcher; public EmailValidator() { pattern = Pattern.compile(EMAIL_PATTERN); } @Override public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { matcher = pattern.matcher(value.toString()); if (!matcher.matches()) { throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, "Invalid e-mail address", null)); } } } ``` 我们必须使用以下行在 XHTML 文件中包含给定字段的自定义验证器 ```java ``` **检查电子邮件是否已在另一个注册中使用** 为此,我们不需要验证器。 您可以在`ManagedBean` – `RegisterView`中使用类似这样的简单检查 ```java if (userEJB.findUserById(email) != null) { // email is already used } ``` ## 成功注册 如果用户在注册表单中输入正确的数据,则将创建一个新的`User`对象并将其插入数据库中。 ```java User user = new User(email, password, name); userEJB.createUser(user); ``` 请注意,我们还在`UserEJB#createUser`方法内为该用户创建了`Group`对象 ```java Group group = new Group(); group.setEmail(user.getEmail()); group.setGroupname(Group.USERS_GROUP); em.persist(user); em.persist(group); ``` 数据库条目如下所示: ![entry in users table](img/5604ab619c507de3be5dcd9673c3a268.jpg) 用户表中的条目 ![entry in user groups table](img/8d75b1f616efb2cfe518a6f633ef6b70.jpg) 用户组表中的条目 最后,用户被重定向到`regdone.xhtml`页面 ![regdone.xhtml page](img/fe927bbd2e5fd40b41061cb0f3a2911a.jpg) `regdone.xhtml`页面 ```java Registration done

Your account has been successfully created



``` ## 登录表单 在此表单中,我们需要用户的电子邮件地址和密码。 表单针对空白字段进行了验证。 在`LoginView`中检查用户名和密码的有效性。 ![Login form](img/5528420612ad56473a4881c1a3c8f87d.jpg) 登录表单 如果用户输入了有效的登录数据,则会创建一个新的`java.security.Principal`对象。 我们还将用户添加到`externalContext`的会话地图中 ```java ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext(); Map sessionMap = externalContext.getSessionMap(); sessionMap.put("User", user); ``` `signin.xhtml`文件 ```java login

Please Sign In

E-Mail: Password:


``` `LoginView ManagedBean` ```java package net.javatutorial.tutorials.gfauthexample.managedbeans; import java.io.Serializable; import java.security.Principal; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.faces.application.FacesMessage; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.inject.Inject; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import net.javatutorial.tutorials.gfauthexample.ejb.UserEJB; import net.javatutorial.tutorials.gfauthexample.entity.User; @ManagedBean @SessionScoped public class LoginView implements Serializable { private static final long serialVersionUID = 3254181235309041386L; private static Logger log = Logger.getLogger(LoginView.class.getName()); @Inject private UserEJB userEJB; private String email; private String password; private User user; public String login() { FacesContext context = FacesContext.getCurrentInstance(); HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest(); try { request.login(email, password); } catch (ServletException e) { context.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Login failed!", null)); return "signin"; } Principal principal = request.getUserPrincipal(); this.user = userEJB.findUserById(principal.getName()); log.info("Authentication done for user: " + principal.getName()); ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext(); Map sessionMap = externalContext.getSessionMap(); sessionMap.put("User", user); if (request.isUserInRole("users")) { return "/user/privatepage?faces-redirect=true"; } else { return "signin"; } } public String logout() { FacesContext context = FacesContext.getCurrentInstance(); HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest(); try { this.user = null; request.logout(); // clear the session ((HttpSession) context.getExternalContext().getSession(false)).invalidate(); } catch (ServletException e) { log.log(Level.SEVERE, "Failed to logout user!", e); } return "/signin?faces-redirect=true"; } public User getAuthenticatedUser() { return user; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } ``` 最后,用户被重定向到`/user/privatepage.xhtml` ![Private page](img/d0e7a2adbe9a44239da7ff6b0f1413c3.jpg) 私人页面 ## 登出功能 注销很简单。 我们只需要执行注销请求并使会话无效 ```java public String logout() { FacesContext context = FacesContext.getCurrentInstance(); HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest(); try { this.user = null; request.logout(); // clear the session ((HttpSession) context.getExternalContext().getSession(false)).invalidate(); } catch (ServletException e) { log.log(Level.SEVERE, "Failed to logout user!", e); } return "/signin?faces-redirect=true"; } ``` ## 故障排除 对安全代码进行故障排除非常困难。 大多数组件不会引发有意义的异常。 这样做是故意进行的,以防止黑客在尝试破解应用程序的安全性时获得有用的信息。 不幸的是,我们也没有获得这些有用的信息。 如果发现一些问题,请尝试遵循例外。 两次检查您的安全领域配置,并检查所有密码编码是否正确。