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 脚本的步骤
- 打开 DataGrip,右键点击已连接的 MySQL 实例→“New”→“Query Console”,打开 SQL 控制台;
- 复制下面的完整 SQL 脚本,粘贴到控制台;
- 点击控制台左上角的 “执行” 按钮(绿色三角),等待执行完成(下方日志无红色报错即成功)。
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');
- 执行完成后,刷新 DataGrip 左侧 “Database” 面板的 “springboot_demo” 数据库;
- 展开 “Tables”,能看到
sys_user两张表; - 右键点击
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")加密密码后存入数据库,且SecurityConfig中passwordEncoderBean 是BCryptPasswordEncoder。
2. 接口返回 403 权限不足,但用户角色正确?
- 原因:角色名称格式错误(Spring Security 要求角色必须以
ROLE_开头,如ROLE_ADMIN,而非ADMIN); - 解决方案:修改数据库
user表的role字段,确保值为ROLE_ADMIN或ROLE_USER。
九、总结:
今天我们完成了Spring Boot集成Spring Security,重点掌握了:
- 核心配置:通过
SecurityFilterChain定义接口权限规则,UserDetailsService查询用户信息; - 权限控制:实现登录验证(未登录返回 401)和角色授权(权限不足返回 403);
现在,你的 Spring Boot 项目已经具备企业级的接口权限控制能力 —— 敏感接口只有授权用户才能访问,数据安全得到有效保障!
下一篇文章,我会教大家 “Spring Boot 3.5.7 集成 JWT 实现无状态登录”,解决前后端分离项目的 Session 共享问题,让接口更适合分布式部署!
👉 关注+私信“Web基础源码”,获取完整工程!有问题评论区见~