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

掌握 Spring Security:认证、授权与高级功能全解析

        作为 Java 生态中最主流的安全框架,Spring Security 凭借其强大的扩展性、与 Spring 生态的无缝集成,成为企业级应用认证(Authentication)与授权(Authorization)的首选方案。本文基于两天的系统学习笔记,从基础入门到高级实战,带你全面掌握 Spring Security 的核心用法,帮你解决项目中的安全需求。

目录

一、引言:为什么选择 Spring Security?

1.1 Spring Security 核心定位

1.2 Spring Security vs Shiro:如何选型?

二、第一天:夯实认证基础

2.1 第一个 Spring Security 项目:5 分钟上手

步骤 1:导入依赖

步骤 2:体验默认行为

步骤 3:自定义默认账号密码

2.2 核心接口:UserDetailsService 详解

2.2.1 接口作用

2.2.2 关键参数与返回值

2.2.3 UserDetails 核心方法

2.2.4 简单实现示例

2.3 密码安全:PasswordEncoder 解析器

2.3.1 为什么需要 PasswordEncoder?

2.3.2 内置解析器对比

2.3.3 BCryptPasswordEncoder 核心用法

2.3.4 注入 PasswordEncoder Bean

2.4 自定义登录逻辑:从数据库获取用户

2.4.1 数据库表结构

2.4.2 导入 MyBatis 依赖

2.4.3 配置数据源与 MyBatis

2.4.4 编写 Mapper 接口与 XML

1. 实体类(User.java)

2. Mapper 接口(UserMapper.java)

3. Mapper XML(UserMapper.xml)

2.4.5 实现 UserDetailsService

2.5 自定义登录页面:告别默认样式

2.5.1 编写登录页面(login.html)

2.5.2 配置 SecurityConfig(关键)

2.5.3 编写控制器(处理成功 / 失败跳转)

2.5.4 编写成功 / 失败页面

2.6 认证常用配置:自定义成功 / 失败处理器

2.6.1 自定义登录成功处理器

2.6.2 自定义登录失败处理器

2.6.3 配置处理器到 SecurityConfig

2.7 深入理解:完整认证流程(面试重点)

步骤 1:请求进入 UsernamePasswordAuthenticationFilter

步骤 2:AuthenticationManager 分发认证任务

步骤 3:DaoAuthenticationProvider 执行认证

步骤 4:认证结果处理

流程总结图

三、第二天:精通授权与高级功能

3.1 记住我:Remember Me 功能实现

步骤 1:导入依赖(MyBatis 已导入)

步骤 2:配置 PersistentTokenRepository

步骤 3:配置 SecurityConfig

步骤 4:修改登录页面

3.2 安全退出:Logout 配置与扩展

3.2.1 默认退出行为

3.2.2 自定义退出配置

3.2.3 自定义退出成功处理器

3.3 访问控制:URL 匹配规则

3.3.1 antMatchers ():Ant 风格表达式(推荐)

3.3.2 anyRequest ():匹配所有请求

3.3.3 regexMatchers ():正则表达式匹配

注意:匹配顺序

3.4 内置访问控制方法

3.5 角色与权限判断

3.5.1 hasAuthority ():判断是否具有指定权限

3.5.2 hasAnyAuthority ():判断是否具有指定权限中的任意一个

3.5.3 hasRole ():判断是否具有指定角色

3.5.4 hasAnyRole ():判断是否具有指定角色中的任意一个

3.5.5 hasIpAddress ():判断是否来自指定 IP

3.6 友好提示:自定义 403 无权限处理

步骤 1:实现 AccessDeniedHandler

步骤 2:配置到 SecurityConfig

3.7 灵活控制:基于表达式的访问控制

自定义表达式逻辑

1. 自定义 Service

2. 配置到 SecurityConfig

3.8 注解驱动:基于注解的访问控制

3.8.1 开启注解支持

3.8.2 @Secured:判断角色(需 ROLE_前缀)

3.8.3 @PreAuthorize:方法执行前判断权限(推荐)

3.8.4 @PostAuthorize:方法执行后判断权限

3.9 视图集成:Thymeleaf 中使用 Spring Security

步骤 1:导入依赖

步骤 2:页面引入命名空间

步骤 3:获取认证信息(sec:authentication)

步骤 4:动态控制内容显示(sec:authorize)

3.10 安全防护:CSRF 原理与配置

3.10.1 什么是 CSRF?

3.10.2 Spring Security 的 CSRF 防护机制

3.10.3 开启 CSRF 防护(生产环境必须开启)

1. 修改 SecurityConfig

2. 表单中携带 CSRF 令牌

3. AJAX 请求携带 CSRF 令牌

四、总结与后续学习方向

后续学习方向


一、引言:为什么选择 Spring Security?

在开始之前,我们先明确两个核心问题:Spring Security 是什么? 以及什么时候该用它?

1.1 Spring Security 核心定位

Spring Security 是一个高度可定制的安全框架,基于 Spring IoC/DI 和 AOP,为应用提供声明式安全访问控制,核心解决两大问题:

  • 认证(Authentication):验证 “你是谁”(比如用户登录时验证账号密码);
  • 授权(Authorization):判断 “你能做什么”(比如普通用户不能访问管理员页面)。

它能替代传统重复的安全代码,支持表单登录、OAuth2.0、JWT、记住我等多种场景,且与 Spring Boot、Spring Cloud 无缝兼容。

1.2 Spring Security vs Shiro:如何选型?

很多人会纠结这两个框架,这里给出客观对比,帮你快速决策:

维度Spring SecurityShiro
生态集成与 Spring 生态深度绑定(Boot/Cloud)不依赖任何框架,可独立使用
社区支持Spring 官方维护,更新快、文档全Apache 项目,社区活跃但更新较慢
功能覆盖功能全面(OAuth2.0/JWT/LDAP 等)核心功能齐全(认证 / 授权 / 记住我)
上手难度略复杂,需理解 Spring 生态简单直观,API 友好,学习成本低

选型建议

  • 若项目基于 Spring Boot/Cloud,优先选 Spring Security(集成顺畅,无额外适配成本);
  • 若项目不依赖 Spring,或追求快速上手、轻量,选 Shiro;
  • 若团队熟悉 Spring,Spring Security 是长期更优解(功能扩展性更强)。

二、第一天:夯实认证基础

认证是安全的第一步,我们从最基础的项目搭建开始,逐步深入核心接口与自定义逻辑。

2.1 第一个 Spring Security 项目:5 分钟上手

Spring Boot 已将 Spring Security 封装为启动器,无需复杂配置,快速体验默认安全机制。

步骤 1:导入依赖

在 Spring Boot 项目的pom.xml中添加启动器:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
步骤 2:体验默认行为

启动项目后,Spring Security 会自动生效:

  • 拦截所有请求:未登录时,无论访问哪个 URL,都会跳转到内置登录页(/login);
  • 默认账号密码:用户名固定为user,密码会打印在控制台(格式如Using generated security password: 7c3018c2-1fba-4d18-bbf6-7d02f545cd91);
  • 登录后访问:输入账号密码,即可访问目标页面(如http://localhost:8080/login.html)。

步骤 3:自定义默认账号密码

不想用随机密码?在application.yml(或application.properties)中配置:

spring:security:user:name: bjsxt  # 自定义用户名password: bjsxt  # 自定义密码

2.2 核心接口:UserDetailsService 详解

        默认账号密码仅用于测试,实际项目中用户信息存储在数据库。UserDetailsService是 Spring Security 提供的用户信息查询接口,我们需实现它来从数据库获取用户。

2.2.1 接口作用

UserDetailsService只有一个核心方法:

public interface UserDetailsService {// 根据用户名查询用户信息,返回UserDetails(用户详情)UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
2.2.2 关键参数与返回值
  • 参数username:客户端提交的用户名(默认表单参数名是username,可自定义);
  • 返回值UserDetails:Spring Security 的用户详情接口,包含用户的账号、密码、权限等信息,常用实现类是org.springframework.security.core.userdetails.User
  • 异常UsernameNotFoundException:当用户名不存在时抛出,Spring Security 会自动识别为 “用户名不存在”。
2.2.3 UserDetails 核心方法

UserDetails接口定义了用户的核心属性,User实现类需满足这些要求:

public interface UserDetails extends Serializable {// 获取用户权限(如"ROLE_ADMIN"、"menu:sys")Collection<? extends GrantedAuthority> getAuthorities();// 获取密码(数据库中存储的加密后密码)String getPassword();// 获取用户名(客户端提交的用户名)String getUsername();// 账号是否未过期boolean isAccountNonExpired();// 账号是否未锁定boolean isAccountNonLocked();// 密码是否未过期boolean isCredentialsNonExpired();// 账号是否可用boolean isEnabled();
}
2.2.4 简单实现示例
@Service
public class MyUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 模拟从数据库查询用户(实际项目中替换为DAO查询)if (!"bjsxt".equals(username)) {throw new UsernameNotFoundException("用户名不存在");}// 2. 构造用户权限(多个权限用逗号分隔,通过AuthorityUtils转换)Collection<? extends GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,menu:sys");// 3. 返回UserDetails实现类(注意:密码需是加密后的,后续讲PasswordEncoder)return new User(username, "$2a$10$EixZaYb4rVw54n11oM4b4.3G7Xf5pF1s5D5a5B5c5D5e5F5g5H5j5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5", authorities);}
}

2.3 密码安全:PasswordEncoder 解析器

Spring Security 强制要求使用PasswordEncoder对密码进行加密存储,禁止明文存储(防止数据库泄露后密码直接被利用)。

2.3.1 为什么需要 PasswordEncoder?
  • 明文密码风险:数据库泄露后,攻击者可直接登录;
  • 加密算法要求:需支持单向加密(无法从密文反推明文)、盐值随机(相同明文加密后密文不同)。
2.3.2 内置解析器对比

Spring Security 提供多种解析器,其中BCryptPasswordEncoder是官方推荐(强哈希算法,支持盐值自动生成):

解析器特点状态
BCryptPasswordEncoder基于 BCrypt 算法,盐值自动生成,强度可配置推荐使用
Md5PasswordEncoder基于 MD5 算法,无盐值,易破解已弃用
ShaPasswordEncoder基于 SHA 算法,无盐值,易破解已弃用
NoOpPasswordEncoder不加密,明文存储仅测试用
2.3.3 BCryptPasswordEncoder 核心用法
// 1. 测试类中演示加密与匹配
@Test
public void testBCrypt() {// 初始化解析器(强度默认10,范围4-31,值越大加密越慢)PasswordEncoder encoder = new BCryptPasswordEncoder();// 2. 加密密码(明文"123456")String encodedPassword = encoder.encode("123456");System.out.println("加密后密码:" + encodedPassword); // 输出示例:$2a$10$EixZaYb4rVw54n11oM4b4.3G7Xf5pF1s5D5a5B5c5D5e5F5g5H5j5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5// 3. 匹配密码(明文 vs 加密后密码)boolean isMatch = encoder.matches("123456", encodedPassword);System.out.println("密码是否匹配:" + isMatch); // 输出true
}
2.3.4 注入 PasswordEncoder Bean

必须将PasswordEncoder注入 Spring 容器,否则 Spring Security 会报错:

@Configuration
public class SecurityConfig {// 注入BCryptPasswordEncoder@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

2.4 自定义登录逻辑:从数据库获取用户

        实际项目中,用户、角色、权限存储在数据库,需通过 MyBatis 查询。我们按 “表结构→依赖→配置→代码” 的顺序实现。

2.4.1 数据库表结构

需设计 5 张表(RBAC 权限模型):

-- 1. 用户表(存储账号密码)
CREATE TABLE `user` (`id` bigint PRIMARY KEY AUTO_INCREMENT,`username` varchar(20) NOT NULL UNIQUE,`password` varchar(64) NOT NULL -- 存储BCrypt加密后的密码
);-- 2. 角色表
CREATE TABLE `role` (`id` bigint PRIMARY KEY AUTO_INCREMENT,`name` varchar(20) NOT NULL -- 如"管理员"、"普通用户"
);-- 3. 用户-角色关联表(多对多)
CREATE TABLE `role_user` (`uid` bigint,`rid` bigint,FOREIGN KEY (`uid`) REFERENCES `user`(`id`),FOREIGN KEY (`rid`) REFERENCES `role`(`id`)
);-- 4. 菜单(权限)表
CREATE TABLE `menu` (`id` bigint PRIMARY KEY AUTO_INCREMENT,`name` varchar(20) NOT NULL,`url` varchar(100),`parent_id` bigint,`permission` varchar(20) NOT NULL -- 如"menu:sys"、"user:list"
);-- 5. 角色-菜单关联表(多对多)
CREATE TABLE `role_menu` (`mid` bigint,`rid` bigint,FOREIGN KEY (`mid`) REFERENCES `menu`(`id`),FOREIGN KEY (`rid`) REFERENCES `role`(`id`)
);-- 插入测试数据
INSERT INTO `user` VALUES (1, '张三', '$2a$10$EixZaYb4rVw54n11oM4b4.3G7Xf5pF1s5D5a5B5c5D5e5F5g5H5j5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5');
INSERT INTO `role` VALUES (1, '管理员'), (2, '普通用户');
INSERT INTO `role_user` VALUES (1, 1);
INSERT INTO `menu` VALUES (1, '系统管理', '', 0, 'menu:sys'), (2, '用户管理', '', 0, 'menu:user');
INSERT INTO `role_menu` VALUES (1, 1), (2, 1), (2, 2);
2.4.2 导入 MyBatis 依赖
<!-- MyBatis启动器 -->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version>
</dependency>
<!-- MySQL驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.11</version>
</dependency>
2.4.3 配置数据源与 MyBatis

application.yml中配置:

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8username: rootpassword: 123456mybatis:type-aliases-package: com.bjsxt.pojo  # 实体类别名包mapper-locations: classpath:mybatis/*.xml  # Mapper XML路径
2.4.4 编写 Mapper 接口与 XML
1. 实体类(User.java)
public class User {private Long id;private String username;private String password;// getter/setter
}
2. Mapper 接口(UserMapper.java)
@Mapper
public interface UserMapper {// 根据用户名查询用户User selectByUsername(String username);// 根据用户名查询权限(menu.permission)List<String> selectPermissionByUsername(String username);// 根据用户名查询角色(role.name)List<String> selectRoleByUsername(String username);
}
3. Mapper XML(UserMapper.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bjsxt.mapper.UserMapper"><!-- 根据用户名查询用户 --><select id="selectByUsername" parameterType="string" resultType="user">SELECT id, username, password FROM user WHERE username = #{username}</select><!-- 根据用户名查询权限 --><select id="selectPermissionByUsername" parameterType="string" resultType="string">SELECT m.permission FROM user uJOIN role_user ru ON u.id = ru.uidJOIN role r ON ru.rid = r.idJOIN role_menu rm ON r.id = rm.ridJOIN menu m ON rm.mid = m.idWHERE u.username = #{username}</select><!-- 根据用户名查询角色 --><select id="selectRoleByUsername" parameterType="string" resultType="string">SELECT r.name FROM user uJOIN role_user ru ON u.id = ru.uidJOIN role r ON ru.rid = r.idWHERE u.username = #{username}</select></mapper>
2.4.5 实现 UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 查询用户User user = userMapper.selectByUsername(username);if (user == null) {throw new UsernameNotFoundException("用户名不存在");}// 2. 查询权限(menu.permission)List<String> permissions = userMapper.selectPermissionByUsername(username);// 3. 查询角色(需拼接"ROLE_"前缀,符合Spring Security规范)List<String> roles = userMapper.selectRoleByUsername(username);roles = roles.stream().map(role -> "ROLE_" + role).collect(Collectors.toList());// 4. 合并权限与角色(Spring Security中角色也是一种权限)List<String> allAuthorities = new ArrayList<>();allAuthorities.addAll(permissions);allAuthorities.addAll(roles);// 5. 转换为GrantedAuthority集合Collection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(allAuthorities.toArray(new String[0]));// 6. 返回UserDetailsreturn new User(user.getUsername(),user.getPassword(), // 数据库中已加密的密码true,  // 账号未过期true,  // 账号未锁定true,  // 密码未过期true,  // 账号可用authorities);}
}

2.5 自定义登录页面:告别默认样式

默认登录页样式简陋,实际项目需替换为自定义页面。

2.5.1 编写登录页面(login.html)

放在src/main/resources/static目录下(静态资源目录):

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>自定义登录页</title>
</head>
<body><!-- action需与配置的loginProcessingUrl一致 --><form action="/doLogin" method="post"><div><label>用户名:</label><input type="text" name="username"> <!-- 参数名需与配置一致 --></div><div><label>密码:</label><input type="password" name="password"> <!-- 参数名需与配置一致 --></div><div><button type="submit">登录</button></div></form>
</body>
</html>
2.5.2 配置 SecurityConfig(关键)

注意:Spring Boot 2.7 后,WebSecurityConfigurerAdapter 已过时,需用SecurityFilterChain Bean 方式配置:

@Configuration
public class SecurityConfig {@Autowiredprivate MyUserDetailsService userDetailsService;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 1. 配置表单登录.formLogin().loginPage("/login.html") // 自定义登录页面路径(未登录时跳转).loginProcessingUrl("/doLogin") // 登录请求处理路径(无需写Controller).successForwardUrl("/toMain") // 登录成功后转发路径(POST请求).failureForwardUrl("/toFail") // 登录失败后转发路径(POST请求).usernameParameter("username") // 自定义用户名参数名(默认username).passwordParameter("password") // 自定义密码参数名(默认password).and()// 2. 配置URL访问控制.authorizeRequests().antMatchers("/login.html", "/toFail").permitAll() // 放行登录页和失败页.anyRequest().authenticated() // 其他所有请求需认证.and()// 3. 关闭CSRF防护(暂时关闭,后续详解).csrf().disable();return http.build();}// 配置AuthenticationManager(指定UserDetailsService和PasswordEncoder)@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}
}
2.5.3 编写控制器(处理成功 / 失败跳转)
@Controller
public class LoginController {// 登录成功转发(POST请求)@PostMapping("/toMain")public String toMain() {return "redirect:/main.html"; // 重定向到main.html(避免表单重复提交)}// 登录失败转发(POST请求)@PostMapping("/toFail")public String toFail() {return "redirect:/fail.html"; // 重定向到fail.html}
}
2.5.4 编写成功 / 失败页面

static目录下新建main.htmlfail.html

<!-- main.html -->
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>首页</title>
</head>
<body><h1>登录成功!欢迎访问首页</h1>
</body>
</html><!-- fail.html -->
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登录失败</title>
</head>
<body><h1>用户名或密码错误,请重新登录!</h1><a href="/login.html">返回登录页</a>
</body>
</html>

2.6 认证常用配置:自定义成功 / 失败处理器

successForwardUrlfailureForwardUrl仅支持转发(POST 请求),若需重定向到外部链接(如百度)或返回 JSON,需自定义处理器。

2.6.1 自定义登录成功处理器

实现AuthenticationSuccessHandler接口:

@Component
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 1. 获取认证用户信息UserDetails userDetails = (UserDetails) authentication.getPrincipal();System.out.println("登录成功,用户名:" + userDetails.getUsername());System.out.println("用户权限:" + userDetails.getAuthorities());// 2. 自定义响应(示例:重定向到百度)response.sendRedirect("https://www.baidu.com");// 若为前后端分离,可返回JSON:// response.setContentType("application/json;charset=utf-8");// PrintWriter out = response.getWriter();// out.write("{\"code\":200,\"msg\":\"登录成功\"}");// out.flush();}
}
2.6.2 自定义登录失败处理器

实现AuthenticationFailureHandler接口:

@Component
public class MyLoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {// 1. 获取失败原因String msg = "登录失败";if (exception instanceof UsernameNotFoundException) {msg = "用户名不存在";} else if (exception instanceof BadCredentialsException) {msg = "密码错误";}// 2. 自定义响应(示例:返回JSON)response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();out.write("{\"code\":401,\"msg\":\"" + msg + "\"}");out.flush();}
}
2.6.3 配置处理器到 SecurityConfig
@Configuration
public class SecurityConfig {@Autowiredprivate MyLoginSuccessHandler loginSuccessHandler;@Autowiredprivate MyLoginFailureHandler loginFailureHandler;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").successHandler(loginSuccessHandler) // 替换successForwardUrl.failureHandler(loginFailureHandler) // 替换failureForwardUrl.usernameParameter("username").passwordParameter("password")// 其他配置不变....and().csrf().disable();return http.build();}
}

2.7 深入理解:完整认证流程(面试重点)

        掌握认证流程是理解 Spring Security 的核心,也是面试高频考点。流程如下(从用户提交登录请求到认证成功):

步骤 1:请求进入 UsernamePasswordAuthenticationFilter

用户提交登录请求(如/doLogin),首先被UsernamePasswordAuthenticationFilter拦截:

  • 从请求中提取用户名和密码(通过usernameParameterpasswordParameter配置);
  • 构造UsernamePasswordAuthenticationToken(未认证状态,仅包含用户名和密码);
  • token交给AuthenticationManager处理。
步骤 2:AuthenticationManager 分发认证任务

AuthenticationManager本身不处理认证,而是管理多个AuthenticationProvider(认证提供者),通过supports()方法判断哪个Provider支持当前认证类型(如表单登录对应DaoAuthenticationProvider)。

步骤 3:DaoAuthenticationProvider 执行认证

DaoAuthenticationProvider是表单登录的核心认证提供者,执行以下操作:

  1. 查询用户:调用UserDetailsService.loadUserByUsername(),从数据库获取UserDetails
  2. 密码匹配:通过PasswordEncoder.matches(),对比客户端提交的密码与数据库中的加密密码;
  3. 校验用户状态:检查UserDetailsisAccountNonExpired()isAccountNonLocked()等方法,确保用户状态正常;
  4. 构造认证成功 token:创建UsernamePasswordAuthenticationToken(已认证状态,包含用户信息和权限),返回给AuthenticationManager
步骤 4:认证结果处理
  • 认证成功AuthenticationManager将认证成功的token传递回UsernamePasswordAuthenticationFilter,过滤器调用loginSuccessHandler处理(如跳转、返回 JSON);
  • 认证失败:抛出AuthenticationException(如UsernameNotFoundExceptionBadCredentialsException),过滤器调用loginFailureHandler处理。
流程总结图
用户提交登录请求 → UsernamePasswordAuthenticationFilter(提取账号密码)→ 
AuthenticationManager(分发任务)→ DaoAuthenticationProvider(执行认证)→ 
UserDetailsService(查用户)→ PasswordEncoder(验密码)→ 
认证成功/失败 → 调用对应处理器

三、第二天:精通授权与高级功能

        认证解决 “你是谁”,授权解决 “你能做什么”。本节学习授权配置、记住我、退出登录、CSRF 防护等高级功能。

3.1 记住我:Remember Me 功能实现

        “记住我” 功能允许用户下次访问时无需重新登录,Spring Security 通过 Cookie 存储用户信息到客户端,服务器端存储令牌到数据库。

步骤 1:导入依赖(MyBatis 已导入)

“记住我” 依赖 Spring JDBC 存储令牌,若已导入 MyBatis 启动器,无需额外导入(MyBatis 包含 Spring JDBC)。

步骤 2:配置 PersistentTokenRepository

用于存储 “记住我” 令牌到数据库(自动创建persistent_logins表):

@Configuration
public class RememberMeConfig {@Autowiredprivate DataSource dataSource;@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);// 第一次启动时自动创建表(创建后注释,避免重复创建)// tokenRepository.setCreateTableOnStartup(true);return tokenRepository;}
}
步骤 3:配置 SecurityConfig
@Configuration
public class SecurityConfig {@Autowiredprivate MyUserDetailsService userDetailsService;@Autowiredprivate PersistentTokenRepository persistentTokenRepository;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 配置记住我.rememberMe().userDetailsService(userDetailsService) // 登录逻辑.tokenRepository(persistentTokenRepository) // 令牌存储.tokenValiditySeconds(60 * 60 * 24 * 7) // 令牌有效期(7天,默认2周).rememberMeParameter("rememberMe") // 表单参数名(默认remember-me).and()// 其他配置不变....csrf().disable();return http.build();}
}
步骤 4:修改登录页面

添加 “记住我” 复选框(参数名与rememberMeParameter一致):

<form action="/doLogin" method="post"><div><label>用户名:</label><input type="text" name="username"></div><div><label>密码:</label><input type="password" name="password"></div><div><input type="checkbox" name="rememberMe" value="true"> 记住我</div><div><button type="submit">登录</button></div>
</form>

3.2 安全退出:Logout 配置与扩展

Spring Security 默认支持退出登录,只需访问/logout即可,也可自定义配置。

3.2.1 默认退出行为
  • 退出 URL:/logout(POST 请求);
  • 退出成功后跳转:/login?logout
  • 退出操作:清除认证信息、销毁 Session、删除 “记住我” 令牌。
3.2.2 自定义退出配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.logout().logoutUrl("/doLogout") // 自定义退出URL.logoutSuccessUrl("/login.html") // 退出成功后跳转路径.deleteCookies("JSESSIONID", "remember-me") // 删除指定Cookie.clearAuthentication(true) // 清除认证信息(默认true).invalidateHttpSession(true) // 销毁Session(默认true).and()// 其他配置不变....csrf().disable();return http.build();
}
3.2.3 自定义退出成功处理器

若需返回 JSON(前后端分离场景),实现LogoutSuccessHandler

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();out.write("{\"code\":200,\"msg\":\"退出登录成功\"}");out.flush();}
}

配置到 SecurityConfig:

@Autowired
private MyLogoutSuccessHandler logoutSuccessHandler;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.logout().logoutUrl("/doLogout").logoutSuccessHandler(logoutSuccessHandler) // 替换logoutSuccessUrl// 其他配置不变....csrf().disable();return http.build();
}

3.3 访问控制:URL 匹配规则

授权的核心是 “哪些 URL 需要什么权限”,Spring Security 提供 3 种 URL 匹配方式:

3.3.1 antMatchers ():Ant 风格表达式(推荐)

支持?***通配符,最常用:

  • ?:匹配 1 个字符(如/user/?匹配/user/1,不匹配/user/12);
  • *:匹配 0 个或多个字符(如/user/*匹配/user/1/user/abc,不匹配/user/1/2);
  • **:匹配 0 个或多个目录(如/user/**匹配/user/1/user/1/2/user/abc/def)。

示例:

http.authorizeRequests().antMatchers("/login.html", "/doLogin").permitAll() // 放行登录相关.antMatchers("/js/**", "/css/**", "/images/**").permitAll() // 放行静态资源.antMatchers("/admin/**").hasRole("管理员") // /admin/**需管理员角色.anyRequest().authenticated(); // 其他请求需认证
3.3.2 anyRequest ():匹配所有请求

通常放在最后,作为 “兜底” 配置:

http.authorizeRequests().antMatchers("/login.html").permitAll().anyRequest().authenticated(); // 所有其他请求需认证
3.3.3 regexMatchers ():正则表达式匹配

适合复杂匹配场景(如匹配所有.js文件):

http.authorizeRequests().regexMatchers(".+[.]js").permitAll() // 放行所有.js文件.anyRequest().authenticated();
注意:匹配顺序

具体的规则要放在前面,笼统的规则要放在后面,否则会被覆盖。例如:

// 错误:anyRequest()放在前面,后面的规则无效
http.authorizeRequests().anyRequest().authenticated().antMatchers("/login.html").permitAll();// 正确:具体规则在前,兜底规则在后
http.authorizeRequests().antMatchers("/login.html").permitAll().anyRequest().authenticated();

3.4 内置访问控制方法

Spring Security 提供 6 种常用的内置访问控制方法,底层均通过access()实现:

方法作用示例
permitAll()允许任何人访问.antMatchers("/login.html").permitAll()
authenticated()需认证后访问.anyRequest().authenticated()
anonymous()允许匿名访问(与permitAll()类似,但会执行匿名过滤器).antMatchers("/home").anonymous()
denyAll()禁止任何人访问.antMatchers("/forbid").denyAll()
rememberMe()仅 “记住我” 用户可访问.antMatchers("/remember").rememberMe()
fullyAuthenticated()仅完全认证用户可访问(排除 “记住我” 用户).antMatchers("/admin").fullyAuthenticated()

示例:

http.authorizeRequests().antMatchers("/home").anonymous() // 匿名用户可访问首页.antMatchers("/remember").rememberMe() // “记住我”用户可访问.antMatchers("/admin").fullyAuthenticated() // 完全认证用户可访问.anyRequest().authenticated();

3.5 角色与权限判断

当用户已认证后,需进一步判断其角色或权限是否满足访问要求,Spring Security 提供 5 种方法:

3.5.1 hasAuthority ():判断是否具有指定权限

权限是UserDetailsgetAuthorities()返回的字符串(如menu:sysuser:list)。

示例:

// /sys/**需"menu:sys"权限
http.authorizeRequests().antMatchers("/sys/**").hasAuthority("menu:sys").anyRequest().authenticated();
3.5.2 hasAnyAuthority ():判断是否具有指定权限中的任意一个

示例:

// /sys/**需"menu:sys"或"menu:user"权限
http.authorizeRequests().antMatchers("/sys/**").hasAnyAuthority("menu:sys", "menu:user").anyRequest().authenticated();
3.5.3 hasRole ():判断是否具有指定角色

角色需满足 “ROLE_前缀” 规范:

  • 数据库中角色名是 “管理员”,则UserDetails中需存储为 “ROLE_管理员”;
  • 使用hasRole()时,参数无需加 “ROLE_”(底层自动拼接)。

示例:

// /admin/**需"管理员"角色(底层拼接为"ROLE_管理员")
http.authorizeRequests().antMatchers("/admin/**").hasRole("管理员").anyRequest().authenticated();
3.5.4 hasAnyRole ():判断是否具有指定角色中的任意一个

示例:

// /admin/**需"管理员"或"操作员"角色
http.authorizeRequests().antMatchers("/admin/**").hasAnyRole("管理员", "操作员").anyRequest().authenticated();
3.5.5 hasIpAddress ():判断是否来自指定 IP

示例:

// 仅127.0.0.1可访问/admin/**
http.authorizeRequests().antMatchers("/admin/**").hasIpAddress("127.0.0.1").anyRequest().authenticated();

3.6 友好提示:自定义 403 无权限处理

当用户无权限访问时,默认返回 403 错误页,体验差。需自定义 403 响应(如返回 JSON 或跳转自定义页面)。

步骤 1:实现 AccessDeniedHandler
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {// 前后端分离场景:返回JSONresponse.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403response.setContentType("application/json;charset=utf-8");PrintWriter out = response.getWriter();out.write("{\"code\":403,\"msg\":\"权限不足,请联系管理员!\"}");out.flush();// 非前后端分离场景:跳转自定义403页// response.sendRedirect("/403.html");}
}
步骤 2:配置到 SecurityConfig
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 配置异常处理.exceptionHandling().accessDeniedHandler(accessDeniedHandler) // 自定义403处理.and()// 其他配置不变....csrf().disable();return http.build();
}

3.7 灵活控制:基于表达式的访问控制

access()方法支持 Spring EL 表达式,可实现更灵活的权限判断,内置表达式与之前的方法对应:

表达式对应方法示例
permitAllpermitAll().antMatchers("/login").access("permitAll")
authenticatedauthenticated().anyRequest().access("authenticated")
hasAuthority('xxx')hasAuthority("xxx").antMatchers("/sys").access("hasAuthority('menu:sys')")
hasRole('xxx')hasRole("xxx").antMatchers("/admin").access("hasRole('管理员')")
isRememberMe()rememberMe().antMatchers("/remember").access("isRememberMe()")
自定义表达式逻辑

若内置表达式无法满足需求,可自定义 Service 方法,通过access()调用:

1. 自定义 Service
@Service
public class MyPermissionService {// 参数固定:HttpServletRequest(请求)、Authentication(认证信息)public boolean hasTwoPermissions(HttpServletRequest request, Authentication authentication) {// 1. 获取当前用户权限UserDetails userDetails = (UserDetails) authentication.getPrincipal();Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();// 2. 判断是否同时具有"sys:save"和"user:save"权限boolean hasSysSave = authorities.contains(new SimpleGrantedAuthority("sys:save"));boolean hasUserSave = authorities.contains(new SimpleGrantedAuthority("user:save"));return hasSysSave && hasUserSave;}
}
2. 配置到 SecurityConfig
@Autowired
private MyPermissionService permissionService;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests()// 调用自定义Service方法:需同时具有两个权限才能访问/bjsxt.antMatchers("/bjsxt").access("@permissionService.hasTwoPermissions(request, authentication)").anyRequest().authenticated();return http.build();
}

3.8 注解驱动:基于注解的访问控制

除了 URL 配置,还可通过注解在 Controller/Service 方法上直接控制权限,需先开启注解支持。

3.8.1 开启注解支持

在启动类或配置类上添加@EnableGlobalMethodSecurity(Spring Boot 2.7 + 推荐@EnableMethodSecurity):

@SpringBootApplication
// 开启@Secured、@PreAuthorize、@PostAuthorize注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SpringSecurityDemoApplication {public static void main(String[] args) {SpringApplication.run(SpringSecurityDemoApplication.class, args);}
}
3.8.2 @Secured:判断角色(需 ROLE_前缀)

@Secured仅支持角色判断,参数需加 “ROLE_” 前缀:

@Controller
public class SysController {// 需"ROLE_管理员"角色才能访问@Secured("ROLE_管理员")@RequestMapping("/admin/sys")public String sysManage() {return "sysManage";}
}
3.8.3 @PreAuthorize:方法执行前判断权限(推荐)

支持 Spring EL 表达式,可判断权限、角色、IP 等,灵活性最高:

@Controller
public class SysController {// 方法执行前判断:需"menu:sys"权限@PreAuthorize("hasAuthority('menu:sys')")@RequestMapping("/sys/list")public String sysList() {return "sysList";}// 方法执行前判断:需"管理员"角色或IP为127.0.0.1@PreAuthorize("hasRole('管理员') or hasIpAddress('127.0.0.1')")@RequestMapping("/sys/delete")public String sysDelete() {return "sysDelete";}
}
3.8.4 @PostAuthorize:方法执行后判断权限

方法执行后才判断权限(适合需返回值参与判断的场景,极少用):

@Controller
public class SysController {// 方法执行后判断:返回值的username等于当前用户名@PostAuthorize("returnObject.username == authentication.principal.username")@RequestMapping("/user/info")public User getUserInfo(Long id) {// 模拟从数据库查询用户User user = userService.getById(id);return user;}
}

3.9 视图集成:Thymeleaf 中使用 Spring Security

        非前后端分离项目中,常使用 Thymeleaf 渲染页面,可通过thymeleaf-extras-springsecurity5集成 Spring Security,实现 “根据权限动态显示内容”。

步骤 1:导入依赖
<!-- Thymeleaf启动器 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Thymeleaf-Spring Security集成 -->
<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId><version>3.0.4.RELEASE</version>
</dependency>
步骤 2:页面引入命名空间

在 Thymeleaf 页面中添加sec命名空间:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"xmlns:th="http://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head><meta charset="UTF-8"><title>Thymeleaf集成Spring Security</title>
</head>
<body><!-- 内容 -->
</body>
</html>
步骤 3:获取认证信息(sec:authentication)

通过sec:authentication获取当前用户的认证信息:

<!-- 获取用户名 -->
<p>当前登录用户:<span sec:authentication="name"></span></p><!-- 获取用户权限 -->
<p>用户权限:<span sec:authentication="authorities"></span></p><!-- 获取用户详情(UserDetails) -->
<p>用户名(从principal获取):<span sec:authentication="principal.username"></span></p><!-- 获取客户端IP -->
<p>客户端IP:<span sec:authentication="details.remoteAddress"></span></p><!-- 获取SessionID -->
<p>SessionID:<span sec:authentication="details.sessionId"></span></p>
步骤 4:动态控制内容显示(sec:authorize)

通过sec:authorize根据权限判断是否显示元素:

<!-- 具有"menu:sys"权限才显示“系统管理”按钮 -->
<button sec:authorize="hasAuthority('menu:sys')">系统管理</button><!-- 具有"管理员"角色才显示“用户管理”按钮 -->
<button sec:authorize="hasRole('管理员')">用户管理</button><!-- 匿名用户显示“登录”按钮 -->
<a sec:authorize="isAnonymous()" href="/login.html">登录</a><!-- 已认证用户显示“退出登录”按钮 -->
<a sec:authorize="isAuthenticated()" href="/doLogout">退出登录</a>

3.10 安全防护:CSRF 原理与配置

前面的配置中我们一直关闭csrf(),本节详解 CSRF 攻击与防护。

3.10.1 什么是 CSRF?

CSRF(Cross-site Request Forgery,跨站请求伪造)是一种攻击方式:攻击者利用用户的登录状态(Cookie 中的 SessionID),伪造用户请求访问受信任网站,执行恶意操作(如转账、删除数据)。

攻击流程

  1. 用户登录www.xxx.com,服务器生成 SessionID 并存储到 Cookie;
  2. 用户未退出www.xxx.com,访问攻击者的www.attacker.com
  3. www.attacker.comwww.xxx.com发起请求(如/transfer?to=attacker&money=1000);
  4. 浏览器自动携带www.xxx.com的 Cookie,服务器认为是用户本人操作,执行转账。
3.10.2 Spring Security 的 CSRF 防护机制

Spring Security 从 4.0 开始默认开启 CSRF 防护,核心原理是令牌验证

  1. 服务器生成随机 CSRF 令牌(_csrf),存储到 Session 和请求作用域;
  2. 客户端提交请求(如登录、表单提交)时,需携带该令牌;
  3. 服务器验证请求中的令牌与 Session 中的令牌是否一致,一致则允许访问,否则拒绝。
3.10.3 开启 CSRF 防护(生产环境必须开启)

注释掉csrf().disable(),并在表单中携带 CSRF 令牌:

1. 修改 SecurityConfig
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 移除csrf().disable(),默认开启.formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").successForwardUrl("/toMain").and().authorizeRequests().antMatchers("/login.html", "/toMain").permitAll().anyRequest().authenticated();return http.build();
}
2. 表单中携带 CSRF 令牌

Thymeleaf 页面中通过${_csrf.token}获取令牌:

<form action="/doLogin" method="post"><!-- 携带CSRF令牌(必须) --><input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"><div><label>用户名:</label><input type="text" name="username"></div><div><label>密码:</label><input type="password" name="password"></div><div><button type="submit">登录</button></div>
</form>
3. AJAX 请求携带 CSRF 令牌

前后端分离项目中,AJAX 请求需在 Header 中携带令牌:

// 获取CSRF令牌(可从页面元标签获取)
var csrfToken = $("meta[name='_csrf']").attr("content");
var csrfHeader = $("meta[name='_csrf_header']").attr("content");// AJAX请求
$.ajax({url: "/doLogin",type: "post",headers: {[csrfHeader]: csrfToken // 在Header中携带令牌},data: {username: "张三",password: "123456"},success: function(res) {console.log(res);}
});

页面元标签配置:

<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">

四、总结与后续学习方向

本文从基础到进阶,覆盖了 Spring Security 的核心功能:

  • 认证:用户登录、自定义登录逻辑、密码加密、登录页面定制;
  • 授权:URL 匹配、角色权限判断、表达式控制、注解控制;
  • 高级功能:记住我、退出登录、自定义 403、Thymeleaf 集成、CSRF 防护。

后续学习方向

  1. OAuth2.0 与 JWT:解决分布式系统认证(如第三方登录、前后端分离 token 认证);
  2. LDAP 认证:集成 LDAP 服务器实现企业级用户认证;
  3. 动态权限:从数据库加载 URL - 权限映射,实现权限动态配置;
  4. 安全审计:记录用户操作日志,追踪安全事件。

Spring Security 的学习重点在于理解流程(如认证流程、授权流程)和灵活配置(根据项目需求定制安全规则)。建议结合实际项目多练手,才能真正掌握其核心用法。

http://www.dtcms.com/a/440346.html

相关文章:

  • 做物流网站模块免费看国际短视频软件
  • 淄博企业网站建设公司网站建设开发流程按钮
  • 网站跳转怎么做360wordpress 图片显示
  • 有没有做花卉种子的网站啊会员卡系统一般哪里买
  • 51单片机外部中断
  • 网站开发需要注意的问题山东省住房和城乡建设厅门户网站
  • 个人网站注销原因包装设计效果图
  • 北京高端网站公司哪家好建设银行官方网站
  • 教育在线网站怎样做直播site之后网站在首页说明说明
  • 网页翻译网站聊大 网站设计
  • 做 58 那样的网站建站公司服务
  • 做网站work什开一个网上商城要多少钱
  • 免费网站托管权威的大良网站建设
  • 【Linux系统】进程替换
  • drupal 网站实例家装公司电话
  • 南京小程序网站开发郑州做系统集成的公司网站
  • 龙海建设局网站NET网站开发程序员
  • 惠州市惠城区建设局网站电商网站建设咨询
  • 襄阳市住房和城乡建设局网站做图片带字的网站
  • 建设网站材料可以下载吗最新国际形势最新消息
  • 网站登记查询做广告推广哪家好
  • wordpress 体育主题广州网站优化服务
  • 青羊区网站建设微信小程序插件开发
  • PID资源
  • 东莞市营销网站建设多级子分类 wordpress
  • 做网站图片视频加载慢厦门seo
  • 外贸视频网站开发网站建设与维护考试
  • 企业网站建设模板多少钱做装修效果图的网站有哪些软件
  • 义乌网站制作电话为什么北京一夜封了
  • 山西响应式网站平台wordpress 信息输入框