版本说明 :本教程基于 Spring Boot 3.x 和 Spring Security 6.x 版本,采用了新的 Lambda DSL 配置风格。如果你使用的是旧版本,配置方式会略有不同。
第一回:初来乍到,藏经阁危在旦夕
我叫小白,是一名刚飞升上来的"代码修仙者"。我的第一份差事,就是担任"星宿宗"的藏经阁管理员。
藏经阁,那可是宗门的核心重地:
-
一楼: 公共休息区,谁都能进 (
/,/home)。 -
二楼: 普通秘籍区,只有本门弟子才能翻阅 (
/books/**)。 -
三楼: 绝学禁区,只有长老才能进入 (
/secret/**)。
我刚上任第一天,老阁主就拍拍我的肩膀,语重心长地说:"小白啊,现在的藏经阁,谁都能上三楼,跟逛菜市场似的。我们的《如来神掌》和《九阳神功》秘籍危矣!你的任务,就是给它建立起一套'护阁大阵'!"
我一脸懵:"阁主,阵法一道,晚辈才疏学浅啊..."
老阁主神秘一笑,掏出一本古籍:"此乃 Spring Boot 心法,能让你快速开宗立派。再配合这本 Spring Security 阵法大全,可布下天罗地网!"
第二回:开宗立派,初布大阵 (项目初始化)
推理时刻: 要布阵,先得有地盘。用 Spring Boot 创建项目是最快的方式。
我按照古籍记载,在 pom.xml 这个"灵气汇聚阵"中,引入了两大核心依赖:
XML
<!-- Spring Boot 核心心法,提供内力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security 阵法核心,提供规则 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
然后,我创建了几个简单的"房间"(Controller):
java
@RestController
public class LibraryController {
@GetMapping("/")
public String home() {
return "欢迎来到藏经阁公共休息区!";
}
@GetMapping("/books/java")
public String getJavaBook() {
return "《Java 编程思想》";
}
@GetMapping("/secret/kungfu")
public String getSecretKungfu() {
return "《如来神掌》秘籍!";
}
}
我信心满满地启动了应用 (SpringBootApplication.run)。
诡异的事情发生了! 我访问首页 http://localhost:8080,没有看到欢迎语,反而跳转到了一个陌生的登录页面!用户名是 user,密码则在控制台的一长串乱码里。
严谨推理:
-
一旦引入
spring-boot-starter-security,Spring Boot 的 自动配置 机制就启动了。 -
它会为应用 自动套上一个默认的安全结界。
-
这个默认结界规定:所有请求都需要认证。
-
它还会自动生成一个随机密码的用户,并提供一个基础的登录页。
关于默认密码的详细说明:
当你第一次启动 Spring Security 应用时,会在控制台看到类似这样的信息:
Using generated security password: 9a2b8f7c-3d6e-4a5b-8c9d-0e1f2a3b4c5d
This generated password is for development use only. Your security configuration must be updated before running in production.
这个随机密码的生成逻辑:
-
Spring Boot 检测到项目中存在 Spring Security 但没有显式配置
UserDetailsService或AuthenticationManager时 -
会自动创建一个
InMemoryUserDetailsManager实例 -
生成一个用户名为
user的账户 -
密码是通过 UUID 随机生成器 创建的,确保每次启动应用时都不同
-
这是一种安全措施,强制开发者在生产环境中配置真实的用户管理
示例控制台输出:
org.springframework.security.web.context.SecurityContextHolderFilter@34567890, ...]
2023-10-01T10:30:45.124+08:00 INFO 12345 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 9a2b8f7c-3d6e-4a5b-8c9d-0e1f2a3b4c5d
This generated password is for development use only. Your security configuration must be updated before running in production.
所以,我还没开始布阵,Spring Security 已经用它的"默认阵法"把我的藏经阁保护起来了------虽然保护得有点蠢,连公共区都进不去。
第三回:自定义大阵,权限分明 (核心配置)
老阁主看了直摇头:"你这阵法敌我不分啊!看我的。"
他带我创建了一个名为 SecurityConfig 的"阵法核心枢纽"。
java
@Configuration
@EnableWebSecurity // 宣告:此乃本宗自定义安全大阵!
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// HttpSecurity 就是我们的"阵法编织器"
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/").permitAll() // 公共区,无需认证
.requestMatchers("/books/**").hasRole("USER") // 普通秘籍区,需"弟子"身份
.requestMatchers("/secret/**").hasRole("ADMIN") // 禁区,需"长老"身份
.anyRequest().authenticated() // 其他所有请求,都需要登录
)
.formLogin(withDefaults()); // 使用默认的登录页面
return http.build();
}
// 创建"身份令牌"发放处 - 基于真实数据库查询
@Bean
public UserDetailsService userDetailsService(UserRepository userRepository) {
return username -> {
// 从数据库中根据用户名查询用户信息
UserEntity userEntity = userRepository.findByUsername(username);
if (userEntity == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 查询用户的角色权限列表
List<SimpleGrantedAuthority> authorities = userRepository.findRolesByUserId(userEntity.getId())
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
// 构建Spring Security需要的UserDetails对象
return org.springframework.security.core.userdetails.User.builder()
.username(userEntity.getUsername())
.password(userEntity.getPassword()) // 数据库中存储的应该是加密后的密码
.authorities(authorities)
.accountExpired(!userEntity.isAccountNonExpired())
.accountLocked(!userEntity.isAccountNonLocked())
.credentialsExpired(!userEntity.isCredentialsNonExpired())
.disabled(!userEntity.isEnabled())
.build();
};
}
// 密码编码器 - 用于密码加密和验证
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt强哈希函数进行密码加密
return new BCryptPasswordEncoder();
}
}
对应的数据库实体类和Repository:
java
// 用户实体类
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private boolean accountNonExpired = true;
private boolean accountNonLocked = true;
private boolean credentialsNonExpired = true;
private boolean enabled = true;
// getters and setters
}
// 角色实体类
@Entity
@Table(name = "roles")
public class RoleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
// getters and setters
}
// 用户角色关联实体类
@Entity
@Table(name = "user_roles")
public class UserRoleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity user;
@ManyToOne
@JoinColumn(name = "role_id")
private RoleEntity role;
// getters and setters
}
// 用户Repository
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// 根据用户名查找用户
UserEntity findByUsername(String username);
// 根据用户ID查找角色列表
@Query("SELECT r.name FROM RoleEntity r " +
"JOIN UserRoleEntity ur ON ur.role.id = r.id " +
"WHERE ur.user.id = :userId")
List<String> findRolesByUserId(Long userId);
}
数据库表结构示例:
sql
-- 用户表
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL, -- 存储BCrypt加密后的密码
account_non_expired BOOLEAN DEFAULT TRUE,
account_non_locked BOOLEAN DEFAULT TRUE,
credentials_non_expired BOOLEAN DEFAULT TRUE,
enabled BOOLEAN DEFAULT TRUE
);
-- 角色表
CREATE TABLE roles (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
-- 用户角色关联表
CREATE TABLE user_roles (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
-- 插入测试数据(密码都是"password",但经过BCrypt加密)
INSERT INTO users (username, password) VALUES
('disciple', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVwUiW'),
('elder', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVwUiW');
INSERT INTO roles (name) VALUES ('USER'), ('ADMIN');
INSERT INTO user_roles (user_id, role_id) VALUES
(1, 1), -- disciple 有 USER 角色
(2, 1), -- elder 有 USER 角色
(2, 2); -- elder 有 ADMIN 角色
故事解读与严谨推理:
-
@EnableWebSecurity: 这是启动我们自定义安全规则的"咒语",它告诉 Spring Boot:"别用你的默认阵法了,用我的!" -
HttpSecurity: 这是整个故事的核心,我们的"阵法编织器"。通过配置它,我们可以精确控制访问规则。 -
authorizeHttpRequests: 这是 权限规则定义,是我们大阵的"识别逻辑"。-
.requestMatchers("/").permitAll(): 匹配根路径,permitAll()表示完全放行。推理: 这里不进行任何安全拦截。 -
.requestMatchers("/books/**").hasRole("USER"): 匹配/books/开头的所有请求,hasRole("USER")表示请求者必须拥有ROLE_USER角色。推理: 系统会检查当前登录用户是否具备该角色。 -
.anyRequest().authenticated(): 这是一个兜底策略,对于其他所有请求,只需要登录(认证)即可,不管是什么角色。
-
-
UserDetailsService: 这是 用户详情服务 ,是我们的"身份令牌发放处"。现在我们改为从真实数据库中查询用户信息和角色。推理: 当用户登录时,Spring Security 会调用这个服务,根据用户名从数据库查找用户的密码和角色信息,用于验证身份和授权。 -
PasswordEncoder: 这是 密码编码器,使用 BCrypt 强哈希算法对密码进行加密和验证,确保密码安全。
现在,让我们测试一下大阵效果:
-
访问
/: 直接进入!(符合permitAll) -
访问
/books/java: 跳转到登录页。-
用
disciple/password登录:成功看到《Java 编程思想》! -
用
elder/password登录:也能看到!(因为长老也有USER角色)
-
-
访问
/secret/kungfu: 跳转到登录页。-
用
disciple/password登录:结果:403 Forbidden 错误!禁止访问! 推理: 弟子只有USER角色,没有ADMIN角色,大阵识别出他权限不足。 -
用
elder/password登录:成功看到《如来神掌》!
-
完美!我们的护阁大阵开始起作用了!
第四回:识破阵法玄机------内置拦截器(过滤器链)
老阁主看我悟性不错,便带我走到大阵的幕后。只见一道道灵光(HTTP请求)进入藏经阁,需要经过一个长长的"过滤走廊",走廊里有各式各样的"拦截器弟子"在执勤。
"看,这就是 Spring Security 的 过滤器链 (Filter Chain),"老阁主说,"每个过滤器都是一个大阵的组成部分。"
几个你必须认识的"核心执勤弟子":
-
SecurityContextPersistenceFilter(身份凭证保管员)- 职责: 当一个请求来时,他从 Session 中取出用户的登录凭证(Authentication)。请求结束时,他再把凭证存回去。这样用户在一个会话中只需要登录一次。
-
UsernamePasswordAuthenticationFilter(账房先生)- 职责: 专管表单登录。当你在登录页提交用户名和密码时,就是他来处理的。他负责验证你的身份,并给你发放"身份令牌"。
-
FilterSecurityInterceptor(权限判官)- 职责: 这是 最重要 的拦截器之一!我们之前在
HttpSecurity里配置的所有访问规则 (hasRole,permitAll等),最终都是由他来执行的。他会在请求到达 Controller 之前,根据规则决定是"放行"还是"抛出异常(403)"。
- 职责: 这是 最重要 的拦截器之一!我们之前在
-
ExceptionTranslationFilter(异常处理外交官)-
职责: 他专门处理
FilterSecurityInterceptor抛出的异常。 -
推理流程:
-
如果
FilterSecurityInterceptor说:"此人未认证!",外交官就会引导用户去登录页(发起认证)。 -
如果
FilterSecurityInterceptor说:"此人权限不足!",外交官就会返回403 Forbidden错误。
-
-
推理链条总结:
一个请求 GET /secret/kungfu 的冒险之旅:
-
SecurityContextPersistenceFilter从 Session 中取出令牌,发现是"弟子"。 -
请求一路向前,没有触发登录,所以绕过了
UsernamePasswordAuthenticationFilter。 -
到达
FilterSecurityInterceptor!判官拿出规则手册一查:"/secret/** 需要 ADMIN 角色"。再一看令牌:"弟子,角色 USER"。权限不足!抛出异常! -
ExceptionTranslationFilter接到"权限不足"异常,直接返回403状态码。
第五回:阵法玄机------核心拦截器与源码详解
老阁主将我带到藏经阁的"过滤走廊"幕后,指着那一排排正在执勤的"拦截器弟子",说道:"小白,知其然,更要知其所以然。今日,我便传你这护阁大阵的核心运转法则!"
第一式:SecurityContextPersistenceFilter (身份凭证保管员)
职责 :他是整个过滤链的第一个和最后一个执勤弟子,负责在请求开始时从Session中取出用户凭证,并在请求结束时清理现场,防止信息泄露。
java
/**
* SecurityContextPersistenceFilter - 身份凭证保管员
*
* 核心源码逻辑分析:
* 1. 在请求开始时,从Session中加载SecurityContext(安全上下文)
* 2. 将SecurityContext设置到SecurityContextHolder中,供后续过滤器使用
* 3. 在请求结束时,清理SecurityContextHolder,防止线程复用导致的信息泄露
*/
public class SecurityContextPersistenceFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 在请求开始时,尝试从Session中获取SecurityContext(安全上下文)
// SecurityContext 包含当前用户的认证信息 Authentication
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 2. 将获取到的SecurityContext设置到SecurityContextHolder中
// SecurityContextHolder相当于一个全局的、线程安全的储物柜,后续过滤器都从这里拿用户信息
// 关键点:使用ThreadLocal实现,确保每个请求线程都有自己的安全上下文
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 3. 放行,让请求继续走后续的过滤器链
// 这里会调用下一个过滤器,最终会调用到FilterSecurityInterceptor进行权限判断
chain.doFilter(holder.getRequest(), holder.getResponse());
} finally {
// 4. 请求结束后,无论如何,清理SecurityContextHolder
// 这是非常重要的安全措施!防止线程池复用时,用户信息泄露到其他请求
SecurityContextHolder.clearContext();
// 同时也会将更新后的SecurityContext保存回Session(如果认证状态有变化)
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
}
}
}
第二式:UsernamePasswordAuthenticationFilter (账房先生)
职责 :专管表单登录,默认拦截 /login 的POST请求。他负责接收用户提交的用户名密码,并尝试进行认证。
java
/**
* UsernamePasswordAuthenticationFilter - 账房先生
*
* 核心源码逻辑分析:
* 1. 只处理/login路径的POST请求
* 2. 从请求参数中提取用户名和密码
* 3. 创建未认证的Authentication令牌
* 4. 委托给AuthenticationManager进行实际认证
*/
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 默认只处理 /login 的 POST 请求
// 这就是为什么我们提交登录表单时必须是POST到/login
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 1. 从请求中获取用户名和密码
// 默认从username和password参数获取,但可以重写这些方法
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 2. 使用获取到的信息,创建一个「未认证」的令牌 (Authentication)
// 此时的Authentication的authenticated属性为false
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 3. 设置一些额外的信息,如远程IP地址、Session ID等
// 这些信息在后续的审计日志等场景中很有用
setDetails(request, authRequest);
// 4. 将这个令牌交给「认证经理」(AuthenticationManager) 进行核实
// AuthenticationManager会找到合适的AuthenticationProvider来执行认证
// 如果认证成功,返回一个充满详细信息的Authentication对象(authenticated=true)
// 如果失败,则抛出AuthenticationException异常
return this.getAuthenticationManager().authenticate(authRequest);
}
// 从请求中获取用户名的默认实现
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter("username");
}
// 从请求中获取密码的默认实现
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter("password");
}
}
第三式:ExceptionTranslationFilter (异常处理外交官)
职责 :他站在 FilterSecurityInterceptor 的身后,专门处理在安全过滤链中抛出的两类异常:AuthenticationException(认证异常)和 AccessDeniedException(访问拒绝异常)。
java
/**
* ExceptionTranslationFilter - 异常处理外交官
*
* 核心源码逻辑分析:
* 1. 捕获后续过滤器抛出的异常
* 2. 如果是AuthenticationException,启动认证流程
* 3. 如果是AccessDeniedException,检查用户是否已认证
* 4. 根据情况返回登录页面或403错误
*/
public class ExceptionTranslationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
// 放行,让请求继续往下走(主要是走向最终的权限判官 FilterSecurityInterceptor)
chain.doFilter(request, response);
} catch (Exception e) {
// 捕获异常并进行判断
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(e);
// 1. 如果是「认证异常」(用户未登录或登录失败)
RuntimeException ase = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase != null) {
// 启动认证流程 - 比如跳转到登录页
handleAuthenticationException(request, response, chain, (AuthenticationException) ase);
return;
}
// 2. 如果是「访问拒绝异常」(权限不足)
ase = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
if (ase != null) {
// 处理权限不足的情况
handleAccessDeniedException(request, response, chain, (AccessDeniedException) ase);
return;
}
}
}
private void handleAuthenticationException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, AuthenticationException failed)
throws IOException, ServletException {
// 将AuthenticationException信息保存到SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(null);
// 重要:触发认证入口点,通常是跳转到登录页面
// 在表单登录中,这会重定向到登录页
this.authenticationEntryPoint.commence(request, response, failed);
}
private void handleAccessDeniedException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, AccessDeniedException denied)
throws IOException, ServletException {
// 获取当前认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 关键判断:如果用户是匿名用户(未登录)或者RememberMe用户
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
// 用户未登录,触发认证流程
this.authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException("Full authentication is required to access this resource"));
} else {
// 用户已登录但权限不足 - 返回403 Forbidden错误
this.accessDeniedHandler.handle(request, response, denied);
}
}
}
第四式:FilterSecurityInterceptor (权限判官)
职责 :这是整个安全链的最后一关,负责根据配置的权限规则做出最终的访问决策。
java
/**
* FilterSecurityInterceptor - 权限判官
*
* 核心源码逻辑分析:
* 1. 在请求到达Controller前进行拦截
* 2. 从SecurityContextHolder获取当前用户认证信息
* 3. 调用AccessDecisionManager进行权限决策
* 4. 根据决策结果决定是否放行
*/
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 创建FilterInvocation对象,封装请求信息
FilterInvocation fi = new FilterInvocation(request, response, chain);
// 核心:进行权限校验
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 如果权限校验通过,执行后续的过滤器链,最终到达Controller
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// 请求完成后的清理工作
super.finallyInvocation(token);
}
// 调用完成后的后置处理
super.afterInvocation(token, null);
}
// 在AbstractSecurityInterceptor中定义的核心方法
protected InterceptorStatusToken beforeInvocation(Object object) {
// 1. 获取当前请求对应的配置属性(就是我们配置的hasRole、permitAll等规则)
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
// 如果没有配置安全规则,直接放行
return null;
}
// 2. 从SecurityContextHolder中获取当前认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 3. 核心:委托给AccessDecisionManager进行权限决策
// AccessDecisionManager会调用一系列的AccessDecisionVoter进行投票
try {
this.accessDecisionManager.decide(authentication, object, attributes);
} catch (AccessDeniedException accessDeniedException) {
// 如果决策结果是拒绝访问,抛出AccessDeniedException
// 这个异常会被前面的ExceptionTranslationFilter捕获
throw accessDeniedException;
}
// 4. 如果权限校验通过,创建并返回InterceptorStatusToken
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
}
完整请求流程的源码级推演:
java
/**
* 一个请求的完整生命周期 - 源码级推演
* 请求:GET /secret/kungfu (由已认证但无权限的"弟子"用户发起)
*/
public void demonstrateRequestFlow() {
// 1. SecurityContextPersistenceFilter从Session中恢复SecurityContext
// SecurityContext包含弟子用户的Authentication对象(authenticated=true, authorities=[ROLE_USER])
// 2. 请求经过一系列过滤器,到达FilterSecurityInterceptor
// 3. FilterSecurityInterceptor.beforeInvocation()被调用:
// - 获取配置属性: [hasRole('ADMIN')]
// - 获取当前认证: 弟子用户(只有ROLE_USER角色)
// - 调用AccessDecisionManager.decide()
// 4. AccessDecisionManager进行投票决策:
// - RoleVoter检查:用户有[ROLE_USER],需要[ROLE_ADMIN]
// - 投票结果:ACCESS_DENIED
// 5. AccessDecisionManager抛出AccessDeniedException
// 6. ExceptionTranslationFilter捕获AccessDeniedException:
// - 检查用户已认证 → 调用AccessDeniedHandler
// - 返回403 Forbidden响应
// 7. 请求结束,SecurityContextPersistenceFilter清理SecurityContextHolder
}
终回:大道至简,万法归宗
通过这场"藏经阁守护战"和深入的源码分析,我们明白了:
-
集成如此简单: 只需一个依赖,Spring Boot 就为你带来了 Spring Security 的强大能力。
-
默认密码机制: Spring Security 会自动生成随机密码并在控制台显示,这是开发阶段的保护措施。
-
配置核心是
HttpSecurity: 就像编织阵法,你用它来精确指定 哪些路径需要什么权限。 -
用户与角色: 通过
UserDetailsService可以从数据库查询真实的用户和权限信息。 -
密码安全: 使用
BCryptPasswordEncoder对密码进行强加密存储。 -
理解过滤器链: 明白请求背后四大核心过滤器的工作流程和源码实现,是解决复杂权限问题的钥匙。
源码层面的核心收获:
-
SecurityContextPersistenceFilter通过 ThreadLocal 实现请求级别的安全上下文隔离 -
UsernamePasswordAuthenticationFilter是认证的入口,负责创建初始的 Authentication 对象 -
FilterSecurityInterceptor是权限决策的最终执行者,调用 AccessDecisionManager 进行投票 -
ExceptionTranslationFilter是异常处理的统一出口,将技术异常转换为用户友好的响应
后续修炼方向(你的下一篇博客主题):
-
自定义登录页面: 替换默认的登录页,设计符合宗门风格的登录界面。
-
记住我功能: 实现"记住我"功能,让弟子们一段时间内无需重复登录。
-
登录成功处理: 自定义登录成功后的跳转逻辑,根据用户角色跳转到不同页面。
-
退出登录: 实现安全的退出登录功能,清理用户会话。
-
探索 JWT: 为你的前后端分离架构,打造无状态的令牌安全机制。