掌握 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 {

    @Override
    public 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
    @Bean
    public 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.Driver
    url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8
    username: root
    password: 123456

mybatis:
  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 u
        JOIN role_user ru ON u.id = ru.uid
        JOIN role r ON ru.rid = r.id
        JOIN role_menu rm ON r.id = rm.rid
        JOIN menu m ON rm.mid = m.id
        WHERE u.username = #{username}
    </select>

    <!-- 根据用户名查询角色 -->
    <select id="selectRoleByUsername" parameterType="string" resultType="string">
        SELECT r.name 
        FROM user u
        JOIN role_user ru ON u.id = ru.uid
        JOIN role r ON ru.rid = r.id
        WHERE u.username = #{username}
    </select>

</mapper>
2.4.5 实现 UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public 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. 返回UserDetails
        return 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 {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public 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)
    @Bean
    public 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 {

    @Override
    public 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 {

    @Override
    public 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 {

    @Autowired
    private MyLoginSuccessHandler loginSuccessHandler;

    @Autowired
    private MyLoginFailureHandler loginFailureHandler;

    @Bean
    public 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 {

    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 第一次启动时自动创建表(创建后注释,避免重复创建)
        // tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
}
步骤 3:配置 SecurityConfig
@Configuration
public class SecurityConfig {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private PersistentTokenRepository persistentTokenRepository;

    @Bean
    public 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 {

    @Override
    public 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 {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
        // 前后端分离场景:返回JSON
        response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
        response.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 的学习重点在于理解流程(如认证流程、授权流程)和灵活配置(根据项目需求定制安全规则)。建议结合实际项目多练手,才能真正掌握其核心用法。

您可能感兴趣的与本文相关的镜像

Seed-Coder-8B-Base

Seed-Coder-8B-Base

文本生成
Seed-Coder

Seed-Coder是一个功能强大、透明、参数高效的 8B 级开源代码模型系列,包括基础变体、指导变体和推理变体,由字节团队开源

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

森林-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值