Java框架快速入门: Spring Security+OAuth2之UserDetails与JDBC认证实践

纲要

  • 核心接口:UserDetailsUserDetailsService
    • UserDetails 接口的方法与扩展点
    • UserDetailsService 的职责与 loadUserByUsername 方法
  • 认证流程中的角色:AuthenticationManagerUserDetailsServicePasswordEncoder
  • 数据库认证的默认表结构:usersauthorities
  • 实战:从内存认证切换到 JDBC 认证
    • 添加 Spring JDBC 与 H2 依赖
    • 配置数据源与 H2 Web 控制台
    • 编写安全配置类,启用 jdbcAuthentication()
    • 启动应用并验证默认表与认证行为
    • 打印 SQL 日志以观察查询过程
  • 自定义 UserDetails 实现的基本思路
  • 小结

UserDetails ------ 安全上下文中的用户模型

UserDetails 是 Spring Security 中对用户信息的抽象,其本身是一个接口,而非具体类。这种设计使得框架能够适配几乎任何用户数据结构,同时保留高度可扩展性。查看 Spring Security 源码可以看到该接口的定义:

java 复制代码
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

关键方法说明

  • getAuthorities():返回用户拥有的权限集合,每个权限通常由 GrantedAuthority 表示。
  • getPassword()getUsername():安全框架要求必须提供用户名和密码。开发时可在实现时自由映射,例如用邮箱代替用户名,只需在 getUsername() 方法中返回邮箱即可。
  • isAccountNonExpired():账户是否未过期。某些企业级项目要求账户具有有效期,超过期限即不可用。
  • isAccountNonLocked():账户是否未被锁定。锁定与禁用(isEnabled())略有不同:锁定的账户可能允许登录但无法执行操作,而禁用的账户直接拒绝登录。
  • isCredentialsNonExpired():凭证(密码)是否未过期。当密码有有效期时,过期后系统可强制用户修改密码。
  • isEnabled():账户是否激活,是最常用的状态控制字段。

通常情况下,最简单的实现可以只关注 usernamepasswordenabled,其余方法直接返回 true。若需要自定义用户对象,只需让实体类实现该接口并提供相应逻辑。

UserDetailsService ------ 数据加载的桥梁

UserDetailsService 负责从数据源(如数据库)中加载用户信息并构建 UserDetails 对象。它仅定义了一个方法:

java 复制代码
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

实现时,根据用户名查询用户实体,若实体类已经实现了 UserDetails 接口,则直接返回即可;否则需要构造一个 UserDetails 实例(例如使用 Spring 提供的 org.springframework.security.core.userdetails.User 类)。

需要特别注意:UserDetailsService 本身并不执行认证,它只是提供数据的服务。真正的认证由 AuthenticationManager 协调,典型的实现是 ProviderManager,它会调用 DaoAuthenticationProvider,后者使用 UserDetailsService 获取用户信息,再通过 PasswordEncoder 验证密码。

在认证成功后,Authentication 对象的 principal 字段存储的就是 UserDetails 实例。由于 getPrincipal() 返回 Object 类型,因此具备极强的扩展性,你可以将任意对象放入安全上下文,但数据库认证场景下最常见的就是 UserDetails

认证流程概览

以下时序图展示了基于数据库认证时的核心交互步骤:
Database UserDetailsService DaoAuthenticationProvider AuthenticationManager UsernamePasswordAuthenticationFilter Client Database UserDetailsService DaoAuthenticationProvider AuthenticationManager UsernamePasswordAuthenticationFilter Client #mermaid-svg-IDJH1mOKhlHDVgyH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IDJH1mOKhlHDVgyH .error-icon{fill:#552222;}#mermaid-svg-IDJH1mOKhlHDVgyH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IDJH1mOKhlHDVgyH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IDJH1mOKhlHDVgyH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IDJH1mOKhlHDVgyH .marker.cross{stroke:#333333;}#mermaid-svg-IDJH1mOKhlHDVgyH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IDJH1mOKhlHDVgyH p{margin:0;}#mermaid-svg-IDJH1mOKhlHDVgyH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IDJH1mOKhlHDVgyH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-IDJH1mOKhlHDVgyH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IDJH1mOKhlHDVgyH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-IDJH1mOKhlHDVgyH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-IDJH1mOKhlHDVgyH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-IDJH1mOKhlHDVgyH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-IDJH1mOKhlHDVgyH .sequenceNumber{fill:white;}#mermaid-svg-IDJH1mOKhlHDVgyH #sequencenumber{fill:#333;}#mermaid-svg-IDJH1mOKhlHDVgyH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-IDJH1mOKhlHDVgyH .messageText{fill:#333;stroke:none;}#mermaid-svg-IDJH1mOKhlHDVgyH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IDJH1mOKhlHDVgyH .labelText,#mermaid-svg-IDJH1mOKhlHDVgyH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-IDJH1mOKhlHDVgyH .loopText,#mermaid-svg-IDJH1mOKhlHDVgyH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-IDJH1mOKhlHDVgyH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IDJH1mOKhlHDVgyH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-IDJH1mOKhlHDVgyH .noteText,#mermaid-svg-IDJH1mOKhlHDVgyH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-IDJH1mOKhlHDVgyH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IDJH1mOKhlHDVgyH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IDJH1mOKhlHDVgyH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IDJH1mOKhlHDVgyH .actorPopupMenu{position:absolute;}#mermaid-svg-IDJH1mOKhlHDVgyH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-IDJH1mOKhlHDVgyH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IDJH1mOKhlHDVgyH .actor-man circle,#mermaid-svg-IDJH1mOKhlHDVgyH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-IDJH1mOKhlHDVgyH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt密码匹配密码错误 提交用户名/密码authenticate(token)authenticate(token)loadUserByUsername(username)查询用户用户记录UserDetailspasswordEncoder.matches(raw, encoded)认证成功 Authentication已认证的 Authentication抛出 BadCredentialsException

默认数据库表结构

Spring Security 为 JDBC 认证提供了默认的表结构,最少仅需两张表:usersauthorities

users 表

列名 描述
username 用户名(主键)
password 加密后的密码
enabled 是否启用(布尔值)

authorities 表

列名 描述
username 用户名(外键)
authority 权限字符串

在实现最基础的数据库认证时,UserDetails 中账户是否过期、锁定等状态均可直接返回 true,数据库中只需存储上述三个字段即可。

实战:从内存认证切换到 JDBC 认证

下面通过一个完整的 Spring Boot 示例,演示如何将内存用户存储切换为基于数据库的 JDBC 认证,并使用 H2 内存数据库快速验证。

项目结构

dir 复制代码
src/
└── main/
    ├── java/com/example/security/
    │   ├── SecurityApplication.java
    │   ├── config/SecurityConfig.java
    │   └── controller/HomeController.java
    └── resources/
        └── application.yml

添加依赖

pom.xml 中引入 spring-boot-starter-jdbc 和 H2 数据库依赖:

xml 复制代码
<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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

配置数据源与 H2 控制台

application.yml 中设置数据源及 H2 Web 控制台:

yml 复制代码
spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    show-sql: false
logging:
  level:
    org.springframework.jdbc.core: DEBUG
  • MODE=MySQL 让 H2 兼容 MySQL 语法,方便将来迁移到真实 MySQL。
  • DB_CLOSE_DELAY=-1 保持内存数据库在连接关闭后不被销毁。
  • 日志级别设为 DEBUG 以便观察 SQL 语句的执行。

安全配置类

创建 SecurityConfig,启用 JDBC 认证并使用默认表结构:

java 复制代码
package com.example.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final DataSource dataSource;

    public SecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .withDefaultSchema()
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/h2-console/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .httpBasic();
        // 允许 H2 控制台使用 frame 框架
        http.headers().frameOptions().sameOrigin();
        // 禁用 CSRF 以方便 H2 控制台访问(生产环境需考虑安全)
        http.csrf().disable();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 演示环境使用明文编码,生产务必使用 BCrypt 等
        return NoOpPasswordEncoder.getInstance();
    }
}

说明:

  • withDefaultSchema() 会基于 H2 数据库自动创建 usersauthorities 两张表,并默认插入两个用户:
    • 用户 user / 密码 password,角色 ROLE_USER
    • 用户 admin / 密码 password,角色 ROLE_USER, ROLE_ADMIN
  • passwordEncoder 为了简单演示使用了 NoOpPasswordEncoder,实际项目必须替换为 BCryptPasswordEncoder
  • 配置放行 /h2-console/** 路径并允许 frame 同源请求,以便通过浏览器访问数据库管理界面。

创建启动类与控制器

java 复制代码
package com.example.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}
java 复制代码
package com.example.security.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "Welcome! You are authenticated.";
    }

    @GetMapping("/admin")
    public String admin() {
        return "Admin page.";
    }
}

启动与验证

  1. 启动应用,访问 http://localhost:8080,会跳转至登录页面。
  2. 使用默认用户 user / passwordadmin / password 登录,可看到欢迎信息。
  3. 访问 http://localhost:8080/h2-console,JDBC URL 使用 jdbc:h2:mem:testdb,用户名 sa,空密码,登录后可查看自动生成的表及数据:
sql 复制代码
SELECT * FROM USERS;
SELECT * FROM AUTHORITIES;

此时会发现数据库中已存在用户和对应的权限记录。

  1. 为了确认 JDBC 认证确实执行了数据库查询,可以在控制台日志中看到类似以下输出:
LOG 复制代码
[DEBUG] Executing prepared SQL query [select username, password, enabled from users where username = ?]
[DEBUG] Executing prepared SQL query [select username, authority from authorities where username = ?]

这就是 JdbcDaoImpl 内部加载用户和权限的 SQL,证明数据来自数据库。

自定义 UserDetails 的方向

若项目需要更复杂的用户属性(如邮箱、手机号等),可让实体类实现 UserDetails 接口,然后自定义 UserDetailsService 实现。典型的步骤如下:

  1. 编写 UserEntity 实现 UserDetails,重写所有方法,将数据库字段映射到对应方法。
  2. 创建 CustomUserDetailsService 实现 UserDetailsService,在 loadUserByUsername 中通过 JPA/MyBatis 查询用户实体并返回。
  3. 在安全配置中注入自定义 UserDetailsService,并使用 BCryptPasswordEncoder 替代明文编码。

认证流程依然遵循前文所述的时序图,只是将 UserDetailsService 的实现替换为自定义版本。

小结

本文梳理了 Spring Security 中两个核心接口 UserDetailsUserDetailsService 的设计意图与使用方法,并演示了如何利用内置的 JDBC 认证机制快速将用户存储从内存切换到数据库。

通过 H2 内存数据库和默认表结构,可以零额外 SQL 编写即实现可运行的数据库认证,这对于原型开发或初期验证非常高效。后续内容将深入探讨如何自定义 UserDetails 实体并集成生产级密码编码器,以及 AuthenticationManager 的详细工作机理。

相关推荐
m0_734571761 小时前
深入理解5g <七> 下行调度的流程
网络·5g
网硕互联的小客服1 小时前
反向路径过滤(rp_filter)是什么?多网卡服务器配置详解
运维·服务器·网络
爱从未走散1 小时前
面试八股题
java·面试·职场和发展
艺杯羹1 小时前
AI编程时代软件工程怎么学:从底层思维认知到驱动智能体的架构跃迁
java·人工智能·ai·架构·软件工程·ai编程
土司大王1 小时前
LeetCode hot100——51.N 皇后:Java 回溯模板、列与对角线剪枝、O(n!) 复杂度分析
java·leetcode·剪枝
疯狂打码的少年1 小时前
【计算机网络】IP地址与子网划分(IP地址分类与特殊地址)
网络·笔记·tcp/ip·计算机网络
GPFyes2 小时前
科东隔离传输失败E文本校验错误
运维·服务器·网络
是程不是序2 小时前
信创终端安全管理怎么落地?纳管、控权、护数据三段走
运维·网络·安全·电脑·系统安全
重生之小比特2 小时前
【Java SE】IDEA 调试 Debug 案例完整分析
java·ide·intellij-idea