原文链接: Spring Security Login Form Example with Database Authentication - 原文作者: Ramesh Fadatare
本文采用的是意译的方式
译者加:上一篇文章 Spring Security 自定义登陆页面 我们基于InMemoryUserDetailsManager
进行用户认证。这篇文章,我们将集合数据库mysql
。
在这篇 Spring Security
文章中,我们将学习怎么使用 Spring Security
和 MySQL
数据库进行数据库认证,并应用在自定义的登陆表单中。
在这个数据库认证案例中,用户在登陆的表单输入登陆凭证,比如用户名和密码,然后点击登陆。接着,我们在数据库表单中对用户输入的凭证,即用户名和密码进行验证。
数据库设置
如图:
添加 Maven 依赖
在你的 Spring Boot
项目中,添加下面的 Maven
依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
译者加:
Lombok
的主要作用是减少重复劳动和简化代码。通过使用Lombok
注解,开发人员可以自动添加生成getter
和setter
方法、equals()
、toString()
等常见的样板代码。
配置 MySQL 数据库
首先,我们使用下面的命令行在 MySQL
服务器中创建一个数据库:
bash
create database login_system
因为我们使用 MySQL
作为我们的数据库,所以我们需要配置数据库的 URL, username 和 password ,以便在数据库启动时 Spring
可以和它建立联系。
在 src/main/resources/application.properties
文件中,添加下面的属性:
bash
spring.datasource.url = jdbc:mysql://localhost:3306/login_system
spring.datasource.username = root
spring.datasource.password = root
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
logging.level.org.springframework.security=DEBUG
Model 层 - 创建 JPA 实体
在这个章节中,我们将创建 User
和 Role
的 JPA
实体,然后让它们之间建立多对多(MANY-to-MANY )的关系。让我们使用 JPA
的注解在 User
和 Role
实体中建立多对多的关系。
User
java
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")
)
private Set<Role> roles;
}
Role
java
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Repository 层
UserRepository
java
import net.javaguides.todo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Boolean existsByEmail(String email);
Optional<User> findByUsernameOrEmail(String username, String email);
boolean existsByUsername(String username);
}
RoleRepository
java
import net.javaguides.todo.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Map;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}
Service 层 - CustomUserDetailsService
让我们添加个逻辑,从数据库中通过 name
或者 email
加载用户详情。
我们创建了一个 CustomUserDetailsService
,它实现了 UserDetailsService
接口(Spring Security
内置的接口),并提供了一个实现了的 loadUserByUsername()
方法:
java
import lombok.AllArgsConstructor;
import net.javaguides.todo.entity.User;
import net.javaguides.todo.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() -> new UsernameNotFoundException("User not exists by Username or Email"));
Set<GrantedAuthority> authorities = user.getRoles().stream()
.map((role) -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet());
return new org.springframework.security.core.userdetails.User(
usernameOrEmail,
user.getPassword(),
authorities
);
}
}
Spring Security
使用 UserDetailsService
接口,包含了loadUserByUsername(String username)
方法,通过 username
来查询 UserDetails
信息。
UserDetails
接口代表一个认证的用户对象,Spring Security
提供了 org.springframework.security.core.userdetails.User
的开箱即用的实现。
Spring Security 配置
让我们创建一个名为 SpringSecurityConfig
的类,如下配置:
java
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@AllArgsConstructor
public class SpringSecurityConfig {
private UserDetailsService userDetailsService;
@Bean
public static PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
).formLogin(
form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/welcome")
.permitAll()
).logout(
logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.permitAll()
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
当我们在 Spring Security
配置中指定登陆的页面,我们就应该编写登陆页面。我们将使用 Spring Security
提供的 BCryptPasswordEncoder
类去加密密码。
Thymeleaf Template - 自定义登陆页面
下面这个 Thymeleaf
模版将产生一个符合 /login
登陆页面的 HTML
登陆表单。
Login Form - src/main/resources/templates/login.html
ini
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
>
<head>
<meta charset="UTF-8">
<title>Login System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" th:href="@{/index}">Spring Security Custom Login Example</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<br /><br />
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<div th:if="${param.error}">
<div class="alert alert-danger">Invalid Email or Password</div>
</div>
<div th:if="${param.logout}">
<div class="alert alert-success"> You have been logged out.</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="text-center">Login Form</h2>
</div>
<div class="card-body">
<form
method="post"
role="form"
th:action="@{/login}"
class="form-horizontal"
>
<div class="form-group mb-3">
<label class="control-label"> Email</label>
<input
type="text"
id="username"
name="username"
class="form-control"
placeholder="Enter email address"
/>
</div>
<div class="form-group mb-3">
<label class="control-label"> Password</label>
<input
type="password"
id="password"
name="password"
class="form-control"
placeholder="Enter password"
/>
</div>
<div class="form-group mb-3">
<button type="submit" class="btn btn-primary" >Submit</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
关于这个自定义登陆表单,有几个关键点,如下:
- 表单应该触发
/login
的post
接口 - 表单应该在参数中指定名为
username
的用户名 - 表单应该在参数中指定名为
password
的密码 - 如果
HTTP
参数名为error
出现,则表明用户提供的用户名或者密码无效 - 如果
HTTP
参数名为logout
出现,则表明用户成功退出 - 很多用户不需要太多自定义用户登陆页面。然而,如果需要,我们可以使用额外的配置自定义想要的内容
Spring MVC Controller
让我们在 Spring MVC
中创建一个 /login
的 GET
方法来渲染登陆模版:
java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WelComeController {
@GetMapping("/welcome")
public String greeting() {
return "welcome";
}
@GetMapping("/login")
public String login(){
return "login";
}
}
当然,我们也需要创建一个方法来处理 welcome 的 Thymeleaf
模版页面。
Thymeleaf 模版 - welcome.html
一旦用户成功登陆,那么欢迎页面将会展示出来:
html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
>
<head>
<meta charset="UTF-8">
<title>Registration and Login System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" th:href="@{/index}">Spring Security Custom Login Example</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" th:href="@{/logout}">Logout</a>
</li>
</ul>
</div>
</div>
</nav>
<br /><br />
<body>
<div class="container">
<div class="row">
<h1> Welcome to Spring Security world!</h1>
</div>
</div>
</body>
</html>
插入 SQL 脚本
在测试 Spring Security
之前,我们确保使用下面的 SQL
脚本在数据库县相关表中插入数据:
bash
INSERT INTO `users` VALUES
(1,'ramesh@gmail.com','ramesh','$2a$10$5PiyN0MsG0y886d8xWXtwuLXK0Y7zZwcN5xm82b4oDSVr7yF0O6em','ramesh'),
(2,'admin@gmail.com','admin','$2a$10$gqHrslMttQWSsDSVRTK1OehkkBiXsJ/a4z2OURU./dizwOQu5Lovu','admin');
INSERT INTO `roles` VALUES (1,'ROLE_ADMIN'),(2,'ROLE_USER');
INSERT INTO `users_roles` VALUES (2,1),(1,2);
Hibernate
将自动创建创建数据库表,所以你不需要手动创建表。
使用浏览器测试数据库鉴权的用户登陆
在浏览器中输入 URL
为 http://localhost:8080
以导航到登陆页面。接着,输入用户名/密码为 admin/admin
,然后点击提交按钮:
登陆成功后,我们会看到下面的网页:
接着,点击退出按钮退出应用:
总结
在这个 Spring Security
教程中,我们学到了怎么将 Spring Security
和 MYSQL
数据库知识应用到自定义的登陆表单上。