如今,密码策略非常普遍,并且存在于大多数在线平台上。虽然某些用户并不真正喜欢它们,但它们存在是有原因的——使密码更安全。
您肯定有过应用程序强制使用某些密码规则的经验,例如允许的最小或最大字符数,包括数字,大写字母等。
无论安全系统有多好,如果用户选择“密码”等弱密码,敏感数据可能会暴露。虽然一些用户可能会对密码策略感到恼火,但它们可以保护用户数据的安全,因为它会使攻击效率低下。
为了在基于Spring的应用程序中实现这一点,我们将使用Passay - 一个专门为此目的制作的库,它使在Java中执行密码策略变得容易。
请注意:本教程假设您具有 Spring 框架的基本知识,因此为了简洁起见,我们将更多地关注 Passay。
除了密码策略之外,实现安全性的一个良好且基本的技术是密码编码。
与往常一样,从框架式 Spring Boot 项目开始的最简单方法是使用Spring Initializr。
选择您喜欢的 Spring 引导版本并添加依赖项:Web和Thymeleaf
在此之后,将其生成为 Maven 项目,一切就绪!
- "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.0modelVersion>
- <parent>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-parentartifactId>
- <version>2.7.5version>
- <relativePath />
- parent>
- <groupId>com.mynotes.spring.mvcgroupId>
- <artifactId>password-strengthartifactId>
- <version>0.0.1-SNAPSHOTversion>
- <name>password-strengthname>
- <description>Demo project for Spring Bootdescription>
-
- <properties>
- <java.version>1.8java.version>
- properties>
-
- <dependencies>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-thymeleafartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
- <dependency>
- <groupId>org.passaygroupId>
- <artifactId>passayartifactId>
- <version>1.6.2version>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-devtoolsartifactId>
- <scope>runtimescope>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-testartifactId>
- <scope>testscope>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-validationartifactId>
- dependency>
- <dependency>
- <groupId>junitgroupId>
- <artifactId>junitartifactId>
- <version>4.13.2version>
- <scope>testscope>
- dependency>
- dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-maven-pluginartifactId>
- 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:
根据项目要求,我们有时必须为应用程序定义特定的自定义代码。
由于我们可以强制实施不同的策略和规则,因此让我们继续定义一个自定义注释来检查有效的密码,我们将在类中使用该注释。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
约束是不言自明的:
LengthRule
CharacterRule
CharacterRule
CharacterRule
CharacterRule
WhitespaceRule
可以使用Passay编写的规则的完整列表可以在官方网站上找到。
最后,我们验证了密码,如果它通过了所有条件,则返回。如果某些条件失败,我们将所有失败条件的错误消息聚合到一个字符串中,以“,”分隔,然后将其放入 and 中返回。true
context
false
让我们再次运行我们的项目并输入无效的密码以验证验证是否有效:
在本文中,我们介绍了如何使用Passay库强制执行某些密码规则。我们为此创建了一个自定义注释和密码约束验证器,并在我们的实例变量中使用它,实际的业务逻辑是在单独的类中实现的。
示例代码下载:https://github.com/allwaysoft/spring-boot-passay-password-strength