跳至主要內容

基于数据库认证

Leospringsecurityspringsecurity约 2927 字大约 10 分钟

image-20231030235443828
image-20231030235443828

1.前言

大家好,我是Leo哥🫣🫣🫣,通过前面几节的学习,我们知道了如果通过内存进行登录认证以及如何获取登录用户的认证信息。但是在实际开发中,我们的用户都是存储在数据库中,并非直接存放在本地内存中。

接下来,我们这篇博客将基于数据库的用户来实现我们的登录认证。

2.概述

其实用户进行认证,最常见的认证方式就是 用户名+密码,认证服务需要根据用户名从存储中查询用户信息,然后判断输入的密码和存储中的密码是否匹配。

对用户名、密码存储,SpringSecurity支持多种存储机制:

  • 内存
  • JDBC 关系型数据库
  • 使用UserDetailsService的自定义数据存储
  • 使用 LDAP认证的 LDAP 存储

在此之前我们已经学习了如果通过内存进行登录认证,本篇则是主要介绍基于数据库进行认证。其他两种方式,有兴趣的可以进行自行研究,不在本系列教程的研究范围内。

3.环境搭建

3.1 准备数据库以及表结构

  -- 用户表
 
CREATE TABLE `sys_user` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(32) NOT NULL,
    `password` VARCHAR(255) NOT NULL,
    `enabled` TINYINT(1) NOT NULL DEFAULT 1,
    `account_non_expired` TINYINT(1) NOT NULL DEFAULT 1,
    `account_non_locked` TINYINT(1) NOT NULL DEFAULT 1,
    `credentials_non_expired` TINYINT(1) NOT NULL DEFAULT 1,
    PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;




-- 角色表
CREATE TABLE `sys_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(32) DEFAULT NULL,
    `describe` VARCHAR(32) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

 
-- 创建 sysuser_role 表
CREATE TABLE `sysuser_role` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `uid` INT(11) DEFAULT NULL,
    `rid` INT(11) DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `uid` (`uid`),
    KEY `rid` (`rid`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- 插入用户数据
BEGIN;
    INSERT INTO `sys_user` (`id`, `username`, `password`, `enabled`, `account_non_expired`, `account_non_locked`, `credentials_non_expired`)
    VALUES
        (1, 'root', '{noop}123', 1, 1, 1, 1),
        (2, 'admin', '{noop}123', 1, 1, 1, 1),
        (3, 'Leo', '{noop}123', 1, 1, 1, 1);
COMMIT;

-- 插入角色数据
BEGIN;
    INSERT INTO `sys_role` (`id`, `name`, `describe`)
    VALUES
        (1, 'ROLE_super', '超级管理员'),
        (2, 'ROLE_admin', '普通管理员'),
        (3, 'ROLE_user', '普通用户');
COMMIT;

-- 插入用户角色数据
BEGIN;
    INSERT INTO `sysuser_role` (`id`, `uid`, `rid`)
    VALUES
        (1, 1, 1),
        (2, 1, 2),
        (3, 2, 2),
        (4, 3, 3);
COMMIT;

3.2 创建项目集成MybatisPlus

引入其他依赖以及mybatisplus依赖。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--  hutool工具包 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

添加application.yml配置文件

server:
  port: 8700
spring:
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://xxxx:3307/security_db?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
    username: xxxx
    password: xxxx





# mybatis-plus配置
mybatis-plus:
  #配置Mapper映射文件
  mapper-locations: classpath:/mapper/*.xml
  # 配置Mybatis数据返回类型别名(默认别名为类名)
  type-aliases-package: org.leocoder.db.domain
  configuration:
    # 自动驼峰命名
    map-underscore-to-camel-case: false
  global-config:
    db-config:
      logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

#配置控制台打印日志Debug
logging:
  level:
    org.leocoder.db.mapper: debug

然后生成mybatisplus的代码即可。

别忘了在启动类上添加@MapperScan扫描:

@MapperScan("org.leocoder.db.mapper")

3.3 环境测试

在测试类中添加测试代码,查验环境是否搭建成功:

@SpringBootTest(classes = DbApplication.class)
public class ApiTest {

    @Autowired
    private SysUserService userService;

    /**
     *  用于测试: 测试代码是否正常
     */
    @Test
    public void test01() {
        SysUser admin = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, "admin"));
        System.out.println(admin);
    }
}

可以看到我们已经成功的查出我们数据库的用户了,基本环境搭建成功!

image-20240701233620838
image-20240701233620838

4.回顾认证整体流程

SpringSecurity的整个认证流程其实就是一个完整的过滤器链,内部包含了提供各种功能的过滤器。

首先我们通过一张建议的流程图来了解一下数据库的整个认证是如何进行的。

image-20240701234437477
image-20240701234437477
  1. 用户提交用户名和密码:用户在登录页面输入用户名和密码,点击登录按钮,浏览器将表单数据发送到服务器。
  2. 封装请求信息:SpringSecurity拦截登录请求,将用户名和密码封装为一个 UsernamePasswordAuthenticationToken 对象,这是一个实现了 Authentication 接口的具体类。
  3. 认证管理器认证AuthenticationManager 是Spring Security的核心认证接口,负责协调认证过程。调用 authenticate() 方法开始认证。
  4. 委托认证AuthenticationManager 通常会委托给一个或多个 AuthenticationProvider进行具体的认证逻辑。在这里,DaoAuthenticationProvider是一个常见的实现类。
  5. 加载用户信息DaoAuthenticationProvider 调用 UserDetailsService 接口的 loadUserByUsername() 方法,从数据库或其他存储中加载用户信息。我们通常会自定义一个UserDetailsService 实现类,通过MyBatis Plus或JPA从数据库中获取用户信息。
  6. 返回用户信息UserDetailsService 实现类从数据库中获取用户信息后,返回一个 UserDetails 对象,包含用户的基本信息和权限信息。
  7. 密码验证DaoAuthenticationProvider 使用配置的PasswordEncoder(如BCryptPasswordEncoder)对用户输入的密码和数据库中的密码进行比对。如果密码匹配,继续下一步,否则抛出认证异常。
  8. 填充权限信息:如果密码验证通过,DaoAuthenticationProvider 将用户的权限信息填充到Authentication 对象中。权限信息通常是从用户的角色中提取的,可以通过SimpleGrantedAuthority 对象表示。
  9. 返回认证结果DaoAuthenticationProvider 将包含用户权限信息的 Authentication 对象返回给 AuthenticationManager
  10. 存储认证信息AuthenticationManager 将认证后的Authentication 对象存储在 SecurityContextHolder中。SecurityContextHolder 是一个全局持有认证信息的地方,后续的安全操作都会依赖于这里存储的认证信息。

我们接下来再详细看看这张详细的流程图,是否对整个认证流程更加清晰了呢。

image-20240701234139144
image-20240701234139144

Spring Security是怎么进行用户认证的呢?

AuthenticationManager 就是SpringSecurity用于执行身份验证的组件,只需要调用它的 authenticate 方法即可完成认证。Spring Security默认的认证方式就是在 UsernamePasswordAuthenticationFilter

这个过滤器中进行认证的,该过滤器负责认证逻辑。

SpringSecurity用户认证关键代码如下:

// 生成一个包含账号密码的认证信息
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

5.基于数据库认证

5.1 UserDetailsService接口

UserDetailsService是 **SpringSecurity **与认证数据交互的地方,如果你不重写 UserDetailsService,那么 SpringSecurity 认证的过程就是走的自己内部默认的那一套,只能是在application.yml 的配置里写死帐号密码和角色,这显然不能用于生产环境,生产环境自然要用数据库里的数据进行用户认证和获得相关权限,代码如下:

@Service
public class SysUserDetailService implements UserDetailsService {

    @Resource
    private SysUserMapper userMapper;

  
   // 因为这里只是做简单的登录认证,关于具体的权限等我们后面接着聊。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userMapper.selectUserByUsername(username);
        if (sysUser == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        return sysUser;
    }
}

UserDetailsService,是架起与数据库沟通的桥梁,这里弄明白了,后面都很简单了。

5.2 UserDetails接口

两个实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_role")
public class SysRole implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @TableField(value = "`name`")
    private String name;

    @TableField(value = "`describe`")
    private String describe;

    private static final long serialVersionUID = 1L;
}

UserDetailsService 接口需要返回一个UserDetails 类型的对象,从名称上也很好理解,就是一个封装了用户信息的类。我们需要将我们查询出来的用户对象,转为Spring Security中支持的用户对象,以便框架进行校验、存储。

在上面的流程中,我们知道,要想通过数据库用户进行自定义认证,那么就需要实现UserDetails这个接口。他是SpringSecurity中用于表示用户实体的核心接口。它提供了用户的基本信息,如用户名、密码、权限等。

通过实现 UserDetails 接口,可以将自定义的用户实体与SpringSecurity框架无缝集成,进而实现我们的需求。

实现 UserDetails 接口需要覆盖以下方法:

  • getAuthorities():返回用户的权限集合,通常是用户的角色信息。
  • getPassword():返回用户的密码。
  • getUsername():返回用户的用户名。
  • isAccountNonExpired():指示用户的账户是否未过期。
  • isAccountNonLocked():指示用户的账户是否未锁定。
  • isCredentialsNonExpired():指示用户的凭证(密码)是否未过期。
  • isEnabled():指示用户是否启用。
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_user")
public class SysUser implements Serializable, UserDetails {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @TableField(value = "username")
    private String username;

    @TableField(value = "`password`")
    private String password;

    @TableField(value = "enabled")
    private Boolean enabled;

    @TableField(value = "account_non_expired")
    private Boolean accountNonExpired;

    @TableField(value = "account_non_locked")
    private Boolean accountNonLocked;

    @TableField(value = "credentials_non_expired")
    private Boolean credentialsNonExpired;

    private static final long serialVersionUID = 1L;

    // 关系属性 用户的所有角色
    @TableField(exist = false)
    private List<SysRole> roleList = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        roleList.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
        return grantedAuthorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

5.3 添加配置类

SpringSecurity 6.0和之前的配置有些区别,后续我们会详细讲讲这一点。

添加配置类,同时我们依然使用我们之前的一些登录表单进行后续测试。

以及自定义登录响应成功之后的JSON。

image-20240702090642730
image-20240702090642730
@Configuration
@EnableWebSecurity
public class MySecurityConfig {


    // 使用基于数据库的数据进行认证
    @Resource
    private SysUserDetailService userDetailService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 放行改资源,不用认证可以直接访问
//                .requestMatchers("/test").permitAll()
                .requestMatchers("/login.html").permitAll()
                // 所有请求都需要认证
                .anyRequest().authenticated()
                // 开启表单登录
                .and().formLogin()
                //我们自定义的登录页面
                .loginPage("/login.html")
                // 处理登录的url
                .loginProcessingUrl("/doLogin")
                .usernameParameter("username")
                .passwordParameter("password")
//                .successForwardUrl("/hello") 访问默认的接口
                // 依然会访问到原来访问的接口
                .defaultSuccessUrl("/test")


//                .failureForwardUrl("/login.html")//登录失败后的forward跳转
//                .failureUrl("/login.html") //redirect跳转

                // 自定义登录失败处理器
                .failureHandler(new MyAuthenticationFailureHandler())

                //自定义退出功能
                .and()
                //开启退出功能的定义
                .logout()
                //退出功能的URL
                .logoutUrl("/out")
                //退出后是否删除session
                .invalidateHttpSession(true)
                //默认为true 清楚当前认证标记
                .clearAuthentication(true)
                .logoutSuccessHandler(new MyLogoutSuccessHandler())

                .and().csrf().disable();
        return http.build();
    }
}

5.4 测试

我们依然使用我们之前自定义的登录页面,并没有使用原始SpringSecurity给我们提供的登录页面,大家选择一个使用即可。

启动项目,访问:http://localhost:8700/login.htmlopen in new window ,这里使用我们数据库的用户进行登录。

可以看到登录成功,说明我们整个认证流程已经完成走通了,大功告成!!!

image-20240702090942673
image-20240702090942673

6.总结

以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是Leo,一个在互联网行业的小白,立志成为更好的自己。

如果你想了解更多关于Leo,可以关注公众号-程序员Leo,后面文章会首先同步至公众号。

公众号封面
公众号封面