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

2023-11-28 10:28:40 浏览数 (2)

原文链接: Spring Security Login Form Example with Database Authentication - 原文作者: Ramesh Fadatare 本文采用的是意译的方式 译者加:上一篇文章 Spring Security 自定义登陆页面 我们基于 InMemoryUserDetailsManager 进行用户认证。这篇文章,我们将集合数据库 mysql

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

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

数据库设置

如图:

添加 Maven 依赖

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

代码语言:javascript复制
<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 服务器中创建一个数据库:

代码语言:javascript复制
create database login_system

因为我们使用 MySQL 作为我们的数据库,所以我们需要配置数据库的 URL, username 和 password,以便在数据库启动时 Spring 可以和它建立联系。

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

代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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() 方法:

代码语言:javascript复制
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 的类,如下配置:

代码语言:javascript复制
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

代码语言:javascript复制
<!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 方法来渲染登陆模版:

代码语言:javascript复制
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

一旦用户成功登陆,那么欢迎页面将会展示出来:

代码语言:javascript复制
<!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 脚本在数据库县相关表中插入数据:

代码语言:javascript复制
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 SecurityMYSQL 数据库知识应用到自定义的登陆表单上。

0 人点赞