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. 基础测试
- 启动项目,Hibernate 自动创建 3 张表:
sys_users、sys_roles、sys_user_roles - 访问
http://localhost:8080/login,输入admin / admin123,登录成功 → 数据库认证生效 - POST 调用
/api/auth/register注册新用户,重复用户名会提示已存在 - 用新账号登录,访问
/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 令牌认证 + 用户数据存在数据库」的完整方案。
浙公网安备 33010602011771号