Spring Boot 实战:基于 Spring Security 实现接口权限控制

68 阅读9分钟

Spring Boot 实战:基于 Spring Security 实现接口权限控制

大家好呀!在上一篇文章中,我们通过 Knife4j 实现了接口文档的自动生成,解决了前后端协作的效率问题。但随着项目上线,新的挑战出现了 ——接口安全。如果不做权限控制,任何人都能访问敏感接口(比如查询所有用户、修改商品库存),会导致数据泄露或系统异常。

今天这篇就教大家集成Spring Security,实现接口的登录验证(必须登录才能访问)和角色授权(不同角色访问不同接口),为系统加上 “安全锁”!

全程分 五 步走,从依赖配置到功能测试,每一步都提供可直接复制的代码和操作说明,零基础也能轻松实现接口权限控制~

一、先搞懂:为什么选 Spring Security?

在 Spring Boot 生态中,尽管 Apache Shiro 也是一个成熟、轻量级的权限控制框架,但大多数项目更倾向于选择 Spring Security,主要原因包括以下几点:

1. 与 Spring Boot 深度集成

2. 强大的生态系统和社区支持

3. 功能全面且灵活

4. 与现代架构兼容性更好

二、准备工作:基于现有项目开发

我们直接在之前的first-springboot-project项目上集成 Spring Security,若尚未了解前置步骤,可参考往期文章,或关注文末公众号,私信获取完整项目代码。

三、第一步:添加 Spring Security 依赖

1. 打开 pom.xml 文件

在左侧项目结构中,找到src/main/resources下的pom.xml,双击打开。

2. 添加依赖代码

<dependencies>标签内粘贴以下依赖:

<!-- Spring Security  Starter(与Spring Boot 适配) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>3.5.7</version>
        </dependency>

刷新依赖:添加后点击 IDEA 右上角的 “Load Maven Changes” 图标,等待 Maven 下载依赖(右下角进度条走完无红色报错即可)。

四、第二步:创建用户表及测试数据

1. 执行 SQL 脚本的步骤

  1. 打开 DataGrip,右键点击已连接的 MySQL 实例→“New”→“Query Console”,打开 SQL 控制台;
  2. 复制下面的完整 SQL 脚本,粘贴到控制台;
  3. 点击控制台左上角的 “执行” 按钮(绿色三角),等待执行完成(下方日志无红色报错即成功)。

2. 完整 SQL 脚本(复制即用)

-- 1. 创建系统用户表(sys_user)
drop table if exists sys_user;
create table sys_user
(
    id   int      not null auto_increment comment '用户ID(自增)',
    username varchar(20) not null comment '用户名',
    password        varchar(100)       not null comment '密码',
    role        varchar(20)      not null comment '角色',
    primary key (id)
)  comment ='系统用户表';

-- 2. 插入测试数据
insert into sys_user (username, password, role) VALUES ('admin', '$2a$10$A95.Uki5rkDNXA9Z.u.E7uuhyi8lgWGz.DP8SlJj1ht6.eQEdjvUm', 'ROLE_ADMIN');
insert into sys_user (username, password, role) VALUES ('zhangsan', '$2a$10$A95.Uki5rkDNXA9Z.u.E7uuhyi8lgWGz.DP8SlJj1ht6.eQEdjvUm', 'ROLE_USER');
  1. 执行完成后,刷新 DataGrip 左侧 “Database” 面板的 “springboot_demo” 数据库;
  2. 展开 “Tables”,能看到sys_user两张表;
  3. 右键点击sys_user表→“Open Table”,能看到 2 条测试用户数据;说明脚本执行成功。

五、第三步:实现实体类与数据访问层

1. 创建 SysUser 实体类

package com.example.firstspringbootproject.entity;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

@Data
@Schema(name = "SysUser", description = "系统用户实体类(包含安全相关字段)")

public class SysUser implements UserDetails { // 实现UserDetails接口,适配Spring Security

    @Schema(description = "用户ID(自增)", example = "1")
    private Integer id;

    @Schema(description = "登录账号(唯一)", required = true, example = "admin")
    private String username;

    @Schema(description = "登录密码(BCrypt加密)", required = true)
    private String password;

    @Schema(description = "用户角色(ROLE_ADMIN/ROLE_USER)", required = true, example = "ROLE_ADMIN")
    private String role;

    // ------------------------------

    // 以下是UserDetails接口的实现方法(Spring Security需要)

    // ------------------------------

    /**
     * 获取用户的权限(角色)
     */

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        // 将role转换为Spring Security认可的权限对象
        return Collections.singletonList(new SimpleGrantedAuthority(role));

    }

    /**
     * 账号是否未过期(true=未过期)
     */

    @Override
    public boolean isAccountNonExpired() {

        return true; // 简化处理,默认账号未过期

    }

    /**
     * 账号是否未锁定(true=未锁定)
     */
    @Override
    public boolean isAccountNonLocked() {

        return true; // 简化处理,默认账号未锁定

    }

    /**
     * 密码是否未过期(true=未过期)
     */
    @Override
    public boolean isCredentialsNonExpired() {

        return true; // 简化处理,默认密码未过期

    }

    /**
     * 账号是否启用(true=启用)
     */
    @Override
    public boolean isEnabled() {

        return true; // 简化处理,默认账号启用

    }

}

2. 创建 SysUserMapper 接口

com.example.firstspringbootproject.mapper下创建SysUserMapper接口:

package com.example.firstspringbootproject.mapper;

import com.example.firstspringbootproject.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface SysUserMapper {

    //根据用户名查询用户(Spring Security登录时会调用此方法)
    SysUser findByUsername(String username);
}

2. ### 创建 SysUserMapper.xml

src/main/resources/mapper下新建SysUserMapper.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">

<!-- namespace必须和Mapper接口全路径一致 -->
<mapper namespace="com.example.firstspringbootproject.mapper.SysUserMapper">
    <!--
      第一步:定义一个 resultMap
      - id: 这个 resultMap 的唯一标识,可以自由命名,通常以 "类名+ResultMap" 命名
      - type: 映射的目标 Java 实体类的全路径
    -->
    <resultMap id="SysUserResultMap" type="com.example.firstspringbootproject.entity.SysUser">
        <!--
          <id> 用于映射主键字段
          - property: Java 对象中的属性名
          - column: 数据库表中的字段名
        -->
        <id property="id" column="id"/>

        <!--
          <result> 用于映射普通字段
          - property: Java 对象中的属性名
          - column: 数据库表中的字段名
        -->
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="role" column="role"/>
    </resultMap>

    <!-- 根据手机号查询用户 -->
    <select id="findByUsername" parameterType="String" resultMap="SysUserResultMap">
        SELECT * FROM sys_user WHERE username = #{username}
    </select>

</mapper>

六、第四步:实现 Spring Security 核心配置

com.example.firstspringbootproject.config包下创建SecurityConfig类:

package com.example.firstspringbootproject.config;

import com.example.firstspringbootproject.common.Result;
import com.example.firstspringbootproject.entity.SysUser;
import com.example.firstspringbootproject.mapper.SysUserMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration  // 告诉Spring这是配置类
@EnableWebSecurity  // 启用Web安全功能
@EnableMethodSecurity  // 启用方法级别的权限控制(后面会用到)
public class SecurityConfig {

    @Autowired
    private SysUserMapper sysUserMapper;

    // 注入ObjectMapper,用来把Result转换成JSON
    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 1. 配置密码编码器:用BCrypt加密密码(官方推荐,安全不可逆)
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 2. 配置用户详情服务:告诉Security“怎么根据用户名查用户”
     */
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            SysUser sysUser = sysUserMapper.findByUsername(username);
            if (sysUser == null) {
                throw new UsernameNotFoundException("用户不存在: " + username);
            }
            return sysUser;
        };
    }

    /**
     * 3. 配置认证提供者:把“用户查询服务”和“密码编码器”关联起来
     */
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService());
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    /**
     * 4. 配置认证管理器:Security内部用的,初学者不用改
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    /**
     * 5. 核心规则配置:控制哪些接口能访问、登录方式、失败提示等
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 1. 关闭CSRF:前后端分离项目必须关,不然接口访问不了(初学者记住就行)
                .csrf(csrf -> csrf.disable())
                // 2. 配置接口访问规则(重点!)
                .authorizeHttpRequests(auth -> auth
                        // ① 不用登录就能访问的接口:接口文档(Knife4j)和登录接口
                        .requestMatchers("/doc.html", "/webjars/**", "/v3/api-docs/**", "/login").permitAll()
                        // ② 只有ADMIN角色能访问的接口:比如“查询所有用户”
                        .requestMatchers("/api/user/all").hasRole("ADMIN")
                        // ③ USER和ADMIN角色都能访问的接口:比如“查询单个用户”“查商品”
                        .requestMatchers("/api/user/**", "/api/product/**").hasAnyRole("USER", "ADMIN")
                        // ④ 其他所有接口:必须登录才能访问
                        .anyRequest().authenticated()
                )
                // 3. 配置登录方式:用HTTP Basic登录(适合初学者,简单易上手)
                .httpBasic(httpBasic -> {
                })
                // 4. 配置登录失败、权限不足的提示(返回统一的Result格式)
                .exceptionHandling(ex -> ex
                        // 未登录或登录过期:返回401
                        .authenticationEntryPoint((request, response, authException) -> {
                            response.setContentType("application/json;charset=UTF-8");
                            Result<Void> result = Result.error(401, "未登录或登录已过期,请重新登录");
                            response.getWriter().write(objectMapper.writeValueAsString(result));
                        })
                        // 权限不足:返回403
                        .accessDeniedHandler((request, response, accessDeniedException) -> {
                            response.setContentType("application/json;charset=UTF-8");
                            Result<Void> result = Result.error(403, "权限不足,无法访问");
                            response.getWriter().write(objectMapper.writeValueAsString(result));
                        })
                );

        // 把认证提供者关联到Security
        http.authenticationProvider(authenticationProvider());

        return http.build();
    }
}

七、第五步:测试接口权限控制

所有配置完成后,启动项目,用 Apifox 测试不同角色用户访问接口的权限:

1. 测试 1:未登录访问接口(应返回 401 未登录)

  • 用 Apifox 访问http://localhost:8080/api/user/1(无添加登录信息);
  • 响应结果:
{
    "code": 401,
    "msg": "未登录或登录已过期,请重新登录",
    "data": null
}

符合预期,未登录无法访问接口。

2. 测试 2:普通用户访问需 ADMIN 角色的接口

  • Apifox Auth页签中选择Basic Auth,输入用户明:zhangsan,密码:123456
  • 访问http://localhost:8080/api/user/all(需 ADMIN 角色);
  • 响应结果:
{
    "code": 403,
    "msg": "权限不足,无法访问",
    "data": null
}}

符合预期,普通用户无权限访问管理员接口。

3. 测试 3:普通用户访问允许 USER 角色的接口

  • Apifox Auth页签中选择Basic Auth,输入用户明:zhangsan,密码:123456
  • 访问http://localhost:8080/api/user/1(允许 USER/ADMIN 角色);
  • 响应结果:
{
    "code": 200,
    "msg": "success",
    "data": {
        "id": 1,
        "name": "小明",
        "age": 20,
        "phone": "13800138000"
    }
}

符合预期,普通用户可访问允许的接口。

4.测试 4:管理员访问允许 USER 角色的接口

  • Apifox Auth页签中选择Basic Auth,输入用户明:admin,密码:123456
  • 访问http://localhost:8080/api/user/1(允许 USER/ADMIN 角色);
  • 响应结果:
{
    "code": 200,
    "msg": "success",
    "data": {
        "id": 1,
        "name": "小明",
        "age": 20,
        "phone": "13800138000"
    }
}

符合预期,管理员可访问允许的接口。

4. 测试 4:管理员访问需 ADMIN 角色的接口

  • Apifox Auth页签中选择Basic Auth,输入用户明:admin,密码:123456
  • 访问http://localhost:8080/api/user/all(需 ADMIN 角色);
  • 响应结果:
{
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "name": "小明",
            "age": 20,
            "phone": "13800138000"
        },
        {
            "id": 2,
            "name": "小红",
            "age": 19,
            "phone": "13900139000"
        },
        {
            "id": 3,
            "name": "小李",
            "age": 22,
            "phone": "13700137000"
        }
    ]
}

符合预期,管理员有权限访问。

八、常见问题:

1. 登录时报 “Bad credentials”(密码错误)?

  • 原因 1:输入的密码未加密,与数据库中加密后的密码不匹配;
  • 原因 2:密码编码器配置错误(未使用 BCrypt);
  • 解决方案:确保用passwordEncoder.encode("123456")加密密码后存入数据库,且SecurityConfigpasswordEncoderBean 是BCryptPasswordEncoder

2. 接口返回 403 权限不足,但用户角色正确?

  • 原因:角色名称格式错误(Spring Security 要求角色必须以ROLE_开头,如ROLE_ADMIN,而非ADMIN);
  • 解决方案:修改数据库user表的role字段,确保值为ROLE_ADMINROLE_USER

九、总结:

今天我们完成了Spring Boot集成Spring Security,重点掌握了:

  1. 核心配置:通过SecurityFilterChain定义接口权限规则,UserDetailsService查询用户信息;
  2. 权限控制:实现登录验证(未登录返回 401)和角色授权(权限不足返回 403);

现在,你的 Spring Boot 项目已经具备企业级的接口权限控制能力 —— 敏感接口只有授权用户才能访问,数据安全得到有效保障!

下一篇文章,我会教大家 “Spring Boot 3.5.7 集成 JWT 实现无状态登录”,解决前后端分离项目的 Session 共享问题,让接口更适合分布式部署!

👉 关注+私信“Web基础源码”,获取完整工程!有问题评论区见~

扫码_搜索联合传播样式-标准色版.png