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_ADMINprivate 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;@Overridepublic 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*/@Overridepublic 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,掌控认证逻辑*/@Overridepublic 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:这个处理器支持用户名密码认证
    @Overridepublic 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;@Beanpublic 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();}// 密码加密器
    @Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 跨域配置
    @Beanpublic 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 令牌认证 + 用户数据存在数据库」的完整方案。