原文链接: Spring Security Login Form Example with Database Authentication – 原文作者: Ramesh Fadatare

本文选用的是意译的方法

译者加:上一篇文章 Spring Security 自定义登陆页面 咱们根据 InMemoryUserDetailsManager 进行用户认证。这篇文章,咱们将集合数据库 mysql

在这篇 Spring Security 文章中,咱们将学习怎样运用 Spring SecurityMySQL 数据库进行数据库认证,并使用在自定义的登陆表单中。

在这个数据库认证事例中,用户在登陆的表单输入登陆凭据,比如用户名和暗码,然后点击登陆。接着,咱们在数据库表单中对用户输入的凭据,即用户名和暗码进行验证。

数据库设置

如图:

Spring Security 登陆表单事例,结合数据库认证

添加 Maven 依靠

在你的 Spring Boot 项目中,添加下面的 Maven 依靠:

<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 注解,开发人员能够主动添加生成 gettersetter 方法、equals()toString() 等常见的样板代码。

装备 MySQL 数据库

首先,咱们运用下面的指令行在 MySQL 服务器中创立一个数据库:

create database login_system

由于咱们运用 MySQL 作为咱们的数据库,所以咱们需要装备数据库的 URL, username 和 password,以便在数据库启动时 Spring 能够和它树立联系。

src/main/resources/application.properties 文件中,添加下面的特点:

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 实体

在这个章节中,咱们将创立 UserRoleJPA 实体,然后让它们之间树立多对多(MANY-to-MANY)的联系。让咱们运用 JPA 的注解在 UserRole 实体中树立多对多的联系

User

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

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

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

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() 方法:

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 的类,如下装备:

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

<!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>

关于这个自定义登陆表单,有几个关键点,如下:

  • 表单应该触发 /loginpost 接口
  • 表单应该在参数中指定名为 username 的用户名
  • 表单应该在参数中指定名为 password 的暗码
  • 假如 HTTP 参数名为 error 呈现,则表明用户供给的用户名或许暗码无效
  • 假如 HTTP 参数名为 logout 呈现,则表明用户成功退出
  • 许多用户不需要太多自定义用户登陆页面。但是,假如需要,咱们能够运用额定的装备自定义想要的内容

Spring MVC Controller

让咱们在 Spring MVC 中创立一个 /loginGET 方法来烘托登陆模版:

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

当然,咱们也需要创立一个方法来处理 welcomeThymeleaf 模版页面。

Thymeleaf 模版 – welcome.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 脚本在数据库县相关表中刺进数据:

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 将主动创立创立数据库表,所以你不需要手动创立表。

运用浏览器测验数据库鉴权的用户登陆

在浏览器中输入 URLhttp://localhost:8080 以导航到登陆页面。接着,输入用户名/暗码为 admin/admin,然后点击提交按钮:

Spring Security 登陆表单事例,结合数据库认证

登陆成功后,咱们会看到下面的网页:

Spring Security 登陆表单事例,结合数据库认证

接着,点击退出按钮退出使用:

Spring Security 登陆表单事例,结合数据库认证

总结

在这个 Spring Security 教程中,咱们学到了怎样将 Spring SecurityMYSQL 数据库常识使用到自定义的登陆表单上。