Spring Security 如何使用@PreAuthorize注解

发布于:2025-09-03 ⋅ 阅读:(18) ⋅ 点赞:(0)

🧱 第一步:环境准备

✅ 1. 创建数据库(MySQL)

-- 创建数据库,使用 utf8mb4 字符集支持 emoji 和多语言
CREATE DATABASE security_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 使用该数据库
USE security_demo;

-- 用户表结构
-- id: 主键自增
-- username: 唯一,不允许重复
-- password: 存储 BCrypt 加密后的密码(明文不可逆)
-- role: 存储用户角色,如 ROLE_ADMIN、ROLE_USER
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,  -- BCrypt 加密后长度约 60
    role VARCHAR(50) NOT NULL
);

-- 插入测试数据
-- 注意:密码 '123456' 已通过 BCrypt 加密(强度为 10)
-- 生成工具:https://www.devglan.com/online-tools/bcrypt-hash-generator
INSERT INTO user (username, password, role) VALUES 
('admin', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_ADMIN'),
('alice', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_USER'),
('bob', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_USER');

🔐 安全提示

  • 不要将明文密码存入数据库。
  • BCrypt 是 Spring Security 推荐的密码加密算法,自带盐值(salt),防彩虹表攻击。
  • $2a$10$... 中的 10 是加密强度(log rounds),值越大越安全但越慢。

✅ 2. Redis

# 确保 Redis 正在运行
redis-server

💡 用途说明

  • 缓存用户角色信息,避免每次请求都查询数据库。
  • 提升系统性能,尤其在高并发场景下。
  • 键名格式:user_role:用户名

📁 第二步:Spring Boot 项目结构

src/main/java/com/example/demo/
├── DemoApplication.java              # 主启动类
├── config/
│   ├── SecurityConfig.java           # 安全核心配置
│   └── MyBatisConfig.java            # MyBatis 配置(可选)
├── controller/
│   └── UserController.java           # 用户操作接口
├── entity/
│   └── User.java                     # 用户实体类
├── mapper/
│   └── UserMapper.java               # 数据访问接口
├── service/
│   ├── CustomUserDetailsService.java # 自定义用户认证逻辑
│   └── RedisService.java             # Redis 操作封装
└── util/
    └── PasswordUtil.java             # 密码工具类(未使用,建议补全)

📦 pom.xml 依赖详解

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.3</version> <!-- 使用最新稳定版 Spring Boot -->
        <relativePath/> <!-- 查找父 POM 从本地开始 -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-demo</name>

    <properties>
        <java.version>17</java.version> <!-- 推荐使用 LTS 版本 -->
    </properties>

    <dependencies>
        <!-- Web 支持:Tomcat + Spring MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 安全框架:Spring Security -->
        <!-- 提供认证、授权、CSRF、Session 等功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- MyBatis 启动器 -->
        <!-- 简化 MyBatis 配置,自动扫描 Mapper -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

        <!-- MySQL 驱动 -->
        <!-- 运行时依赖,编译时不需要 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Redis 支持 -->
        <!-- 用于缓存用户权限信息 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- Lombok -->
        <!-- 自动生成 getter/setter/toString 等方法 -->
        <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>
                <configuration>
                    <!-- 打包时排除 Lombok,避免运行时报错 -->
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

✅ 第三步:代码实现

1. DemoApplication.java - 主启动类

package com.example.demo;

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

/**
 * Spring Boot 主启动类
 * @SpringBootApplication 注解 = @Configuration + @EnableAutoConfiguration + @ComponentScan
 * 自动扫描 com.example.demo 包下所有组件
 */
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2. User.java - 实体类

package com.example.demo.entity;

import lombok.Data;

/**
 * 用户实体类
 * 对应数据库 user 表
 * 使用 Lombok @Data 自动生成:
 * - getter/setter
 * - toString()
 * - equals()/hashCode()
 * - requiredArgsConstructor
 */
@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String role; // 如 ROLE_ADMIN
}

3. UserMapper.java - MyBatis Mapper

package com.example.demo.mapper;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

/**
 * 数据访问接口(DAO)
 * @Mapper 注解:让 Spring 能扫描到该接口并创建代理对象
 * SQL 注解方式:@Select 直接写 SQL,适合简单查询
 */
@Mapper
public interface UserMapper {

    /**
     * 根据用户名查询用户信息
     * #{username} 是预编译参数,防止 SQL 注入
     * @param username 用户名
     * @return 用户对象,不存在返回 null
     */
    @Select("SELECT * FROM user WHERE username = #{username}")
    User findByUsername(String username);
}

4. RedisService.java - Redis 工具类

package com.example.demo.service;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * Redis 操作服务类
 * 封装常用操作,便于业务调用
 * 使用 StringRedisTemplate(只处理字符串),适合缓存简单键值对
 */
@Service
public class RedisService {

    private final StringRedisTemplate redisTemplate;

    /**
     * 构造器注入 RedisTemplate
     * Spring 自动注入 RedisConnectionFactory 创建的模板
     */
    public RedisService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 设置字符串值,并设置过期时间(分钟)
     * @param key 键
     * @param value 值
     * @param timeout 过期时间(分钟)
     */
    public void set(String key, String value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MINUTES);
    }

    /**
     * 获取字符串值
     * @param key 键
     * @return 值,不存在返回 null
     */
    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 删除指定键
     * @param key 键
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }
}

5. CustomUserDetailsService.java - 自定义用户详情服务

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.Collection;
import java.util.Collections;

/**
 * 自定义用户详情服务
 * Spring Security 通过此服务加载用户信息用于认证
 * 实现 UserDetailsService 接口是必须的
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisService redisService;

    /**
     * 根据用户名加载用户详情
     * 调用时机:用户登录时(/login)
     * 流程:
     *  1. 先查 Redis 缓存
     *  2. 缓存命中 → 返回
     *  3. 未命中 → 查数据库 → 写入缓存
     * @param username 用户名
     * @return UserDetails(Spring Security 用户模型)
     * @throws UsernameNotFoundException 用户不存在时抛出
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 尝试从 Redis 获取角色
        String cachedRole = redisService.get("user_role:" + username);
        if (cachedRole != null) {
            System.out.println("✅ Redis 缓存命中: " + username);
            return buildUserDetails(username, "******", cachedRole);
        }

        // 2. 缓存未命中,查询数据库
        User user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("❌ 用户不存在: " + username);
        }

        // 3. 将角色写入 Redis,有效期 30 分钟
        redisService.set("user_role:" + username, user.getRole(), 30);
        System.out.println("🔥 数据库查询并缓存: " + username);

        return buildUserDetails(user.getUsername(), user.getPassword(), user.getRole());
    }

    /**
     * 构建 Spring Security 的 UserDetails 对象
     * @param username 用户名
     * @param password 加密后的密码
     * @param role 角色(如 ROLE_ADMIN)
     * @return UserDetails 实例
     */
    private UserDetails buildUserDetails(String username, String password, String role) {
        // 将角色封装为 GrantedAuthority(权限对象)
        Collection<? extends GrantedAuthority> authorities =
            Collections.singletonList(new SimpleGrantedAuthority(role));

        // 创建 Spring Security 内置用户对象
        // 参数:用户名、密码、权限集合
        return new org.springframework.security.core.userdetails.User(
            username,
            password,
            authorities
        );
    }
}

6. SecurityConfig.java - 安全配置

package com.example.demo.config;

import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 * Spring Security 配置类
 * 控制认证、授权、密码编码、会话等行为
 */
@Configuration
@EnableWebSecurity  // 启用 Web 安全
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级安全(支持 @PreAuthorize)
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    /**
     * 密码编码器 Bean
     * 用于比对用户输入密码与数据库加密密码
     * @return BCryptPasswordEncoder 实例
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 安全过滤链配置
     * 定义哪些请求需要认证、使用何种认证方式等
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // 禁用 CSRF(适合无状态 API)
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话
            .and()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/login").permitAll() // 登录接口放行
                .anyRequest().authenticated()         // 其他请求需认证
            )
            .httpBasic(); // 使用 HTTP Basic 认证(测试用)

        return http.build();
    }

    /**
     * Redis Template Bean
     * 用于操作 Redis
     * Spring Boot 自动配置 RedisConnectionFactory
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }
}

7. UserController.java - 控制器

package com.example.demo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

/**
 * 用户操作控制器
 * 演示 @PreAuthorize 方法级权限控制
 */
@RestController
@RequestMapping("/api/users")
public class UserController {

    /**
     * 删除用户接口
     * @PreAuthorize("hasRole('ROLE_ADMIN')")
     * 只有拥有 ROLE_ADMIN 角色的用户才能调用
     * 注意:hasRole() 会自动添加 ROLE_ 前缀,所以写 'ADMIN' 也可以
     */
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @DeleteMapping("/{username}")
    public String deleteUser(@PathVariable String username) {
        return "🗑️ 用户 " + username + " 已删除";
    }

    /**
     * 查看用户信息
     * @PreAuthorize("authentication.principal.username == #username")
     * 表达式含义:
     *   当前登录用户名(authentication.principal.username)
     *   必须等于路径参数 #username
     * 实现“只能查看自己信息”的业务逻辑
     */
    @PreAuthorize("authentication.principal.username == #username")
    @GetMapping("/{username}")
    public String getUserInfo(@PathVariable String username) {
        return "👤 用户信息: " + username;
    }
}

8. application.yml - 配置文件

server:
  port: 8080  # 服务端口

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: yourpassword  # 请替换为真实密码
    driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: localhost
    port: 6379  # Redis 服务地址

mybatis:
  type-aliases-package: com.example.demo.entity  # 别名包,SQL 中可用类名代替全路径
  configuration:
    map-underscore-to-camel-case: true  # 数据库下划线字段自动映射到 Java 驼峰属性

logging:
  level:
    com.example.demo.mapper: debug  # 显示 MyBatis 执行的 SQL

▶️ 第四步:运行与测试

1. 启动项目

服务启动后,访问 http://localhost:8080 会跳转登录页(Basic Auth)。


2. 测试命令

✅ 测试1:管理员查看自己
curl -u admin:123456 http://localhost:8080/api/users/admin
# 响应:👤 用户信息: admin
# 说明:用户名匹配,授权通过
✅ 测试2:管理员删除用户
curl -X DELETE -u admin:123456 http://localhost:8080/api/users/alice
# 响应:🗑️ 用户 alice 已删除
# 说明:admin 拥有 ROLE_ADMIN,权限通过
❌ 测试3:普通用户删别人
curl -X DELETE -u alice:123456 http://localhost:8080/api/users/bob
# 响应:403 Forbidden
# 说明:alice 是 ROLE_USER,不满足 hasRole('ROLE_ADMIN')

✅ 查看 Redis 缓存

redis-cli
> KEYS user_role:*
# 输出:
# "user_role:admin"
# "user_role:alice"
# "user_role:bob"
> GET user_role:admin
# "ROLE_ADMIN"

🏁 总结:核心知识点

技术 作用
@PreAuthorize 方法级权限控制,支持 SpEL 表达式
hasRole() 检查角色(自动加 ROLE_ 前缀)
authentication.principal.username 获取当前登录用户名
#param 引用方法参数
Redis 缓存 提升性能,避免重复查库
BCrypt 安全存储密码
HTTP Basic 简单认证方式(适合测试)

KEY: user_role:admin
VAL: ROLE_ADMIN

KEY: user_role:alice
VAL: ROLE_USER

网站公告

今日签到

点亮在社区的每一天
去签到