SpringBoot整合SpringSecurity

一、SpringSecurity 基本概念

SpringSecurity 是 Spring 生态中用于身份认证(Authentication)和授权(Authorization)的安全框架,它基于 Servlet 过滤器链实现,提供了全面的安全解决方案,包括用户认证、角色权限控制、会话管理、密码加密等功能。

简单来说,它解决了两个核心问题:

  • 认证(Who are you?):验证用户身份(如用户名密码是否正确)。
  • 授权(What are you allowed to do?):验证用户是否有权限执行某个操作(如普通用户能否访问管理员页面)。

二、为什么使用 SpringSecurity

  1. 开箱即用:内置多种认证机制(表单登录、OAuth2、JWT 等),无需从零开发。
  2. 全面的安全防护:默认防御 CSRF、XSS、会话固定等常见攻击。
  3. 灵活扩展:支持自定义认证逻辑、权限规则、登录页面等。
  4. 与 Spring 生态无缝集成:完美兼容 SpringBoot、SpringCloud 等。
  5. 企业级标准:广泛应用于生产环境,稳定性和安全性经过验证。

三、SpringSecurity 实战教程

1. 环境准备

  • JDK
  • SpringBoot 2.7.x
  • Maven/Gradle

image-20251027110253151

案例 1:基础入门(公开 / 私有接口控制)

功能目标

  • 公开接口 /hello/public:无需登录,直接访问。
  • 私有接口 /hello/private:未登录跳转默认登录页,登录后访问。

1. 核心依赖(pom.xml)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- SpringBoot父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/>
    </parent>
    <groupId>com.yqd</groupId>
    <artifactId>SpringSecrity-Demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringSecrity-Demo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 配置类(关键:明确接口权限)

(1)SecurityConfig.java(安全过滤链)

package com.yqd.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity  // 启用 Spring Security 安全控制
public class SecurityConfig {

    // 核心:配置安全过滤链,控制接口访问权限
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 1. 关闭 CSRF(开发环境简化,避免拦截 GET/POST 请求)
                .csrf().disable()

                // 2. 配置接口访问权限(关键修复点)
                .authorizeHttpRequests(auth -> auth
                        // 公开接口:允许所有人访问(无需登录)
                        .antMatchers("/hello/public").permitAll()
                        // 其他所有接口:必须登录后访问
                        .anyRequest().authenticated()
                )

                // 3. 配置默认表单登录(未登录时跳转)
                .formLogin(form -> form
                        .permitAll()  // 登录页允许匿名访问
                );

        return http.build();
    }
}

3. 控制器(HelloController.java)

package com.yqd.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    // 公开接口:无需登录
    @GetMapping("/hello/public")
    public String publicHello() {
        return "公开资源:任何人可访问";
    }

    // 私有接口:需登录
    @GetMapping("/hello/private")
    public String privateHello() {
        return "私有资源:登录后可访问";
    }
}

4. 启动类(SpringSecurityDemo.java)

package com.yqd;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringSecurityDemo {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityDemo.class, args);
    }
}

5. 测试步骤

  1. 启动项目:控制台输出默认用户 user 和随机密码(格式:Using generated security password: xxxx),复制密码。
  2. 访问公开接口http://localhost:8080/hello/public → 直接返回公开资源内容。
  3. 访问私有接口http://localhost:8080/hello/private → 跳转默认登录页。
  4. 登录验证:输入用户名 user + 控制台随机密码 → 登录成功后返回私有资源内容。

案例 2:认证管理(基于数据库的自定义认证)

功能目标

替换默认用户,从 springbootdata 数据库查询用户(zhangsan/lisi),实现自定义认证(zhangsan 为普通用户,lisi 为管理员)。

SQL脚本

-- 选择使用数据库
USE springbootdata;
-- 创建表user并插入相关数据
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
                        `id` int(20) NOT NULL AUTO_INCREMENT,
                        `username` varchar(200) DEFAULT NULL,
                        `password` varchar(200) DEFAULT NULL,
                        `valid` tinyint(1) NOT NULL DEFAULT 1,
                        PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES ('1', 'zhangsan',
                           '$2a$10$7fWqX7Y010pMnyym/AHZX.3chIbnPZbj3N/iqcG4APCF.hC6CMh5a', '1');
INSERT INTO `user` VALUES ('2', 'lisi',
                           '$2a$10$7fWqX7Y010pMnyym/AHZX.3chIbnPZbj3N/iqcG4APCF.hC6CMh5a', '1');
-- 创建表priv并插入相关数据
DROP TABLE IF EXISTS `priv`;
CREATE TABLE `priv` (
                        `id` int(20) NOT NULL AUTO_INCREMENT,
                        `authority` varchar(20) DEFAULT NULL,
                        PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `priv` VALUES ('1', 'ROLE_COMMON');
INSERT INTO `priv` VALUES ('2', 'ROLE_ADMIN');
-- 创建表user_priv并插入相关数据
DROP TABLE IF EXISTS `user_priv`;
CREATE TABLE `user_priv` (
                             `id` int(20) NOT NULL AUTO_INCREMENT,
                             `user_id` int(20) DEFAULT NULL,
                             `priv_id` int(20) DEFAULT NULL,
                             PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `user_priv` VALUES ('1', '1', '1');
INSERT INTO `user_priv` VALUES ('2', '2', '2');

1. 更新pom.xml

在案例 1 基础上添加 MyBatis-Plus 和 MySQL 依赖:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- SpringBoot父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/>
    </parent>
    <groupId>com.yqd</groupId>
    <artifactId>SpringSecrity-Demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringSecrity-Demo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- MyBatis-Plus:简化数据库操作 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <!-- MySQL 驱动:连接数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Lombok(简化实体类) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 全局配置(application.yml)

# 全局配置:数据库、MyBatis-Plus、Thymeleaf 等
spring:
  # 数据库配置(适配你的 springbootdata 库)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springbootdata?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root  # 替换为你的数据库用户名
    password: 123456  # 替换为你的数据库密码

  # Thymeleaf 配置(后续页面案例用,开发环境关闭缓存)
  thymeleaf:
    cache: false

# MyBatis-Plus 配置
mybatis-plus:
  type-aliases-package: com.yqd.entity  # 实体类包路径(简化别名)
  configuration:
    map-underscore-to-camel-case: true  # 开启下划线转驼峰(如 user_id → userId)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印 SQL 日志(调试用)
  mapper-locations: classpath*:mapper/**/*.xml  # 可选:自定义 SQL 路径(本案例用注解 SQL)

# 日志配置(优化 Mapper 接口日志显示)
logging:
  level:
    com.yqd.mapper: debug

3. 实体类(适配数据库表)

(1)User.java(对应 user 表)

package com.yqd.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * 用户实体:对应数据库 user 表
 */
@Data
@TableName("user") // 绑定数据库表名
public class User {
    @TableId(type = IdType.AUTO) // 主键自增(适配表中 id 字段)
    private Long id;

    private String username; // 对应表中 username 字段(用户名)

    private String password; // 对应表中 password 字段(BCrypt 加密存储)

    private Integer valid; // 对应表中 valid 字段(1=启用,0=禁用)
}

(2)Priv.java(对应 priv 表)

package com.yqd.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * 权限实体:对应数据库 priv 表
 */
@Data
@TableName("priv") // 绑定数据库表名
public class Priv {
    @TableId(type = IdType.AUTO) // 主键自增
    private Long id;

    private String authority; // 对应表中 authority 字段(权限名称,如 ROLE_COMMON)
}

(3)UserPriv.java(对应 user_priv 表)

package com.yqd.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * 用户-权限关联实体:对应数据库 user_priv 表
 */
@Data
@TableName("user_priv") // 绑定数据库表名
public class UserPriv {
    @TableId(type = IdType.AUTO) // 主键自增
    private Long id;

    private Long userId; // 对应表中 user_id 字段(关联 user 表)

    private Long privId; // 对应表中 priv_id 字段(关联 priv 表)
}

4. Mapper 接口(MyBatis-Plus)

(1)UserMapper.java(核心:用户查询与权限查询)

package com.yqd.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yqd.entity.User;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 用户 Mapper 接口:继承 BaseMapper 获得 CRUD 能力,自定义查询方法
 */
@Repository // 标识为持久层组件(避免 IDE 报错)
public interface UserMapper extends BaseMapper<User> {

    /**
     * 自定义 SQL:根据用户名查询用户(适配 user 表字段)
     */
    @Select("SELECT id, username, password, valid FROM user WHERE username = #{username}")
    User selectByUsername(@Param("username") String username);

    /**
     * 自定义 SQL:根据用户ID查询权限(关联 user_priv 和 priv 表)
     */
    @Select("SELECT p.authority " +
            "FROM priv p " +
            "JOIN user_priv up ON p.id = up.priv_id " +
            "WHERE up.user_id = #{userId}")
    List<String> selectAuthoritiesByUserId(@Param("userId") Long userId);
}

(2)PrivMapper.java(可选:单独操作权限表)

package com.yqd.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yqd.entity.Priv;
import org.springframework.stereotype.Repository;

/**
 * 权限 Mapper 接口:继承 BaseMapper 获得 CRUD 能力
 */
@Repository
public interface PrivMapper extends BaseMapper<Priv> {
}

(3)UserPrivMapper.java(可选:单独操作关联表)

package com.yqd.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yqd.entity.UserPriv;
import org.springframework.stereotype.Repository;

/**
 * 用户-权限关联 Mapper 接口:继承 BaseMapper 获得 CRUD 能力
 */
@Repository
public interface UserPrivMapper extends BaseMapper<UserPriv> {
}

5. 认证核心(CustomUserDetailsService.java)

package com.yqd.service;

import com.yqd.mapper.UserMapper;
import com.yqd.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 自定义 UserDetailsService:认证核心逻辑,从数据库加载用户信息
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Resource
    private UserMapper userMapper; // 注入 UserMapper,查询数据库

    /**
     * 核心方法:Spring Security 认证时自动调用,根据用户名加载用户信息
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据用户名查询数据库用户
        User user = userMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("❌ 用户名不存在");
        }

        // 2. 校验用户是否启用(valid=1 为启用)
        if (user.getValid() != 1) {
            throw new UsernameNotFoundException("❌ 用户已禁用");
        }

        // 3. 根据用户ID查询权限列表
        List<String> authorityNames = userMapper.selectAuthoritiesByUserId(user.getId());
        if (authorityNames.isEmpty()) {
            throw new UsernameNotFoundException("❌ 用户无权限");
        }

        // 4. 封装权限为 Spring Security 所需的 GrantedAuthority 列表
        List<GrantedAuthority> authorities = authorityNames.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        // 5. 封装为 UserDetails 对象(返回给 Spring Security 进行认证)
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),        // 用户名
                user.getPassword(),        // 数据库中已加密的密码(BCrypt)
                true,                      // 账号是否启用(已校验)
                true,                      // 账号是否未过期(默认 true)
                true,                      // 密码是否未过期(默认 true)
                true,                      // 账号是否未锁定(默认 true)
                authorities                // 用户权限列表
        );
    }
}

6. 启动类

package com.yqd;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.yqd.mapper") // 扫描Mapper接口所在包
public class SpringSecurityDemo {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityDemo.class, args);
    }
}

7. PasswordConfig.java(密码加密器)

package com.yqd.config;

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;

/**
 * 密码加密器配置:Spring Security 5+ 强制要求密码加密
 */
@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用 BCrypt 加密算法(后续认证案例与数据库密码匹配)
        return new BCryptPasswordEncoder();
    }
}

8. 测试步骤

  1. 启动项目:控制台无报错,MyBatis-Plus 打印 SQL 日志。
  2. 访问私有接口http://localhost:8080/hello/private → 跳转默认登录页。
  3. 使用数据库用户登录:
    • 普通用户:用户名 zhangsan,密码 123456 → 登录成功,返回私有资源。
    • 管理员用户:用户名 lisi,密码 123456 → 登录成功,返回私有资源。
  4. 错误测试:输入不存在的用户名(如 wangwu)→ 登录页提示 “用户名或密码错误”。

案例 3:授权管理(URL 级 + 方法级权限控制)

功能目标

  • 图书列表页 /book/list:所有登录用户可访问(zhangsan/lisi)。
  • 图书管理页 /book/manag:仅管理员可访问(lisi),普通用户访问提示 403。

1. 修改pom文件,添加Thymeleaf依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- SpringBoot父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/>
    </parent>
    <groupId>com.yqd</groupId>
    <artifactId>SpringSecrity-Demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringSecrity-Demo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Thymeleaf 模板引擎:必须添加,否则无法解析 login.html -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- MyBatis-Plus:简化数据库操作 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <!-- MySQL 驱动:连接数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Lombok(简化实体类) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 添加WebMvcConfig.java

package com.yqd.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Spring MVC 配置:处理视图映射(页面跳转)
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 配置视图映射:无需控制器方法,直接跳转页面
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login"); // 登录页映射
        registry.addViewController("/main").setViewName("main"); // 首页映射(关键)
    }
}

3. 修改application.yml

# 全局配置:数据库、MyBatis-Plus、Thymeleaf 等
spring:
  # 数据库配置(适配你的 springbootdata 库)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springbootdata?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root  # 替换为你的数据库用户名
    password: 123456  # 替换为你的数据库密码

  # Thymeleaf 核心配置
  thymeleaf:
    cache: false  # 开发环境关闭缓存,实时加载页面修改
    prefix: classpath:/templates/  # 模板文件前缀(默认就是这个,显式配置避免冲突)
    suffix: .html  # 模板文件后缀(默认就是这个,显式配置更明确)
    encoding: UTF-8  # 编码格式
    mode: HTML5  # 模板模式

# MyBatis-Plus 配置
mybatis-plus:
  type-aliases-package: com.yqd.entity  # 实体类包路径(简化别名)
  configuration:
    map-underscore-to-camel-case: true  # 开启下划线转驼峰(如 user_id → userId)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印 SQL 日志(调试用)
  mapper-locations: classpath*:mapper/**/*.xml  # 可选:自定义 SQL 路径(本案例用注解 SQL)

# 日志配置(优化 Mapper 接口日志显示)
logging:
  level:
    com.yqd.mapper: debug

4. 补充配置(修改 SecurityConfig.java)

package com.yqd.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;

import javax.annotation.Resource;

/**
 * 授权管理配置:添加 URL 级授权和方法级授权支持
 */
@Configuration
@EnableWebSecurity
// 启用方法级授权注解(prePostEnabled=true 支持 @PreAuthorize 注解)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Resource
    private UserDetailsService userDetailsService; // 注入自定义认证逻辑

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // 1. URL 级授权:先于方法级授权生效
                .authorizeHttpRequests(auth -> auth
                        .antMatchers("/hello/public", "/login").permitAll() // 公开资源
                        .antMatchers("/book/list").authenticated() // 图书列表:所有登录用户
                        .antMatchers("/book/manag").hasRole("ADMIN") // 图书管理:仅管理员
                        .anyRequest().authenticated()
                )
                // 2. 自定义登录页(替换默认登录页)
                .formLogin(form -> form
                        .loginPage("/login") // 自定义登录页路径(对应 login.html)
                        .loginProcessingUrl("/doLogin") // 登录请求处理路径(表单提交地址)
                        .defaultSuccessUrl("/main", true) // 登录成功跳转首页(main.html)
                        .failureUrl("/login?error") // 登录失败跳转登录页(带错误参数)
                        .permitAll()
                );

        return http.build();
    }
}

2. 控制器(BookController.java)

package com.yqd.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 图书控制器:演示 URL 级和方法级授权
 */
@Controller
@RequestMapping("/book")
public class BookController {

    /**
     * 图书列表页:方法级授权(与 URL 级授权一致,双重保险)
     * isAuthenticated():表示“已登录的用户即可访问”
     */
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/list")
    public String bookList() {
        return "book_list"; // 跳转到 Thymeleaf 页面 book_list.html
    }

    /**
     * 图书管理页:方法级授权(仅管理员可访问)
     * hasRole("ADMIN"):表示“拥有 ROLE_ADMIN 角色的用户可访问”
     */
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/manag")
    public String bookManage() {
        return "book_manag"; // 跳转到 Thymeleaf 页面 book_manag.html
    }
}

3. Thymeleaf 页面(resources/templates)

(1)login.html(自定义登录页)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
    <style>
        .container { width: 350px; margin: 100px auto; }
        .error { color: red; margin: 10px 0; }
        input { margin: 5px 0; padding: 5px; width: 200px; }
        button { padding: 6px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
<div class="container">
    <h2>图书管理系统 - 登录</h2>
    <!-- 登录错误提示(如用户名不存在、密码错误) -->
    <div class="error" th:if="${param.error}">用户名或密码错误,请重试!</div>

    <!-- 登录表单:提交到 /doLogin(与 SecurityConfig 中 loginProcessingUrl 一致) -->
    <form th:action="@{/doLogin}" method="post">
        <div>
            <label>用户名:</label>
            <br>
            <input type="text" name="username" required placeholder="请输入用户名">
        </div>
        <div style="margin: 10px 0;">
            <label>密码:</label>
            <br>
            <input type="password" name="password" required placeholder="请输入密码">
        </div>
        <button type="submit">登录</button>
    </form>
</div>
</body>
</html>

(2)main.html(系统首页)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>系统首页</title>
    <style>
        .container { width: 800px; margin: 50px auto; }
        .menu { margin: 20px 0; }
        .menu a { margin-right: 20px; font-size: 18px; color: #007bff; text-decoration: none; }
        .user-info { text-align: right; margin-bottom: 20px; }
    </style>
</head>
<body>
<div class="container">
    </div>
    <h1>图书管理系统</h1>
    <div class="menu">
        <a th:href="@{/book/list}">图书列表</a>
        <a th:href="@{/book/manag}">图书管理</a>
    </div>
</div>
</body>
</html>

(3)book_list.html(图书列表页)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>图书列表</title>
    <style>
        .container { width: 800px; margin: 50px auto; }
        table { border-collapse: collapse; width: 100%; margin: 20px 0; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        th { background-color: #f8f9fa; }
        .back { margin-top: 20px; }
        .back a { color: #007bff; text-decoration: none; }
    </style>
</head>
<body>
<div class="container">
    <h2>图书列表(所有登录用户可访问)</h2>
    <table>
        <tr>
            <th>图书ID</th>
            <th>图书名称</th>
            <th>作者</th>
            <th>价格</th>
        </tr>
        <tr>
            <td>1</td>
            <td>Spring Boot 实战</td>
            <td>张三</td>
            <td>59.90 元</td>
        </tr>
        <tr>
            <td>2</td>
            <td>MyBatis-Plus 指南</td>
            <td>李四</td>
            <td>49.90 元</td>
        </tr>
        <tr>
            <td>3</td>
            <td>Spring Security 安全开发</td>
            <td>王五</td>
            <td>69.90 元</td>
        </tr>
    </table>
    <div class="back">
        <a th:href="@{/main}">返回首页</a>
    </div>
</div>
</body>
</html>

(4)book_manag.html(图书管理页)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>图书管理</title>
    <style>
        .container { width: 800px; margin: 50px auto; }
        .btn { padding: 6px 15px; margin-right: 10px; border: none; cursor: pointer; }
        .add-btn { background: #28a745; color: white; }
        .edit-btn { background: #ffc107; color: black; }
        .del-btn { background: #dc3545; color: white; }
        table { border-collapse: collapse; width: 100%; margin: 20px 0; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        th { background-color: #f8f9fa; }
        .back { margin-top: 20px; }
        .back a { color: #007bff; text-decoration: none; }
    </style>
</head>
<body>
<div class="container">
    <h2>图书管理(仅管理员可访问)</h2>
    <button class="btn add-btn">添加图书</button>
    <table>
        <tr>
            <th>图书ID</th>
            <th>图书名称</th>
            <th>作者</th>
            <th>价格</th>
            <th>操作</th>
        </tr>
        <tr>
            <td>1</td>
            <td>Spring Boot 实战</td>
            <td>张三</td>
            <td>59.90 元</td>
            <td>
                <button class="btn edit-btn">编辑</button>
                <button class="btn del-btn">删除</button>
            </td>
        </tr>
        <tr>
            <td>2</td>
            <td>MyBatis-Plus 指南</td>
            <td>李四</td>
            <td>49.90 元</td>
            <td>
                <button class="btn edit-btn">编辑</button>
                <button class="btn del-btn">删除</button>
            </td>
        </tr>
    </table>
    <div class="back">
        <a th:href="@{/main}">返回首页</a>
    </div>
</div>
</body>
</html>

4. 测试步骤

  1. 使用普通用户登录:zhangsan/123456→ 进入首页。
    • 访问 /book/list:正常显示图书列表。
    • 访问 /book/manag:提示 403 禁止访问(无管理员权限)。
  2. 使用管理员登录:lisi/123456→ 进入首页。
    • 访问 /book/manag:正常显示图书管理页,可看到 “添加 / 编辑 / 删除” 按钮。

案例 4:会话管理(控制登录状态与用户信息获取)

功能目标

  1. 限制同一用户最多 1 个设备登录(多端登录踢下线)。
  2. 设置会话超时时间(30 分钟)。
  3. 获取当前登录用户的用户名和权限。

1. 补充配置(修改 SecurityConfig.java)

package com.yqd.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;

import javax.annotation.Resource;

/**
 * 授权管理配置:添加 URL 级授权和方法级授权支持
 */
@Configuration
@EnableWebSecurity
// 启用方法级授权注解(prePostEnabled=true 支持 @PreAuthorize 注解)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Resource
    private UserDetailsService userDetailsService; // 注入自定义认证逻辑

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // 1. URL 级授权:先于方法级授权生效
                .authorizeHttpRequests(auth -> auth
                        .antMatchers("/hello/public", "/login").permitAll() // 公开资源
                        .antMatchers("/book/list").authenticated() // 图书列表:所有登录用户
                        .antMatchers("/book/manag").hasRole("ADMIN") // 图书管理:仅管理员
                        .anyRequest().authenticated()
                )
                // 2. 自定义登录页(替换默认登录页)
                .formLogin(form -> form
                        .loginPage("/login") // 自定义登录页路径(对应 login.html)
                        .loginProcessingUrl("/doLogin") // 登录请求处理路径(表单提交地址)
                        .defaultSuccessUrl("/main", true) // 登录成功跳转首页(main.html)
                        .failureUrl("/login?error") // 登录失败跳转登录页(带错误参数)
                        .permitAll()
                )

                // 会话管理核心配置
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 按需创建会话(默认)
                        .maximumSessions(1) // 同一用户最多 1 个设备登录(多端登录踢下线)
                        .expiredUrl("/login?expired") // 会话过期跳转地址
                );

        return http.build();
    }
}

2. 会话超时配置(修改 application.yml)

server:
  servlet:
    session:
      timeout: 1800s  # 会话超时时间:30分钟(单位:s)

3. 控制器(UserController.java:获取用户信息)

package com.yqd.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用户控制器:获取当前登录用户信息(会话管理核心)
 */
@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 获取当前登录用户的用户名和权限
     * 原理:从 SecurityContext 中获取认证信息(认证成功后自动存储)
     */
    @GetMapping("/info")
    public String getUserInfo() {
        // 1. 从 SecurityContextHolder 获取当前线程的认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            return "❌ 未登录,无法获取用户信息";
        }

        // 2. 提取 UserDetails 对象(存储用户核心信息)
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();

        // 3. 拼接用户信息返回
        StringBuilder info = new StringBuilder();
        info.append("✅ 当前登录用户信息\n");
        info.append("用户名:").append(userDetails.getUsername()).append("\n");
        info.append("用户权限:").append(userDetails.getAuthorities()).append("\n");
        info.append("会话ID:").append(SecurityContextHolder.getContext().getAuthentication().getDetails());

        return info.toString().replace("\n", "<br>"); // 网页端换行显示
    }
}

4. 测试步骤

  1. 多端登录限制:
    • 浏览器 1:登录 lisi/123456 → 访问 /user/info 正常显示信息。
    • 浏览器 2:登录 lisi/123456 → 登录成功,浏览器 1 自动被踢下线。
    • 浏览器 1 刷新 /user/info → 跳转 login?expired(会话过期)。
  2. 会话超时测试:
    • 登录后闲置 30 分钟 → 访问 /user/info → 跳转 login?expired
  3. 获取用户信息:
    • 登录 zhangsan → 访问 /user/info → 显示用户名 zhangsan 和权限 [ROLE_COMMON]
    • 登录 lisi → 访问 /user/info → 显示用户名 lisi 和权限 [ROLE_ADMIN]

案例 5:用户退出(安全销毁会话)

功能目标

实现 “点击退出→销毁会话→清除认证信息→跳转登录页” 的完整流程,优化用户体验。

1. 修改pom.xml,增加Thymeleaf 与 Spring Security 5 整合依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- SpringBoot父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/>
    </parent>
    <groupId>com.yqd</groupId>
    <artifactId>SpringSecrity-Demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringSecrity-Demo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- SpringSecurity -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Thymeleaf 模板引擎:必须添加,否则无法解析 login.html -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- Thymeleaf 与 Spring Security 5 整合依赖(关键) -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <!-- MyBatis-Plus:简化数据库操作 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <!-- MySQL 驱动:连接数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Lombok(简化实体类) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 补充配置(修改 SecurityConfig.java)

package com.itheima.securitydemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;

import javax.annotation.Resource;

/**
 * 用户退出配置:添加退出登录逻辑
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .antMatchers("/hello/public", "/login").permitAll()
                .antMatchers("/user/info").authenticated()
                .antMatchers("/book/list").authenticated()
                .antMatchers("/book/manag").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/doLogin")
                .successForwardUrl("/main")
                .failureUrl("/login?error")
                .permitAll()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .maximumSessions(1)
                .expiredUrl("/login?expired")
                .invalidSessionUrl("/login?invalid")
            )
            // 退出登录核心配置
            .logout(logout -> logout
                .logoutUrl("/doLogout") // 退出请求处理路径(表单提交地址)
                .logoutSuccessUrl("/login?logout") // 退出成功跳转地址(带提示参数)
                .deleteCookies("JSESSIONID") // 删除会话 Cookie(避免残留)
                .invalidateHttpSession(true) // 销毁当前会话(默认 true)
                .clearAuthentication(true) // 清除认证信息(默认 true)
                .permitAll() // 退出路径允许匿名访问
            );

        return http.build();
    }
}

3. 修改首页(main.html:添加退出按钮)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>系统首页</title>
    <style>
        .container { width: 800px; margin: 50px auto; }
        .menu { margin: 20px 0; }
        .menu a { margin-right: 20px; font-size: 18px; color: #007bff; text-decoration: none; }
        .user-info { text-align: right; margin-bottom: 20px; }
        .logout-btn { padding: 5px 15px; background: #dc3545; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
<div class="container">
    <div class="user-info" sec:authorize="isAuthenticated()">
        <!-- sec:authorize="isAuthenticated()":仅登录用户可见 -->
        欢迎您,<span sec:authentication="name"></span>!
        <form th:action="@{/doLogout}" method="post" style="display: inline;">
            <button type="submit" class="logout-btn">退出登录</button>
        </form>
    </div>
    <h1>图书管理系统</h1>
    <div class="menu">
        <a th:href="@{/book/list}">图书列表</a>
        <a th:href="@{/book/manag}">图书管理</a>
        <a th:href="@{/user/info}">查看用户信息</a>
    </div>
</div>
</body>
</html>

4. 修改登录页(login.html:添加退出成功提示)

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
    <style>
        .container { width: 350px; margin: 100px auto; }
        .error { color: red; margin: 10px 0; }
        .success { color: green; margin: 10px 0; }
        input { margin: 5px 0; padding: 5px; width: 200px; }
        button { padding: 6px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
<div class="container">
    <h2>图书管理系统 - 登录</h2>
    <!-- 退出成功提示 -->
    <div class="success" th:if="${param.logout}">✅ 退出成功!欢迎再次登录</div>
    <!-- 会话过期提示 -->
    <div class="error" th:if="${param.expired}">❌ 会话已过期,请重新登录</div>
    <!-- 登录错误提示 -->
    <div class="error" th:if="${param.error}">❌ 用户名或密码错误,请重试!</div>

    <form th:action="@{/doLogin}" method="post">
        <div>
            <label>用户名:</label>
            <br>
            <input type="text" name="username" required placeholder="请输入用户名">
        </div>
        <div style="margin: 10px 0;">
            <label>密码:</label>
            <br>
            <input type="password" name="password" required placeholder="请输入密码">
        </div>
        <button type="submit">登录</button>
    </form>
</div>
</body>
</html>

5. 测试步骤

  1. 登录系统:使用 lisi/123456 登录 → 进入首页。
  2. 点击退出:点击首页 “退出登录” 按钮 → 跳转登录页,显示 “✅ 退出成功!欢迎再次登录”。
  3. 验证会话销毁:退出后访问 /user/info/book/manag → 自动跳转登录页,需重新登录。
  4. Cookie 验证:浏览器 F12 打开 “应用”→“Cookie”→ 查看 JSESSIONID → 退出后已被删除。

总结

5 个案例覆盖 Spring Security 全流程,所有文件结构完整、代码可直接运行:

  1. 基础入门:掌握公开 / 私有接口控制,理解默认认证流程。
  2. 认证管理:对接数据库,实现自定义用户认证(核心)。
  3. 授权管理:URL 级 + 方法级权限控制,区分普通用户 / 管理员。
  4. 会话管理:控制多端登录、超时时间,获取用户信息。
  5. 用户退出:安全销毁会话,优化用户体验。
posted @ 2025-10-27 11:07  碧水云天4  阅读(26)  评论(0)    收藏  举报