介绍
如今,密码策略非常普遍,并且存在于大多数在线平台上。虽然某些用户并不真正喜欢它们,但它们存在是有原因的——使密码更安全。
您肯定有过应用程序强制使用某些密码规则的经验,例如允许的最小或最大字符数,包括数字,大写字母等。
无论安全系统有多好,如果用户选择“密码”等弱密码,敏感数据可能会暴露。虽然一些用户可能会对密码策略感到恼火,但它们可以保护用户数据的安全,因为它会使攻击效率低下。
为了在基于Spring的应用程序中实现这一点,我们将使用Passay - 一个专门为此目的制作的库,它使在Java中执行密码策略变得容易。
请注意:本教程假设您具有 Spring 框架的基本知识,因此为了简洁起见,我们将更多地关注 Passay。
除了密码策略之外,实现安全性的一个良好且基本的技术是密码编码。
注册表格
与往常一样,从框架式 Spring Boot 项目开始的最简单方法是使用Spring Initializr。
选择您喜欢的 Spring 引导版本并添加依赖项:Web和Thymeleaf
在此之后,将其生成为 Maven 项目,一切就绪!
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.mynotes.spring.mvc</groupId>
<artifactId>password-strength</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>password-strength</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
让我们定义一个简单的数据传输对象,其中我们将包含要从表单中捕获的所有属性:
public class UserDto {
@NotEmpty
private String name;
@Email
@NotEmpty
private String email;
private String password;
我们尚未对密码字段进行批注,因为我们将为此实现自定义批注。
然后我们有一个简单的控制器类,它服务于注册表单,并在使用映射提交时捕获其数据:GET/POST
@Controller
@RequestMapping("/signup")
public class SignUpController {
@ModelAttribute("user")
public UserDto userDto() {
return new UserDto();
}
@GetMapping
public String showForm() {
return "signup";
}
@PostMapping
public String submitForm(@Valid @ModelAttribute("user") UserDto user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "signup";
}
return "success";
}
}
我们首先定义了 a,并为其分配了实例。这是提交后将保存信息的对象。@ModelAttribute("user")
UserDto
使用此对象,我们可以提取数据并将其持久保存在数据库中。
该方法返回一个值为“注册”的字符串。由于我们的类路径中有Thymeleaf,Spring 将在资源中的模板文件夹中搜索“注册.html”。showForm()
同样,我们有一个POST映射,它将检查表单是否有任何错误。如果是这样,它将重定向回“注册.html”页面。否则,它会将用户转发到“成功”页面。submitForm()
Thymeleaf是一个现代的服务器端Java模板引擎,用于处理和创建HTML,XML,JavaScript,CSS和文本。它是Java服务器页面(JSP)等旧模板引擎的现代替代方案。
让我们继续定义一个“注册.html”页面:
<form action="#" th:action="@{/signup}" th:object="${user}" method="post">
<div class="form-group">
<input type="text" th:field="*{name}" class="form-control"
id="name" placeholder="Name"> <span
th:if="${#fields.hasErrors('name')}" th:errors="*{name}"
class="text-danger"></span>
</div>
<div class="form-group">
<input type="text" th:field="*{email}" class="form-control"
id="email" placeholder="Email"> <span
th:if="${#fields.hasErrors('email')}" th:errors="*{email}"
class="text-danger"></span>
</div>
<div class="form-group">
<input type="text" th:field="*{password}" class="form-control"
id="password" placeholder="Password">
<ul class="text-danger" th:each="error: ${#fields.errors('password')}">
<li th:each="message : ${error.split(',')}">
<p class="error-message" th:text="${message}"></p>
</li>
</ul>
</div>
<div class="col-md-6 mt-5">
<input type="submit" class="btn btn-primary" value="Submit">
</div>
</form>
这里有几点应该指出:
th:action = "@{/signup}"
- 操作属性是指我们在提交表单时调用的 URL。我们以控制器中的“注册”URL 映射为目标。method="post"
- 方法属性是指我们发送的请求类型。这必须与方法中定义的请求类型匹配。submitForm()
th:object="${user}"
- 对象属性是指我们之前在控制器中使用的对象名称。使用窗体的其余部分,我们将填充实例的字段,然后保存实例。@ModelAttribute("user")
UserDto
我们还有其他 3 个映射到、和usingtag的输入字段。如果字段有错误,将通过标签通知用户。name
email
password
th:field
th:errors
让我们运行我们的应用程序并导航到http://localhost:8080/signup:
自定义@ValidPassword注释
根据项目要求,我们有时必须为应用程序定义特定的自定义代码。
由于我们可以强制实施不同的策略和规则,因此让我们继续定义一个自定义注释来检查有效的密码,我们将在类中使用该注释。UserDto
批注只是代码的元数据,不包含任何业务逻辑。它们只能提供有关定义它的属性(类/方法/包/字段)的信息。
让我们创建注释:@ValidPassword
@Documented
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ValidPassword {
String message() default "Invalid Password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
如您所见,要创建注释,我们使用关键字。让我们看一下几个关键字并充分理解它们,然后再继续:@interface
@Documented
:一个简单的标记注释,用于判断是否在Javadocs中添加注释。@Constraint
:将批注标记为Bean 验证约束。该元素指定实现约束的类。稍后我们将创建该类。validatedBy
PasswordConstraintValidator
@Target
:是可以使用我们的注释的地方。如果未指定此项,则可以将批注放置在任何位置。目前,我们的注释可以放置在实例变量和其他注释上。@Retention
:定义批注应保留多长时间。我们已经选择了它可以被运行时环境使用。RUNTIME
要在我们的类中使用它,只需注释密码字段:UserDto
@ValidPassword
private String password;
自定义密码约束验证程序
现在我们有了注释,让我们实现它的验证逻辑。在此之前,请确保您的pom.xml文件中包含Passay Maven依赖项:
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>{$version}</version>
</dependency>
您可以在此处检查最新的依赖项。
最后,让我们编写我们的类:PasswordConstraintValidator
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {
@Override
public void initialize(ValidPassword arg0) {
}
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
PasswordValidator validator = new PasswordValidator(Arrays.asList(
// at least 8 characters
new LengthRule(8, 30),
// at least one upper-case character
new CharacterRule(EnglishCharacterData.UpperCase, 1),
// at least one lower-case character
new CharacterRule(EnglishCharacterData.LowerCase, 1),
// at least one digit character
new CharacterRule(EnglishCharacterData.Digit, 1),
// at least one symbol (special character)
new CharacterRule(EnglishCharacterData.Special, 1),
// no whitespace
new WhitespaceRule()
));
RuleResult result = validator.validate(new PasswordData(password));
if (result.isValid()) {
return true;
}
List<String> messages = validator.getMessages(result);
String messageTemplate = messages.stream()
.collect(Collectors.joining(","));
context.buildConstraintViolationWithTemplate(messageTemplate)
.addConstraintViolation()
.disableDefaultConstraintViolation();
return false;
}
}
我们实现了接口,这迫使我们实现几个方法。ConstraintValidator
我们首先通过传递要在密码中强制执行的约束数组来创建对象。PasswordValidator
约束是不言自明的:
- 它的长度必须介于 8 到 30 个字符之间,由
LengthRule
- 它必须至少具有 1 个小写字符,如
CharacterRule
- 它必须至少具有 1 个大写字符,如
CharacterRule
- 它必须至少具有 1 个由
CharacterRule
- 它必须至少具有 1 位数字字符,如
CharacterRule
- 它不得包含由
WhitespaceRule
可以使用Passay编写的规则的完整列表可以在官方网站上找到。
最后,我们验证了密码,如果它通过了所有条件,则返回。如果某些条件失败,我们将所有失败条件的错误消息聚合到一个字符串中,以“,”分隔,然后将其放入 and 中返回。true
context
false
让我们再次运行我们的项目并输入无效的密码以验证验证是否有效:
结论
在本文中,我们介绍了如何使用Passay库强制执行某些密码规则。我们为此创建了一个自定义注释和密码约束验证器,并在我们的实例变量中使用它,实际的业务逻辑是在单独的类中实现的。
示例代码下载:https://github.com/allwaysoft/spring-boot-passay-password-strength