spring boot 数据库认证 + CSRF -----12

 

 

从「用户角色表设计 → JPA 映射 → 注册接口 → 自定义认证 → CSRF 防护」完整闭环,替换之前的内存用户,账号密码真实存入 MySQL,逐行带通俗注释。

一、整体项目结构

plaintext
 
 
com.example.jobportal
├── entity
│   ├── SysUser.java          # 用户实体
│   └── SysRole.java          # 角色实体
├── repository
│   ├── SysUserRepository.java
│   └── SysRoleRepository.java
├── dto
│   └── RegisterRequest.java  # 注册请求参数
├── service
│   └── AuthService.java      # 注册业务逻辑
├── security
│   ├── CustomUserDetailsService.java  # 数据库用户查询
│   └── CustomAuthenticationProvider.java # 自定义认证逻辑
├── config
│   ├── SecurityConfig.java   # 安全配置
│   └── InitUserDataLoader.java # 初始化测试数据
└── controller
    └── AuthController.java   # 注册/登录接口

 

 

二、第一步:实体类设计(对应 131、132 集)

1. 角色实体 SysRole.java

java
 
运行
 
 
package com.example.jobportal.entity;

import jakarta.persistence.*;
import lombok.Data;

@Data
@Entity
@Table(name = "sys_roles")
public class SysRole {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 角色名规范:必须以 ROLE_ 开头,比如 ROLE_USER、ROLE_ADMIN
    private String roleName;
    // 角色描述
    private String roleDesc;
}
 

 

2. 用户实体 SysUser.java(多对多关联角色)

java
 
运行
 
 
package com.example.jobportal.entity;

import jakarta.persistence.*;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@Data
@Entity
@Table(name = "sys_users")
public class SysUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 用户名唯一,不允许为空
    @Column(unique = true, nullable = false)
    private String username;

    // 密码加密存储
    @Column(nullable = false)
    private String password;

    private String email;

    /**
     * 多对多关联:一个用户有多个角色,一个角色属于多个用户
     * @JoinTable:自动生成中间表 sys_user_roles
     * FetchType.EAGER:查用户时自动把角色一起查出来,认证需要
     */
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "sys_user_roles",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<SysRole> roles = new ArrayList<>();
}

 

 

三、第二步:Repository 接口(对应 132、134 集 派生查询)

JPA 派生查询:方法名按规则写,自动生成 SQL,不用手写

1. SysRoleRepository.java

java
 
运行
 
 
package com.example.jobportal.repository;

import com.example.jobportal.entity.SysRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface SysRoleRepository extends JpaRepository<SysRole, Long> {
    // 根据角色名查询 → 自动生成 SQL: select * from sys_roles where role_name = ?
    Optional<SysRole> findByRoleName(String roleName);
}
 

 

2. SysUserRepository.java(对应 134、135 集)

java
 
运行
 
 
package com.example.jobportal.repository;

import com.example.jobportal.entity.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface SysUserRepository extends JpaRepository<SysUser, Long> {

    // 根据用户名查用户 → 派生查询
    Optional<SysUser> findByUsername(String username);

    // 判断用户名是否存在 → 对应135集:防止重复注册
    boolean existsByUsername(String username);
}

 

 

四、第三步:用户注册功能(对应 133、135 集)

1. 注册参数 DTO RegisterRequest.java

java
 
运行
 
 
package com.example.jobportal.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class RegisterRequest {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度3-20位")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码至少6位")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;
}

 

 

2. 业务逻辑 AuthService.java

java
 
运行
 
 
package com.example.jobportal.service;

import com.example.jobportal.dto.RegisterRequest;
import com.example.jobportal.entity.SysRole;
import com.example.jobportal.entity.SysUser;
import com.example.jobportal.exception.BusinessException;
import com.example.jobportal.repository.SysRoleRepository;
import com.example.jobportal.repository.SysUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthService {

    private final SysUserRepository userRepository;
    private final SysRoleRepository roleRepository;
    private final PasswordEncoder passwordEncoder;

    /**
     * 用户注册
     * 133集:注册接口开发
     * 135集:派生查询防重复用户
     */
    public String register(RegisterRequest request) {
        // 1. 校验用户名是否重复
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new BusinessException("该用户名已被注册");
        }

        // 2. 创建用户对象,密码用BCrypt哈希加密(不可逆,对应119-120集)
        SysUser user = new SysUser();
        user.setUsername(request.getUsername());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setEmail(request.getEmail());

        // 3. 给新用户分配默认角色:普通用户
        SysRole defaultRole = roleRepository.findByRoleName("ROLE_USER")
                .orElseThrow(() -> new BusinessException("系统角色异常"));
        user.getRoles().add(defaultRole);

        // 4. 保存到数据库
        userRepository.save(user);
        return "注册成功";
    }
}

 

 

3. 接口 AuthController.java

java
 
运行
 
 
package com.example.jobportal.controller;

import com.example.jobportal.common.Result;
import com.example.jobportal.dto.RegisterRequest;
import com.example.jobportal.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    // 注册接口:所有人可访问
    @PostMapping("/register")
    public Result<String> register(@Valid @RequestBody RegisterRequest request) {
        return Result.success(authService.register(request));
    }
}
 

 


五、第四步:初始化测试数据

项目启动自动创建角色和管理员账号,不用手动插数据
java
 
运行
 
 
package com.example.jobportal.config;

import com.example.jobportal.entity.SysRole;
import com.example.jobportal.entity.SysUser;
import com.example.jobportal.repository.SysRoleRepository;
import com.example.jobportal.repository.SysUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class InitUserDataLoader implements CommandLineRunner {

    private final SysUserRepository userRepository;
    private final SysRoleRepository roleRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public void run(String... args) {
        // 1. 初始化角色表
        if (roleRepository.count() == 0) {
            SysRole userRole = new SysRole();
            userRole.setRoleName("ROLE_USER");
            userRole.setRoleDesc("普通用户");
            roleRepository.save(userRole);

            SysRole adminRole = new SysRole();
            adminRole.setRoleName("ROLE_ADMIN");
            adminRole.setRoleDesc("管理员");
            roleRepository.save(adminRole);
        }

        // 2. 初始化管理员账号
        if (!userRepository.existsByUsername("admin")) {
            SysUser admin = new SysUser();
            admin.setUsername("admin");
            admin.setPassword(passwordEncoder.encode("admin123"));
            admin.setEmail("admin@job.com");
            // 管理员同时拥有两个角色
            admin.getRoles().add(roleRepository.findByRoleName("ROLE_ADMIN").get());
            admin.getRoles().add(roleRepository.findByRoleName("ROLE_USER").get());
            userRepository.save(admin);
        }
    }
}

 

 

六、第五步:数据库认证核心(对应 137、138 集)

1. 自定义用户查询服务 CustomUserDetailsService.java

Spring Security 认证时会自动调用它,从数据库查用户
java
 
运行
 
 
package com.example.jobportal.security;

import com.example.jobportal.entity.SysUser;
import com.example.jobportal.repository.SysUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final SysUserRepository userRepository;

    /**
     * 替换内存用户,从数据库加载用户
     * 对应138集:用数据库认证替换In-Memory
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查用户
        SysUser user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));

        // 把数据库里的角色,转成Spring Security能识别的权限格式
        List<SimpleGrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getRoleName()))
                .collect(Collectors.toList());

        // 返回标准的UserDetails对象
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}
 

 

2. 自定义认证提供者 CustomAuthenticationProvider.java

完全掌控认证流程,可加验证码、IP 限制等扩展
java
 
运行
 
 
package com.example.jobportal.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final CustomUserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    /**
     * 自定义认证全流程
     * 对应137集:自定义AuthenticationProvider,掌控认证逻辑
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 1. 取出前端传的用户名、明文密码
        String username = authentication.getName();
        String inputPassword = authentication.getCredentials().toString();

        // 2. 查数据库用户
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        // 3. 比对密码(明文和数据库哈希值比对)
        if (!passwordEncoder.matches(inputPassword, userDetails.getPassword())) {
            throw new BadCredentialsException("用户名或密码错误");
        }

        // 4. 认证成功,返回带权限的认证对象
        return new UsernamePasswordAuthenticationToken(
                userDetails,
                null,
                userDetails.getAuthorities()
        );
    }

    // 告诉Spring Security:这个处理器支持用户名密码认证
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

 

 

七、第六步:安全配置 + CSRF(对应 139-142 集)

java
 
运行
 
 
package com.example.jobportal.config;

import com.example.jobportal.security.CustomAuthenticationProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomAuthenticationProvider customAuthenticationProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // ========== CSRF防护(对应139-142集) ==========
                // 服务端渲染页面建议开启;纯前后端分离项目一般关闭
                // 学习阶段先开启,ignoringRequestMatchers 排除注册、公开接口
                .csrf(csrf -> csrf
                        .ignoringRequestMatchers("/api/auth/register", "/api/public/**")
                )

                // 跨域配置
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))

                // 接口权限规则
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/register", "/api/public/**").permitAll()
                        .requestMatchers("/api/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated()
                )

                // 开启默认表单登录页,方便测试数据库认证
                .formLogin(form -> form.permitAll())
                // 开启Basic认证,Postman测试用
                .httpBasic(basic -> {});

        // 注册我们自定义的认证提供者
        http.authenticationProvider(customAuthenticationProvider);

        return http.build();
    }

    // 密码加密器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 跨域配置
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(List.of("*"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowedMethods(List.of("*"));
        config.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}
 

 


八、补充:136 集 弱密码 / 泄露密码检查

课程里的 CompromisedPasswordChecker 是 Spring Security 提供的密码泄露检测,会调用公开库检查密码是否已被泄露。
java
 
运行
 
 
// 可选配置,需要联网,学习阶段了解即可
// @Bean
// public CompromisedPasswordChecker compromisedPasswordChecker() {
//     // 调用 Have I Been Pwned 公开API检查密码是否在泄露库中
//     return new HaveIBeenPwnedPasswordChecker();
// }
 

 

注册时调用 checker.check(password),如果返回泄露状态就拒绝注册。

九、测试方法 & 课程对应

1. 基础测试

  1. 启动项目,Hibernate 自动创建 3 张表:sys_userssys_rolessys_user_roles
  2. 访问 http://localhost:8080/login,输入 admin / admin123,登录成功 → 数据库认证生效
  3. POST 调用 /api/auth/register 注册新用户,重复用户名会提示已存在
  4. 用新账号登录,访问 /api/admin/xxx 会 403(普通用户无权限)

2. CSRF 测试(对应 141、142 集)

  • 开启 CSRF 后,直接 POST 受保护接口会返回 403
  • 正确做法:先 GET 访问页面,从响应里获取 CSRF Token,POST 请求时在请求头加 X-CSRF-TOKEN: 令牌值

3. 知识点对应表

表格
 
集数核心内容对应代码
131 用户角色表设计 SysUser、SysRole 实体
132 JPA 实体映射 + Repository 实体多对多注解、Repository 接口
133 用户注册 REST 接口 AuthService + AuthController
134 派生查询原理 findByUsername、existsByUsername
135 防止重复用户 existsByUsername 校验
136 泄露密码检查 CompromisedPasswordChecker
137 自定义 AuthenticationProvider CustomAuthenticationProvider
138 替换内存为数据库认证 CustomUserDetailsService
139-140 CSRF 攻击原理与防护理论 SecurityConfig 中 csrf 配置
141-142 CSRF 完整演示 开启 CSRF 后带令牌请求

十、和之前 JWT 的衔接

这套数据库认证是底层用户数据源,如果要继续用 JWT,只需要把之前 JWT 过滤器里的UserDetailsService换成这个CustomUserDetailsService即可,实现「JWT 令牌认证 + 用户数据存在数据库」的完整方案。
posted @ 2026-08-01 01:34  漫漫长路</>  阅读(4)  评论(0)    收藏  举报