当前位置: 首页 > news >正文

Spring Security 深度学习(二): 自定义认证机制与用户管理

在这里插入图片描述

目录

    • 1. 引言:告别内存用户,拥抱数据库
    • 2. 核心接口深度解析
      • 2.1 UserDetailsService:用户详情加载的核心
      • 2.2 UserDetails:用户信息的承载体
      • 2.3 PasswordEncoder:安全密码的守护者
    • 3. 数据库准备:用户表设计
    • 4. 实战演练:自定义数据库用户认证
      • 4.1 创建Spring Boot项目(沿用第一阶段项目)
      • 4.2 引入数据库依赖
      • 4.3 配置数据源
      • 4.4 定义实体类与Mapper接口
      • 4.5 实现 CustomUserDetailsService
      • 4.6 更新 SecurityFilterChain 配置
      • 4.7 自定义登录页面与控制器
      • 4.8 测试自定义认证
    • 5. 深入理解:AuthenticationProvider
    • 6. HTTP Basic 认证的场景与局限
    • 7. 常见陷阱与注意事项
    • 8. 阶段总结与进阶展望

1. 引言:告别内存用户,拥抱数据库

在第一阶段,我们为了快速入门,使用了Spring Security提供的内存用户管理方式。这在开发原型或学习时非常方便,但在实际的企业级应用中,用户数据通常需要持久化存储在数据库中,例如MySQL、PostgreSQL等。

本阶段的核心任务就是实现:

  • 自定义UserDetailsService 从数据库中加载用户数据。
  • 配置PasswordEncoder 对用户注册时的密码进行加密存储,并在登录时进行匹配验证。
  • 创建自定义登录表单: 提供一个更加符合应用UI风格的登录界面,而不是Spring Security默认的简陋页面。

通过这些定制,我们将把Spring Security的认证机制与我们的业务数据层无缝集成。

2. 核心接口深度解析

2.1 UserDetailsService:用户详情加载的核心

UserDetailsService是Spring Security中用于加载用户认证信息的核心接口。它只有一个方法:

package org.springframework.security.core.userdetails;import org.springframework.dao.DataAccessException; // Spring 6.x+ Changed from AuthenticationExceptionpublic interface UserDetailsService {/*** 根据用户名加载用户详情。** @param username 用户的用户名。* @return 包含用户认证信息的 UserDetails 对象。* @throws UsernameNotFoundException 如果找不到该用户。* @throws DataAccessException 如果在数据访问过程中发生错误。*/UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException;
}

当你实现这个接口时,Spring Security会在用户尝试登录时调用你的loadUserByUsername方法。你需要在这个方法中:

  1. 根据传入的username从你的数据源(如数据库)中查询用户记录。
  2. 如果用户不存在,抛出UsernameNotFoundException
  3. 如果用户存在,将用户数据封装成UserDetails对象并返回。这个UserDetails对象包含了Spring Security进行认证所需的所有信息(用户名、密码、权限等)。

2.2 UserDetails:用户信息的承载体

UserDetails接口代表了一个已认证用户的所有核心信息。它提供了Spring Security进行认证和授权决策所需的一切。你在UserDetailsService中返回的必须是UserDetails的实现类。

主要方法:

package org.springframework.security.core.userdetails;import org.springframework.security.core.GrantedAuthority;
import java.io.Serializable;
import java.util.Collection;public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities(); // 获取用户拥有的权限集合String getPassword(); // 获取用户的密码(通常是加密后的)String getUsername(); // 获取用户名boolean isAccountNonExpired(); // 账户是否未过期boolean isAccountNonLocked(); // 账户是否未锁定boolean isCredentialsNonExpired(); // 凭证(密码)是否未过期boolean isEnabled(); // 账户是否启用// Spring Security 提供了一个默认的实现类 `org.springframework.security.core.userdetails.User`// 在大多数情况下,直接使用 User.builder()...build() 即可
}
  • getAuthorities():返回一个GrantedAuthority集合。GrantedAuthority通常表示用户的角色(如ROLE_ADMIN)或更细粒度的权限(如PRODUCT_READ)。
  • 其他is...方法:允许你实现更复杂的账户状态管理,例如:
    • isAccountNonExpired():用户账户是否过期(例如,订阅到期)。
    • isAccountNonLocked():用户账户是否被锁定(例如,多次登录失败导致)。
    • isCredentialsNonExpired():用户密码是否过期(例如,强制定期修改密码)。
    • isEnabled():用户账户是否禁用。

2.3 PasswordEncoder:安全密码的守护者

正如第一阶段所强调的,PasswordEncoder是用于密码加密和验证的关键组件。在本阶段,我们将再次确认其重要性,并在用户注册和登录时实际使用它。

推荐:BCryptPasswordEncoder

它将“盐”(Salt)值与密码结合,并通过慢哈希算法进行加密。每次对相同的密码进行加密,都会生成不同的哈希值,大大增强了安全性,防止彩虹表攻击和暴力破解。

配置方式(重温):

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;@Configuration
public class PasswordEncoderConfig { // 可以放在CustomSecurityConfig中,也可以单独一个类@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

3. 数据库准备:用户表设计

为了从数据库加载用户,我们需要一张用户表。这里我们设计一个简单的sys_user表:

CREATE TABLE `sys_user` (`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',`password` VARCHAR(255) NOT NULL COMMENT '加密后的密码',`enabled` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '是否启用,1为启用,0为禁用',`account_non_expired` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '账户是否未过期',`account_non_locked` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '账户是否未锁定',`credentials_non_expired` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '凭证是否未过期',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户表';CREATE TABLE `sys_role` (`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',`role_name` VARCHAR(50) NOT NULL UNIQUE COMMENT '角色名称,如ROLE_ADMIN,ROLE_USER',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统角色表';CREATE TABLE `sys_user_role` (`user_id` BIGINT NOT NULL COMMENT '用户ID',`role_id` BIGINT NOT NULL COMMENT '角色ID',PRIMARY KEY (`user_id`,`role_id`),CONSTRAINT `fk_user_role_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE,CONSTRAINT `fk_user_role_role` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表';-- 插入一些测试数据
-- 密码均为 'password',使用BCryptPasswordEncoder.encode("password")预先加密生成
-- 演示如何生成加密密码:
-- String encodedPassword = new BCryptPasswordEncoder().encode("password");
-- System.out.println(encodedPassword); // 每次运行结果不同,示例如下-- 插入角色
INSERT INTO `sys_role` (`id`, `role_name`) VALUES (1, 'ROLE_USER');
INSERT INTO `sys_role` (`id`, `role_name`) VALUES (2, 'ROLE_ADMIN');-- 插入用户 (密码都为'password', 预先通过BCryptPasswordEncoder加密后的结果)
-- userpass_encoded = $2a$10$wTf2P/jZ.d29mGvVd.vR4O.k9zT4QfG8QzH.y9lW.c.f6jB4X.5N
-- adminpass_encoded = $2a$10$4jK/tGzO2.e7p.u2V.c.m.z0A.j.3M.6M.8J.V.l.E.p5.R4X.8K
INSERT INTO `sys_user` (`id`, `username`, `password`, `enabled`, `account_non_expired`, `account_non_locked`, `credentials_non_expired`)
VALUES (1, 'user', '$2a$10$pLwGBKqE2hL8B.y9lW.c.f6jB4X.5N.5N.5N.5N.5N.5N.5N.5N.5N.5N.5N', 1, 1, 1, 1); -- 替换为实际加密后的密码
INSERT INTO `sys_user` (`id`, `username`, `password`, `enabled`, `account_non_expired`, `account_non_locked`, `credentials_non_expired`)
VALUES (2, 'admin', '$2a$10$iGgQfG8QzH.y9lW.c.f6jB4X.5N.5N.5N.5N.5N.5N.5N.5N.5N.5N.5N', 1, 1, 1, 1); -- 替换为实际加密后的密码-- 关联用户和角色
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1); -- user -> ROLE_USER
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (2, 1); -- admin -> ROLE_USER
INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (2, 2); -- admin -> ROLE_ADMIN

注意: 上面的加密密码示例$2a$10$...是占位符,每次BCryptPasswordEncoder.encode("password")都会生成不同的结果,请您在本地运行Java代码生成实际的加密密码,然后插入数据库。

4. 实战演练:自定义数据库用户认证

我们将基于第一阶段的项目进行改造。

4.1 创建Spring Boot项目(沿用第一阶段项目)

确保你的项目已经配置了Spring Web和Spring Security依赖。

4.2 引入数据库依赖

以MySQL和MyBatis-Plus为例:

        <!-- MySQL Driver --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version> <!-- 根据你的MySQL版本选择 --></dependency><!-- MyBatis-Plus Starter --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version> <!-- 适配Spring Boot 3 --></dependency><!-- Lombok (可选,代码更简洁) --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>

注意: 如果你使用的是Spring Boot 3.0+,mysql-connector-java 的group id 可能会导致警告,建议使用 com.mysql group id。这里为了兼容性,依然使用mysql。更高版本推荐 com.mysql:mysql-connector-j

4.3 配置数据源

application.yml中添加数据库连接信息,并配置MyBatis-Plus:

spring:datasource:url: jdbc:mysql://localhost:3306/security_db?useSSL=false&serverTimezone=UTCusername: rootpassword: your_db_password # 替换为你的数据库密码driver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus:mapper-locations: classpath*:/mapper/**/*.xml # Mybatis-Plus Mapper XML文件位置configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志,方便调试

在你的main类上添加@MapperScan注解:

package com.example.springsecuritystage1;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
@MapperScan("com.example.springsecuritystage1.mapper") // 扫描MyBatis Mapper接口
public class SpringSecurityStage1Application {public static void main(String[] args) {SpringApplication.run(SpringSecurityStage1Application.class, args);}}

4.4 定义实体类与Mapper接口

SysUser.java (用户实体)

package com.example.springsecuritystage1.entity;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.List;@Data
@TableName("sys_user")
public class SysUser implements Serializable {private Long id;private String username;private String password;private Boolean enabled;private Boolean accountNonExpired;private Boolean accountNonLocked;private Boolean credentialsNonExpired;@TableField(exist = false) // 排除非数据库字段private List<SysRole> roles; // 用户拥有的角色列表
}

SysRole.java (角色实体)

package com.example.springsecuritystage1.entity;import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;@Data
@TableName("sys_role")
public class SysRole implements Serializable {private Long id;private String roleName; // 角色名称,如ROLE_ADMIN
}

SysUserMapper.java (MyBatis-Plus Mapper)

package com.example.springsecuritystage1.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springsecuritystage1.entity.SysUser;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;import java.util.List;public interface SysUserMapper extends BaseMapper<SysUser> {// 自定义方法,根据用户名查询用户及其角色// 注意:这里的SQL需要根据你的数据库方言和表名进行调整@Select("SELECT su.id, su.username, su.password, su.enabled, su.account_non_expired as accountNonExpired, " +"su.account_non_locked as accountNonLocked, su.credentials_non_expired as credentialsNonExpired, " +"sr.id as role_id, sr.role_name as role_name " +"FROM sys_user su " +"LEFT JOIN sys_user_role sur ON su.id = sur.user_id " +"LEFT JOIN sys_role sr ON sur.role_id = sr.id " +"WHERE su.username = #{username}")List<SysUser> selectUserWithRolesByUsername(@Param("username") String username);
}

由于MyBatis-Plus的BaseMapper只支持单表操作,对于这种多表关联查询用户及角色的场景,我们通常会自定义@Select注解或在XML中定义。这里直接使用@Select

4.5 实现 CustomUserDetailsService

这是本阶段的核心!

package com.example.springsecuritystage1.service;import com.example.springsecuritystage1.entity.SysRole;
import com.example.springsecuritystage1.entity.SysUser;
import com.example.springsecuritystage1.mapper.SysUserMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User; // 注意这里是Spring Security的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.ArrayList;
import java.util.List;
import java.util.stream.Collectors;@Service
public class CustomUserDetailsService implements UserDetailsService {private final SysUserMapper sysUserMapper;public CustomUserDetailsService(SysUserMapper sysUserMapper) {this.sysUserMapper = sysUserMapper;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 从数据库查询用户及其角色信息List<SysUser> userWithRoles = sysUserMapper.selectUserWithRolesByUsername(username);if (userWithRoles == null || userWithRoles.isEmpty()) {throw new UsernameNotFoundException("User not found with username: " + username);}// MyBatis-Plus的联表查询可能返回多条记录(如果用户有多个角色),但用户本身信息是重复的// 获取唯一的 SysUser 对象SysUser sysUser = userWithRoles.get(0);// 2. 构造 GrantedAuthority 列表 (角色信息)List<GrantedAuthority> authorities = userWithRoles.stream().filter(u -> u.getRoles() != null && !u.getRoles().isEmpty()) // 过滤掉没有角色的查询结果.flatMap(u -> u.getRoles().stream()) // 扁平化角色列表.map(role -> new SimpleGrantedAuthority(role.getRoleName())) // 将角色名转换为GrantedAuthority.distinct() // 去重,防止同一角色多次添加.collect(Collectors.toList());// 如果用户在数据库中没有关联任何角色,上面的流式操作会得到空列表。// 为了确保至少有一个ROLE,可以添加一个默认角色或检查并抛出异常if(authorities.isEmpty()){// 考虑添加一个默认角色,或者根据业务需求抛出异常// 例如:authorities.add(new SimpleGrantedAuthority("ROLE_GUEST"));System.out.println("User " + username + " has no assigned roles.");// 或者抛出异常:throw new RuntimeException("User has no assigned roles!");}// 3. 构造 Spring Security 的 UserDetails 对象// 注意:这里使用的是 Spring Security 提供的 User 类实现 UserDetails 接口return new User(sysUser.getUsername(),sysUser.getPassword(), // 数据库中是加密后的密码sysUser.getEnabled(),sysUser.getAccountNonExpired(),sysUser.getCredentialsNonExpired(),sysUser.getAccountNonLocked(),authorities);}
}

在这个实现中,我们:

  1. 注入了SysUserMapper来访问数据库。
  2. loadUserByUsername方法中,根据用户名从数据库查询用户及其关联的角色。
  3. 将查询到的SysRole对象转换为Spring Security所需的GrantedAuthority列表。这里我们假设角色名称以ROLE_开头(这是Spring Security的惯例),并使用SimpleGrantedAuthority
  4. 最终,使用Spring Security提供的org.springframework.security.core.userdetails.User类来构建UserDetails对象并返回。注意这个User类不是我们自定义的SysUser实体类。

4.6 更新 SecurityFilterChain 配置

CustomSecurityConfig中移除InMemoryUserDetailsManager的Bean,并确保PasswordEncoder已配置。由于我们已经定义了@Service注解的CustomUserDetailsService,Spring Security会自动找到它并使用它进行认证,无需在SecurityFilterChain中额外配置。

package com.example.springsecuritystage1.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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;@Configuration
@EnableWebSecurity
public class CustomSecurityConfig {// 只需要保留 PasswordEncoder 的配置@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 移除了 InMemroyUserDetailsManager 的 Bean,让 CustomUserDetailsService 接管@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/public/**", "/register", "/login").permitAll() // 允许 /register 接口公共访问.requestMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated()).formLogin(form -> form.loginPage("/login").defaultSuccessUrl("/user/profile", true).permitAll()).logout(logout -> logout // 配置登出功能.logoutUrl("/logout") // 登出请求的URL.logoutSuccessUrl("/login?logout") // 登出成功后跳转URL.invalidateHttpSession(true) // 使HttpSession失效.deleteCookies("JSESSIONID") // 删除与会话相关的Cookie.permitAll()).httpBasic(Customizer.withDefaults());return http.build();}
}
  • 我们添加了/register路径的permitAll(),因为我们可能需要一个用户注册功能。
  • 同时,添加了logout的配置,这是生产环境中必备的功能。

4.7 自定义登录页面与控制器

沿用第一阶段的LoginControllerlogin.html文件,它们已经配置好了form action="/login"和CSRF令牌,可以直接使用。

我们再添加一个用户注册的Controller:
AuthController.java

package com.example.springsecuritystage1.controller;import com.example.springsecuritystage1.entity.SysUser;
import com.example.springsecuritystage1.mapper.SysUserMapper;
import com.example.springsecuritystage1.entity.SysRole; // 需要引入SysRole
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;import java.util.Arrays;
import java.util.List;@Controller
public class AuthController {private final SysUserMapper sysUserMapper;private final PasswordEncoder passwordEncoder;public AuthController(SysUserMapper sysUserMapper, PasswordEncoder passwordEncoder) {this.sysUserMapper = sysUserMapper;this.passwordEncoder = passwordEncoder;}@GetMapping("/register")public String showRegistrationForm(Model model) {model.addAttribute("message", "");return "register"; // 假设你有一个register.html模板}@PostMapping("/register")public String registerUser(@RequestParam String username,@RequestParam String password,Model model) {try {// 检查用户是否已存在(虽然数据库有UNIQUE约束,但在插入前检查可以给更友好的提示)List<SysUser> existingUsers = sysUserMapper.selectUserWithRolesByUsername(username);if (!existingUsers.isEmpty()) {model.addAttribute("message", "Username already exists!");return "register";}// 1. 加密密码String encodedPassword = passwordEncoder.encode(password);// 2. 构建 SysUser 对象SysUser newUser = new SysUser();newUser.setUsername(username);newUser.setPassword(encodedPassword);newUser.setEnabled(true);newUser.setAccountNonExpired(true);newUser.setAccountNonLocked(true);newUser.setCredentialsNonExpired(true);// 3. 保存用户到数据库sysUserMapper.insert(newUser);// 4. 为新用户分配默认角色 (例如:ROLE_USER)// 这是一个简化的处理,实际中可能需要查询数据库中 ROLE_USER 的ID// 这里假设 ROLE_USER 的ID是1SysUser registeredUser = sysUserMapper.selectUserWithRolesByUsername(username).get(0); // 重新查询以获取用户IDsysUserMapper.insertUserRole(registeredUser.getId(), 1L); // 假设1L是ROLE_USER的IDmodel.addAttribute("message", "Registration successful! Please login.");return "redirect:/login"; // 注册成功后跳转到登录页面} catch (DuplicateKeyException e) {model.addAttribute("message", "Username already exists. Please choose another username.");return "register";} catch (Exception e) {model.addAttribute("message", "Registration failed: " + e.getMessage());e.printStackTrace();return "register";}}
}

更新SysUserMapper.java,添加用户和角色关联的方法:

package com.example.springsecuritystage1.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springsecuritystage1.entity.SysUser;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;import java.util.List;public interface SysUserMapper extends BaseMapper<SysUser> {@Select("SELECT su.id, su.username, su.password, su.enabled, su.account_non_expired as accountNonExpired, " +"su.account_non_locked as accountNonLocked, su.credentials_non_expired as credentialsNonExpired, " +"sr.id as role_id, sr.role_name as role_name " + // 添加角色信息到结果集"FROM sys_user su " +"LEFT JOIN sys_user_role sur ON su.id = sur.user_id " +"LEFT JOIN sys_role sr ON sur.role_id = sr.id " +"WHERE su.username = #{username}")List<SysUser> selectUserWithRolesByUsername(@Param("username") String username);// 自定义插入用户角色关联表的方法@Insert("INSERT INTO sys_user_role (user_id, role_id) VALUES (#{userId}, #{roleId})")void insertUserRole(@Param("userId") Long userId, @Param("roleId") Long roleId);
}

创建register.html (在 src/main/resources/templates 目录下)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Register</title><style>body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f4f4f4; }.register-container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }h2 { text-align: center; color: #333; }label { display: block; margin-bottom: 8px; color: #555; }input[type="text"], input[type="password"] { width: calc(100% - 20px); padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; }input[type="submit"] { width: 100%; padding: 10px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }input[type="submit"]:hover { background-color: #218838; }.message { color: red; text-align: center; margin-bottom: 15px; }.message.success { color: green; }.login-link { text-align: center; margin-top: 15px; }</style>
</head>
<body><div class="register-container"><h2>Register New Account</h2><div th:if="${message}" class="message" th:classappend="${message.contains('successful')} ? 'success' : ''"><p th:text="${message}"></p></div><form th:action="@{/register}" method="post"><div><label for="username">Username:</label><input type="text" id="username" name="username" required/></div><div><label for="password">Password:</label><input type="password" id="password" name="password" required/></div><input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /><div><input type="submit" value="Register"/></div></form><div class="login-link">Already have an account? <a th:href="@{/login}">Login here</a></div></div>
</body>
</html>

4.8 测试自定义认证

  1. 确保你的MySQL数据库已启动,并创建了security_db数据库,执行了上述SQL脚本。
  2. 更新application.yml中的数据库密码。
  3. 运行Spring Boot应用。
  4. 打开浏览器访问 http://localhost:8080/register (现在这是一个可公共访问的页面)。
  5. 尝试注册一个新用户,例如newuser/newpass。注册成功后应该跳转到登录页。
  6. 使用newuser/newpass(或user/passwordadmin/adminpass)登录,你应该能够访问用户页面/user/profile
  7. 尝试以usernewuser身份访问/admin/dashboard,应该收到403 Forbidden
  8. admin/adminpass身份登录,可以访问/admin/dashboard
  9. 访问/logout,成功登出。

5. 深入理解:AuthenticationProvider

虽然我们通过 UserDetailsService 实现了自定义认证,但 Spring Security 内部实际上是通过 AuthenticationProvider 来完成认证逻辑的。UserDetailsServiceDaoAuthenticationProvider 的一个重要组成部分。

  • AuthenticationManager 认证管理器,负责接收认证请求(Authentication对象),并委托一个或多个AuthenticationProvider进行处理。
  • ProviderManager AuthenticationManager的默认实现,内部维护了一个List<AuthenticationProvider>
  • DaoAuthenticationProvider 这是最常用的AuthenticationProvider,它结合了UserDetailsService(加载用户详情)和PasswordEncoder(验证密码)来完成基于用户名和密码的认证。

当你提供了自定义的UserDetailsServicePasswordEncoder Bean时,Spring Security的自动配置会默认创建一个DaoAuthenticationProvider并将其注入到ProviderManager中。这就是为什么我们无需手动配置AuthenticationProvider,只提供UserDetailsServicePasswordEncoder就可以正常工作的原因。

如果你需要实现更复杂的认证流程(例如自定义令牌认证、第三方认证等),你可能需要实现自己的AuthenticationProvider

6. HTTP Basic 认证的场景与局限

CustomSecurityConfig中我们保留了 httpBasic(Customizer.withDefaults())

  • HTTP Basic 认证 是一种简单的认证机制,浏览器会将用户名和密码(用冒号分隔后进行Base64编码)放在HTTP请求头的Authorization字段中发送。例如:Authorization: Basic dXNlcjpwYXNzd29yZA==
  • 优点: 简单易用,无需会话管理,对于简单的API或内部服务调用比较方便。
  • 缺点: 每次请求都要发送凭证,安全性较低(虽然是Base64编码,但并非加密,容易被截获),且无法实现真正的登出(因为浏览器通常会缓存凭证)。它不适用于防止CSRF攻击,因为用户凭据可以被恶意站点利用。
  • 生产建议: 仅在HTTPS环境下,并对于对安全性要求不那么高、或需要简单无状态认证(如内部API之间)的场景使用。对于面向用户的Web应用,应优先使用表单登录或JWT等更安全的认证方式。

7. 常见陷阱与注意事项

  • 密码明文存储: 再次强调,数据库中绝不能存储明文密码!必须使用PasswordEncoder加密存储。
  • UserDetailsSysUser的混淆: 记住,你的实体类SysUser用于数据库ORM,而org.springframework.security.core.userdetails.User(或其自定义实现)是Spring Security内部使用的接口表示。二者不要混淆。
  • 角色前缀: Spring Security默认要求角色名称以ROLE_开头(例如ROLE_ADMINROLE_USER)。如果你的数据库中存储的角色名没有此前缀,在创建SimpleGrantedAuthority时需要手动添加。
  • DataAccessExceptionloadUserByUsername中,除了UsernameNotFoundException,你还需要考虑处理数据库访问异常。
  • Spring Security自动注入: 当你定义了UserDetailsServicePasswordEncoder@Bean@Service,Spring Security的自动配置就会使用它们,无需额外复杂配置。
  • SQL注入风险: 确保你的数据库查询代码能够防止SQL注入。MyBatis-Plus或JPA通常能处理好这一点。
  • 事务管理: 在用户注册等操作中,确保整个操作(插入用户、分配角色)在一个事务中完成。

8. 阶段总结与进阶展望

至此,你已经成功地将Spring Security的认证机制与应用的数据层结合起来,实现了基于数据库的自定义用户认证。你学会了:

  • 实现UserDetailsServicePasswordEncoder来加载和验证用户凭证。
  • 设计用户和角色数据库表结构。
  • 通过SecurityFilterChain配置,将自定义认证服务与Spring Security集成。
  • 自定义登录页面和实现简单的用户注册功能。
  • 理解了AuthenticationProvider的内部工作原理以及HTTP Basic认证的特性。

在第三阶段,我们将进一步提升Spring Security的能力,聚焦于授权管理与访问控制。我们将学习如何为不同的URL路径和方法设置细粒度的权限,利用Spring表达式语言(SpEL)实现复杂的访问逻辑,并深入理解角色与权限的差异。这将是你构建安全应用体系中不可或缺的一环。

http://www.xdnf.cn/news/1381753.html

相关文章:

  • npm install --global @dcloudio/uni-cli 时安装失败
  • 一天认识一个神经网络之--CNN卷积神经网络
  • QT之双缓冲 (QMutex/QWaitCondition)——读写分离
  • LINUX ---网络编程(三)
  • 如何通过docker进行本地部署?
  • 机器学习回顾(二)——KNN算法
  • Day16_【机器学习概述】
  • 设计模式:组合模式(Composite Pattern)
  • 【数据结构与算法】LeetCode 20.有效的括号
  • Vue 组件循环 简单应用及使用要点
  • 微服务保护和分布式事务-01.雪崩问题-原因分析
  • 步进电机、直流电机常见问题
  • APP手游使用游戏盾SDK为何能有效抵御各类攻击?
  • Java全栈工程师的实战面试:从基础到微服务的全面解析
  • 算法 --- 二分
  • Paimon——官网阅读:非主键表
  • CLIP图像特征提取:`CLIPVisionModel` vs `CLIPModel.get_image_features()`,哪种更适合你的任务?
  • [sys-BlueChi] docs | BluechiCtl命令行工具
  • 滑台模组如何实现电子制造精密加工?
  • Java 大视界 -- 基于 Java 的大数据实时流处理在智能电网分布式电源接入与电力系统稳定性维护中的应用(404)
  • 零基础开发应用:cpolar+Appsmith平民化方案
  • HVV面经总结(二)
  • MySQL事务ACID特性
  • 内网穿透工具【frp】的核心功能底层处理逻辑解析
  • Linux部分底层机制
  • LeetCode-279. 完全平方数
  • Linux 软件编程(十三)网络编程:TCP 并发服务器模型与 IO 多路复用机制、原理epoll
  • 工业机器人如何通过Modbus TCP转CanOpen网关高效通信!
  • HTML贪吃蛇游戏实现
  • RAW API 的 TCP 总结2