124.md 34.3 KB
Newer Older
W
init  
wizardforcel 已提交
1 2
# 基于 Glassfish 表单的身份验证示例

W
wizardforcel 已提交
3
> 原文: [https://javatutorial.net/glassfish-form-based-authentication-example](https://javatutorial.net/glassfish-form-based-authentication-example)
W
init  
wizardforcel 已提交
4 5 6 7 8 9 10 11 12 13 14

在本教程中,我将向您展示如何使用内置的 Glassfish 身份验证机制通过用户登录创建基于 Web 的应用程序。

![](img/a9a02b91242a5747befea43ffbcac8ca.jpg)

如今,身份验证已广泛用于所有类型的 Web 应用程序中。 从网上商店,低谷社交媒体和要求用户登录的其他服务。Glassfish 带有集成的身份验证和授权机制,可让用户登录并维护应用程序中的会话。 我们将利用此功能并构建和应用程序,以允许用户注册和登录以访问授权的内容。 最后,我将向您展示如何创建注销功能以允许用户终止会话。

这是一个完整的 Java EE 教程,它要求 DB 设置,Glassfish 配置,身份验证后端逻辑的开发以及创建前端部分。 我将使用 Glassfish 4,MySQL 数据库和 JSF 2.2 来构建前端。 该代码也可以在 Payara 4 Server 和其他数据库(MSSQL,Oracle 等)上运行,而无需进行任何修改。

## 项目描述

W
wizardforcel 已提交
15
我们将创建一个简单的 Web 应用程序,其中包含一个注册表单,一个登录表单和一个私人页面。 用户登录后即可访问该私人页面。 表单中的所有字段都将根据错误的用户输入进行验证。
W
init  
wizardforcel 已提交
16 17 18

## 创建数据库表

W
wizardforcel 已提交
19
在继续之前,请确保已使用数据库配置了 Glassfish 服务器。 如果尚未执行此操作,请先阅读[如何使用 MySQL 配置 Glassfish 4](https://javatutorial.net/configure-glassfish-mysql)。 当然,您可以使用您喜欢的任何其他数据库来学习本教程。 您不仅限于 MySQL。
W
init  
wizardforcel 已提交
20

W
wizardforcel 已提交
21
您可以在多个位置存储用户凭据,例如数据库,文件,LDAP 等。在本教程中,我们将使用数据库存储用户名和密码。 在 Glassfish 中,这称为 JDBC 安全领域。 创建安全领域需要两张表 - 第一个用于存储用户凭据,第二个用于将特定用户映射到角色。 Glassfish 并未预定义角色,这意味着我们可以创建自己的角色 - 管理员,用户,主持人,查看者等。为了使本教程尽可能简短,我将只实现一个角色:用户
W
init  
wizardforcel 已提交
22 23 24

第一个表称为`users`,具有以下字段:

W
wizardforcel 已提交
25 26 27
*  `email` – 唯一的主要字段。 我们将使用用户的电子邮件地址作为用户名,如果您愿意,可以使用简单的旧用户名
*   `password` – SHA-256 编码的用户密码。 切勿将用户密码存储为纯文本格式 - 不好😉
*   `name` – 用户名
W
init  
wizardforcel 已提交
28

W
wizardforcel 已提交
29
Glassfishe 的安全领域只有用户名和密码字段为必填项。 我在此处添加了名称,以说明您可以在同一表单中为用户添加其他信息
W
init  
wizardforcel 已提交
30 31 32 33 34 35 36 37 38 39 40 41

```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 个必填字段:

W
wizardforcel 已提交
42 43
*   `email` - 这是用户名
*   `groupname` – 存储用户的角色,例如管理员,用户,主持人,查看者等。
W
init  
wizardforcel 已提交
44 45 46 47 48 49 50 51 52 53 54

```java
CREATE TABLE `user_groups` ( 
    `email` VARCHAR(255) NOT NULL, 
    `groupname` VARCHAR(32) NOT NULL, 
    PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

## 项目结构

W
wizardforcel 已提交
55
在开始实现项目的后端部分之前,我想向您展示项目结构。 这样,您将知道所有文件所在的位置
W
init  
wizardforcel 已提交
56 57 58 59 60

![](img/7a9e5e0c8bc0b6c412ed33d495753d4d.jpg)

您可以在 GitHub 上找到完整的源代码,网址为 [https://github.com/JavaTutorialNetwork/Tutorials/tree/master/GlassfishFormBasedAuthentication](https://github.com/JavaTutorialNetwork/Tutorials/tree/master/GlassfishFormBasedAuthentication)

W
wizardforcel 已提交
61
## 项目依赖关系(`pom.xml`文件)
W
init  
wizardforcel 已提交
62

W
wizardforcel 已提交
63
这是纯 Java EE 7 的实现,除了 Java 企业 API 本身之外,我们在项目中不需要其他依赖项。 该 API 已包含在 Glassfish 中,因此您应将依赖项标记为`provided`
W
init  
wizardforcel 已提交
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116

```java
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

	<modelVersion>4.0.0</modelVersion>

	<groupId>net.javatutorial.tutorials</groupId>
	<artifactId>GlassfishFormBasedAuthentication</artifactId>
	<version>1</version>
	<packaging>war</packaging>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>javax</groupId>
			<artifactId>javaee-api</artifactId>
			<version>7.0</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>

	<build>
		<finalName>authexample</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>2.3</version>
				<configuration>
					<webXml>src/main/webapp/WEB-INF/web.xml</webXml>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>
```

## 用户和组实体

W
wizardforcel 已提交
117
在数据库中添加表`users``user_groups`后,必须为这两个表创建 JPA 实体。 实体会将数据库表映射到 Java 对象,因此您可以使用实体对象中的 getter 和 setter 方法轻松地修改或获取数据库。 使用`@Entity`注解将 Java 类标记为实体。 该类还必须实现`Serializable`接口。
W
init  
wizardforcel 已提交
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186

这是**用户实体**的代码

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

```

W
wizardforcel 已提交
187
在第 14 行,定义了`NamedQuery "findUserById"`。 我们稍后将在项目中使用此查询来:
W
init  
wizardforcel 已提交
188 189 190 191

*   验证注册期间提供的电子邮件是否尚未用于其他注册
*   登录后显示用户名和电子邮件

W
wizardforcel 已提交
192
在第 11 行,用户实体通过`@Table`注释映射到数据库表`users`
W
init  
wizardforcel 已提交
193

W
wizardforcel 已提交
194
在第 18 行,`@Id`注释用于将“电子邮件”表示为主字段
W
init  
wizardforcel 已提交
195

W
wizardforcel 已提交
196
`@Column`注解用于将字段从 Java 类映射到表中的字段
W
init  
wizardforcel 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253

**团体实体**

```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 映射到某个角色。 为了使此示例简单,在本教程中创建的所有帐户都将具有“用户”角色。 您可以根据需要添加更多角色。 角色不是预定义的,您可以随意命名。 角色之间的区别在于实现。 例如,如果您具有“管理员”角色,则将要赋予它比“用户”更多的特权。 您必须以编程方式限制一个角色,并以编程方式为另一个角色提供更多选择,并在您自己的代码中进行管理。

W
wizardforcel 已提交
254
## `persistence.xml`文件
W
init  
wizardforcel 已提交
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285

```java
<?xml version="1.0" encoding="UTF-8"?>

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
	http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
	version="2.0">

	<persistence-unit name="tutorialsPU" transaction-type="JTA">
		<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
		<jta-data-source>jdbc/tutorialsDS</jta-data-source>
		<class>net.javatutorial.tutorials.gfauthexample.entity.User</class>
		<class>net.javatutorial.tutorials.gfauthexample.entity.Group</class>

		<properties>
			<!--  tables will be created only if they do not exist. Use for production 
			<property name="eclipselink.ddl-generation" value="create-tables"/>
			-->

			<!--  will first drop the existing table, and then create the new table. Use for development 
			<property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
			-->
			<property name="eclipselink.logging.level" value="INFO"/>
		</properties>
	</persistence-unit>

</persistence>
```

W
wizardforcel 已提交
286
在此文件中,我们指定持久层属性。 首先,我们创建一个命名的持久性单元`tutorialsPU`。 我们将在 EJB 中使用此持久性单元为`EntityManager`创建隔离的`PersistenceContext`。 其次,我们指定将用于此持久性单元的数据源。 数据源在 Glassfish 管理控制台中配置(有关更多详细信息,请阅读[如何使用 MySQL 配置 Glassfish 4](https://javatutorial.net/configure-glassfish-mysql))。 第三,我们列出要包含在持久性单元中的实体类。 在我们的例子中,这是我们在上一步中创建的`User``Group`类。
W
init  
wizardforcel 已提交
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301

我还包括了两个其他有用的属性。 您可以根据需要将其注释掉:

如果数据库表不存在,此属性将生成数据库表

```java
<property name="eclipselink.ddl-generation" value="create-tables"/>
```

部署应用程序时,此属性将擦除现有表,并根据您的实体类创建新的(空)表。 您可以将其用于开发

```java
<property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
```

W
wizardforcel 已提交
302
## 编写业务层 – `UserEJB`
W
init  
wizardforcel 已提交
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360

在这一步中,我们将编写一个 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<User> 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;
	}
}

```

W
wizardforcel 已提交
361
请注意,第 16 行的 Bean 被注释为`@Stateless`
W
init  
wizardforcel 已提交
362

W
wizardforcel 已提交
363
在第 19 行,`EntityManager`指向`"tutorialsPU"`作为持久性单元,全部通过`@PersistenceContext`注解完成。 我们在`persistence.xml`文件中指定了持久性单元
W
init  
wizardforcel 已提交
364 365 366

`createUser`方法首先将纯文本密码编码为 SHA-256 字符串,然后将两个对象(用户和组)存储到数据库中。

W
wizardforcel 已提交
367
如果在数据库中找不到该用户,则`findUserById`方法将返回一个`User`对象或`null` 请注意此处`try-catch`的用法。 如果未找到具有给定 ID(电子邮件)的用户,则对`query.getSingleResult()`的调用将引发`NoResultException`。 我们将忽略该异常,而是返回一个`null`用户。 稍后,我们将在托管 Bean 中处理`null`
W
init  
wizardforcel 已提交
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403

实用程序将密码编码为 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 安全领域。

W
wizardforcel 已提交
404
`localhost:4848`打开 Glassfish 管理控制台。 打开“配置 -&gt; 服务器配置 -&gt; 安全 -&gt; 领域”,然后单击“新建…”按钮以创建新的安全领域。
W
init  
wizardforcel 已提交
405 406 407 408 409 410 411 412 413 414 415

![Glassfish server configuration security realms](img/08e1e625a22c339aab716879c3da9e95.jpg)

Glassfish 服务器配置安全领域

![Create new JDBC security realm](img/d3712488e8b862c4928b28f0c919e95f.jpg)

创建新的 JDBC 安全领域

填写以下字段:

W
wizardforcel 已提交
416 417 418 419 420 421 422 423 424 425 426 427 428
*   `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
W
init  
wizardforcel 已提交
429

W
wizardforcel 已提交
430
## `glassfish-web.xml`文件
W
init  
wizardforcel 已提交
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451

```java
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
	<security-role-mapping>
		<role-name>users</role-name>
		<group-name>users</group-name>
	</security-role-mapping>
	<class-loader delegate="true" />
	<jsp-config>
		<property name="keepgenerated" value="true">
			<description>Keep a copy of the generated servlet class' java code.</description>
		</property>
	</jsp-config>
	<parameter-encoding default-charset="UTF-8" />
</glassfish-web-app>
```

我们必须在`glassfish-web.xml`文件中指定`security-role-mapping`,并添加角色名称和组名称

W
wizardforcel 已提交
452
## `web.xml`文件
W
init  
wizardforcel 已提交
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522

```java
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	version="3.1">

    <display-name>Form Based Auth Example</display-name>

    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <context-param>
        <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
        <param-value>.xhtml</param-value>
    </context-param>     
    <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>

    <session-config>
        <session-timeout>
            300
        </session-timeout>
    </session-config>

    <welcome-file-list>
        <welcome-file>signin.xhtml</welcome-file>
    </welcome-file-list>

    <!-- SECURITY -->
    <login-config>
	<auth-method>FORM</auth-method>
        <realm-name>jdbc-realm</realm-name>
	<form-login-config>
		<form-login-page>/signin.xhtml</form-login-page>
		<form-error-page>/signin.xhtml</form-error-page>
	</form-login-config>
    </login-config>

    <security-role>
        <description/>
        <role-name>users</role-name>
    </security-role>

    <security-constraint>
        <display-name>Restricted to users</display-name>
        <web-resource-collection>
            <web-resource-name>Restricted Access</web-resource-name>
            <url-pattern>/user/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>users</role-name>
        </auth-constraint>
        <user-data-constraint>
		<transport-guarantee>NONE</transport-guarantee>
	</user-data-constraint>
    </security-constraint>
</web-app>
```

W
wizardforcel 已提交
523
`web.xml`部署描述符也需要进行一些更改。 我们将在此处配置安全规则。
W
init  
wizardforcel 已提交
524 525 526 527 528

首先,我们配置会话超时。 这是使用户会话保持活动状态的时间。 当会话超时时,用户不必再次登录。 该值以分钟为单位。 300 表示用户会话将持续 5 个小时。

其次,我们将`welcome-file`设置为`signin.xhtml`。 当用户打开应用程序时,将显示登录屏幕。

W
wizardforcel 已提交
529
`<login-config>`中设置以下内容:
W
init  
wizardforcel 已提交
530

W
wizardforcel 已提交
531 532 533
*   `auth-method``FORM` – 用户使用 Web 表单输入用户名和密码
*   `realm-name``jdbc-realm` – 这是我们在上一步中配置的安全领域的名称
*   `form-login-page``form-error-page`是带有登录表单的 xhtml 文件和在发生登录错误时显示的 xhtml 文件(我们都指向`signin.xhtml`,因为在此文件中进行了错误处理)
W
init  
wizardforcel 已提交
534 535 536

我们只有一个安全角色–用户

W
wizardforcel 已提交
537
`<security-constraints>`标签中,我们为特定用户角色定义了受限资源。 换句话说 – `user`目录中的每个文件都需要登录才能访问。 我们将在其中放置私有 XHTML 文件。
W
init  
wizardforcel 已提交
538

W
wizardforcel 已提交
539
## 创建展示层
W
init  
wizardforcel 已提交
540 541 542

![Registration form](img/0c3808c455e96f9b6cc17ae1a64b8184.jpg)

W
wizardforcel 已提交
543
注册表单
W
init  
wizardforcel 已提交
544

W
wizardforcel 已提交
545
### 注册表单页面
W
init  
wizardforcel 已提交
546

W
wizardforcel 已提交
547
在注册表单中,我们要求提供用户名,电子邮件地址和密码。 我们还要求用户确认密码。 我们将验证电子邮件,密码和确认密码字段中的用户输入。
W
init  
wizardforcel 已提交
548

W
wizardforcel 已提交
549
首先让我们看一下 JSF XHTML 文件和`ManagedBean`
W
init  
wizardforcel 已提交
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605

```java
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:f="http://java.sun.com/jsf/core">
<h:head>
	<title>register</title>
</h:head>
<h:body>

	<h:form>

		<!-- register a PostValidateEvent -->
		<f:event listener="#{registerView.validatePassword}"
			type="postValidate" />

		<h3>Create new account</h3>
		<h:panelGrid columns="2">
			<h:outputLabel for="name">Name:</h:outputLabel>
			<h:inputText id="name" value="#{registerView.name}" required="true"
				requiredMessage="Please enter your name" maxlength="30"></h:inputText>

			<h:outputLabel for="email">E-Mail:</h:outputLabel>
			<h:inputText id="email" value="#{registerView.email}" required="true"
				requiredMessage="Please enter your e-mail address">
				<f:validator validatorId="emailValidator" />
			</h:inputText>

			<h:outputLabel for="password">Password:</h:outputLabel>
			<h:inputSecret id="password" value="#{registerView.password}"
				required="true" requiredMessage="Please enter password"
				validatorMessage="Password must contain atleast one lowercase character, 
					uppercase character, a digit and it's length must be between 6 and 20 characters">
				<f:validateRegex pattern="((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20})" />
			</h:inputSecret>

			<h:outputLabel for="confirmpassword">Confirm password:</h:outputLabel>
			<h:inputSecret id="confirmpassword"
				value="#{registerView.confirmPassword}" required="true"
				requiredMessage="Please confirm your password"></h:inputSecret>

			<h:commandButton action="#{registerView.register}" value="Register"></h:commandButton>

		</h:panelGrid>

	</h:form>

	<br />
	<br />
	<h:link value="I already have an account" outcome="signin" />

</h:body>
</html>
```

W
wizardforcel 已提交
606
注册视图
W
init  
wizardforcel 已提交
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737

```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 种方法。

W
wizardforcel 已提交
738
**使用`JSFvalidateRegex`**
W
init  
wizardforcel 已提交
739 740 741 742 743 744 745 746 747

我们使用此方法验证密码字段。 我们要做的就是插入具有给定正则表达式模式的`f:validateRegex`标签。 如果您想了解更多有关正则表达式的信息,请阅读[基本 Java 正则表达式](https://javatutorial.net/basic-java-regular-expressions)教程

```java
<f:validateRegex pattern="((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20})" />
```

我们还在`validatorMessage`属性内的 JSF 文件中指定了错误消息

W
wizardforcel 已提交
748
**在系统事件(`PostValidateEvent`)中验证**
W
init  
wizardforcel 已提交
749 750 751 752 753 754 755 756 757 758 759 760 761 762 763

确认密码字段通过名为`PostValidateEvent`的系统事件进行验证。

我们在 XHTML 文件中注册了一个新事件:

```java
<f:event listener="#{registerView.validatePassword}" type="postValidate" />
```

用户单击“注册”按钮时,将触发`RegisterView` bean 中的`public void validatePassword(ComponentSystemEvent event)`方法

**使用自定义验证程序类**

对于电子邮件验证,我们使用一个自定义类,该类实现`javax.faces.validator.Validator`接口

W
wizardforcel 已提交
764
`Validator`类必须实现一个称为`validate`的方法
W
init  
wizardforcel 已提交
765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817

```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
<f:validator validatorId="emailValidator" />
```

**检查电子邮件是否已在另一个注册中使用**

W
wizardforcel 已提交
818
为此,我们不需要验证器。 您可以在`ManagedBean``RegisterView`中使用类似这样的简单检查
W
init  
wizardforcel 已提交
819 820 821 822 823 824 825

```java
if (userEJB.findUserById(email) != null) { // email is already used }
```

## 成功注册

W
wizardforcel 已提交
826
如果用户在注册表单中输入正确的数据,则将创建一个新的`User`对象并将其插入数据库中。
W
init  
wizardforcel 已提交
827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856

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

W
wizardforcel 已提交
857
`regdone.xhtml`页面
W
init  
wizardforcel 已提交
858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873

```java
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" 
		xmlns:h="http://java.sun.com/jsf/html" >
<h:head>
	<title>Registration done</title>
</h:head>
<h:body>
	<h3>Your account has been successfully created</h3>
	<br/><br/>
	<h:link value="Sign in with your new account" outcome="signin" />
</h:body>
</html>
```

W
wizardforcel 已提交
874
## 登录表单
W
init  
wizardforcel 已提交
875

W
wizardforcel 已提交
876
在此表单中,我们需要用户的电子邮件地址和密码。 表单针对空白字段进行了验证。 在`LoginView`中检查用户名和密码的有效性。
W
init  
wizardforcel 已提交
877 878 879

![Login form](img/5528420612ad56473a4881c1a3c8f87d.jpg)

W
wizardforcel 已提交
880
登录表单
W
init  
wizardforcel 已提交
881 882 883 884 885 886 887 888 889

如果用户输入了有效的登录数据,则会创建一个新的`java.security.Principal`对象。 我们还将用户添加到`externalContext`的会话地图中

```java
ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
Map<String, Object> sessionMap = externalContext.getSessionMap();
sessionMap.put("User", user);
```

W
wizardforcel 已提交
890
`signin.xhtml`文件
W
init  
wizardforcel 已提交
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923

```java
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:h="http://java.sun.com/jsf/html">
<h:head>
	<title>login</title>
</h:head>
<h:body>

	<h:form>
		<h3>Please Sign In</h3>
		<h:panelGrid columns="2">
			<h:outputLabel for="email">E-Mail:</h:outputLabel>
			<h:inputText id="email" value="#{loginView.email}" required="true"
				requiredMessage="Please enter your e-mail address"></h:inputText>

			<h:outputLabel for="password">Password:</h:outputLabel>
			<h:inputSecret id="password" value="#{loginView.password}"
				required="true" requiredMessage="Please enter password"></h:inputSecret>

			<h:commandButton action="#{loginView.login}" value="Login"></h:commandButton>
		</h:panelGrid>
	</h:form>

	<br />
	<br />
	<h:link value="Create new account" outcome="register" />

</h:body>
</html>
```

W
wizardforcel 已提交
924
`LoginView ManagedBean`
W
init  
wizardforcel 已提交
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059

```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<String, Object> 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";
}
```

## 故障排除

对安全代码进行故障排除非常困难。 大多数组件不会引发有意义的异常。 这样做是故意进行的,以防止黑客在尝试破解应用程序的安全性时获得有用的信息。 不幸的是,我们也没有获得这些有用的信息。 如果发现一些问题,请尝试遵循例外。 两次检查您的安全领域配置,并检查所有密码编码是否正确。