在前文内容中,我们已经完成了Spring Boot与MySQL+MyBatis的整合、统一异常处理和拦截器开发,实现了规范的接口开发、异常兜底和前置拦截(如登录校验)。但在实际开发中,仅仅实现“登录校验”远远不够——一个规范的项目,需要对不同角色的用户分配不同的权限(如普通用户只能查询数据,管理员可以新增、修改、删除数据),避免越权操作,筑牢项目安全防线。
今天,我们进入Spring Boot学习的第七个核心环节——权限管理实战(Spring Security入门),这是从“规范开发”走向“安全开发”的关键一步。本文依旧适配零基础学习者,不堆砌复杂底层原理,全程结合前序springboot-demo项目,手把手教你整合Spring Security、实现用户认证(登录)、角色授权(权限控制),每一步都搭配具体代码示例、实操说明和踩坑点,衔接前文的异常处理、拦截器知识,让你既能掌握权限管理核心技能,又能完善现有项目,实现“登录+权限”的完整安全体系。
建议学习过程中,对照前序项目代码同步实操——权限管理是企业级Spring Boot项目的“必备功能”,无论是用户管理、商品管理还是订单管理系统,都离不开权限控制,学会本文内容,可直接复用至实际开发场景。
为什么需要权限管理?核心价值与学习重点
在学习具体实操步骤之前,我们先搞懂两个核心问题:为什么Spring Boot项目必须做权限管理?零基础学习者需要重点掌握哪些内容?搞懂这些,能让我们更有针对性地学习,避免盲目操作。
1.1 核心价值:杜绝越权操作,保障项目数据安全
先看一个场景:前文我们实现了登录拦截器,能阻止未登录用户访问接口,但无法区分“普通用户”和“管理员”——如果普通用户也能访问“删除用户”“修改用户”等高危接口,会导致数据泄露、误操作甚至恶意破坏;再比如,不同部门的用户,只能访问本部门的相关数据,不能跨部门查看,这也需要通过权限管理来实现。
权限管理的核心价值,就是“按角色分配权限,按权限控制接口访问”,具体体现在三个方面:
1. 用户认证:验证用户身份的合法性(即“登录”,确认用户是系统的合法用户),替代前文的简单登录接口,实现更安全的登录校验(如密码加密、记住密码、登录失败限制);
2. 角色授权:给不同用户分配不同角色(如普通用户USER、管理员ADMIN),给不同角色分配不同权限(如USER只能查询,ADMIN可以增删改查);
3. 接口拦截:拦截用户请求,验证用户是否拥有访问该接口的权限,没有权限则拒绝访问,返回统一的权限不足响应。
简单来说,权限管理是项目的“安全闸门”,结合前文的异常处理和拦截器,形成“前置拦截(登录)→ 权限校验(角色)→ 异常兜底”的完整安全体系,让项目既规范又安全。
1.2 学习重点:零基础必掌握的3个核心内容
对于零基础学习者而言,无需深入理解Spring Security的底层架构(如过滤器链、安全管理器),重点掌握以下3个核心内容,即可满足基础权限管理需求,也是本文的重点讲解内容:
1. Spring Security整合:在现有Spring Boot项目中导入依赖、配置核心类,实现基础的用户认证功能;
2. 用户认证实现:替换前文的简单登录接口,实现密码加密存储、登录校验、登录失败限制等功能;
3. 角色授权实现:设计角色表、权限表,关联用户与角色,实现“不同角色访问不同接口”的权限控制。
需要注意的是,本文所有操作依旧基于前序的springboot-demo项目(已整合MySQL+MyBatis、异常处理、拦截器),无需重新创建项目,直接在原有项目上扩展即可,全程贴合实际开发场景,让你感受到“基础功能→进阶优化→安全加固”的连贯学习链路。
Spring Security整合与权限管理实现
Spring Security是Spring官方提供的权限管理框架,功能强大、配置简洁,能快速实现用户认证和授权,无需我们手动编写复杂的权限校验逻辑。本文将分4个步骤实现权限管理:整合Spring Security依赖→设计权限相关数据库表→实现用户认证(登录)→实现角色授权(权限控制),每一步都有详细代码和实操说明。
2.1 步骤1:导入Spring Security依赖(pom.xml)
首先,在项目的pom.xml文件中导入Spring Security的核心依赖,Spring Boot会自动整合相关组件,无需额外配置版本(与Spring Boot版本保持一致)。
操作步骤:打开pom.xml,添加以下依赖:
<!-- Spring Security 核心依赖,用于权限管理 --> org.springframework.boot spring-boot-starter-security <!-- 补充:Spring Security与Spring MVC整合依赖(可选,本文无需额外添加,starter-security已包含) --> org.springframework.security spring-security-web org.springframework.security spring-security-config 【重点说明】
1. 依赖说明:spring-boot-starter-security是Spring Boot提供的权限管理 starter,包含了security-web和security-config的核心功能,导入后即可使用Spring Security的核心API;
2. 自动配置:导入依赖后,Spring Security会自动生效,默认拦截所有接口,生成一个默认用户名(user)和随机密码(启动项目时控制台会打印,格式为:Using generated security password: xxxxxxxx),用于测试登录;
3. 踩坑点:导入依赖后,启动项目,访问任何接口都会跳转到Spring Security默认的登录页面(http://localhost:8080/login),这是正常现象,后续我们会自定义登录接口和页面。
2.2 步骤2:设计权限相关数据库表(核心,贴合实际开发)
权限管理的核心是“用户-角色-权限”的关联关系,即一个用户可以拥有多个角色,一个角色可以拥有多个权限,一个权限对应一个接口。结合前文的user表,我们需要新增3张表:角色表(role)、权限表(permission)、用户角色关联表(user_role),实现完整的权限关联。
操作步骤:在MySQL中执行以下SQL语句,创建4张表(含前文的user表,补充完善):
-- 1. 用户表(前文已创建,补充role_id字段,关联角色表,简化设计:一个用户对应一个角色)ALTER TABLE `user` ADD COLUMN `role_id` INT NOT NULL COMMENT '角色ID,关联role表' AFTER `email`;-- 2. 角色表(存储角色信息,如普通用户、管理员)CREATE TABLE `role` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID', `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称(如USER、ADMIN)', `role_desc` VARCHAR(200) DEFAULT NULL COMMENT '角色描述(如普通用户、系统管理员)', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';-- 3. 权限表(存储权限信息,对应接口访问权限)CREATE TABLE `permission` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID', `perm_name` VARCHAR(100) NOT NULL COMMENT '权限名称(如user:query、user:add)', `perm_desc` VARCHAR(200) DEFAULT NULL COMMENT '权限描述(如查询用户、新增用户)', `url` VARCHAR(200) NOT NULL COMMENT '权限对应接口路径(如/user/getAll、/user/add)', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';-- 4. 角色权限关联表(一个角色对应多个权限)CREATE TABLE `role_permission` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID', `role_id` INT NOT NULL COMMENT '角色ID,关联role表', `perm_id` INT NOT NULL COMMENT '权限ID,关联permission表', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 联合唯一约束,避免同一角色重复关联同一权限 UNIQUE KEY `uk_role_perm` (`role_id`,`perm_id`), -- 外键约束 FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE, FOREIGN KEY (`perm_id`) REFERENCES `permission` (`id`) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';【表结构说明(零基础必看)】
1. user表:新增role_id字段,关联role表,简化设计为“一个用户对应一个角色”(实际开发中可设计为多对多,通过user_role表关联,本文简化便于入门);
2. role表:核心字段是role_name(角色标识,如USER、ADMIN),用于后续权限判断;
3. permission表:核心字段是perm_name(权限标识)和url(对应接口路径),一个权限对应一个接口;
4. role_permission表:关联role和permission表,实现“一个角色拥有多个权限”的关联关系。
补充测试数据(执行以下SQL,便于后续测试):
-- 插入角色数据INSERT INTO `role` (`role_name`, `role_desc`) VALUES ('USER', '普通用户,只能查询用户'), ('ADMIN', '系统管理员,可增删改查用户');-- 插入权限数据(对应前文的用户接口)INSERT INTO `permission` (`perm_name`, `perm_desc`, `url`) VALUES ('user:query', '查询用户权限', '/user/getAll'),('user:queryOne', '查询单个用户权限', '/user/get/{id}'),('user:add', '新增用户权限', '/user/add'),('user:update', '修改用户权限', '/user/update'),('user:delete', '删除用户权限', '/user/delete/{id}');-- 插入角色权限关联数据(普通用户只有查询权限,管理员有所有权限)INSERT INTO `role_permission` (`role_id`, `perm_id`) VALUES (1, 1), -- 普通用户(role_id=1)拥有查询所有用户权限(perm_id=1)(1, 2), -- 普通用户拥有查询单个用户权限(perm_id=2)(2, 1), -- 管理员(role_id=2)拥有查询所有用户权限(2, 2), -- 管理员拥有查询单个用户权限(2, 3), -- 管理员拥有新增用户权限(2, 4), -- 管理员拥有修改用户权限(2, 5); -- 管理员拥有删除用户权限-- 更新user表的role_id(给现有用户分配角色)UPDATE `user` SET `role_id` = 1 WHERE `name` = '张三'; -- 张三为普通用户UPDATE `user` SET `role_id` = 2 WHERE `name` = '李四'; -- 李四为管理员2.3 步骤3:创建权限相关实体类(Entity)
结合新增的数据库表,创建对应的实体类(Role、Permission),并完善User实体类,添加角色关联字段,用于后续MyBatis查询和Spring Security权限校验。
操作步骤:在com.example.springbootdemo.entity包下,创建Role、Permission实体类,完善User实体类:
角色实体类(Role.java)
package com.example.springbootdemo.entity;import lombok.Data;import java.util.Date;import java.util.List;/** * 角色实体类,对应role表 */@Datapublic class Role { // 角色ID private Integer id; // 角色名称(如USER、ADMIN) private String roleName; // 角色描述 private String roleDesc; // 创建时间 private Date createTime; // 该角色拥有的所有权限(关联permission表,一对多) private List permissions;} 权限实体类(Permission.java)
package com.example.springbootdemo.entity;import lombok.Data;import java.util.Date;/** * 权限实体类,对应permission表 */@Datapublic class Permission { // 权限ID private Integer id; // 权限名称(如user:query) private String permName; // 权限描述 private String permDesc; // 权限对应接口路径 private String url; // 创建时间 private Date createTime;}完善用户实体类(User.java,补充角色关联)
package com.example.springbootdemo.entity;import lombok.Data;import java.util.Date;/** * 用户实体类,对应user表(完善角色关联) */@Datapublic class User { // 用户ID private Integer id; // 用户名 private String name; // 年龄 private Integer age; // 邮箱 private String email; // 角色ID(关联role表) private Integer roleId; // 关联的角色信息(一对一,一个用户对应一个角色) private Role role; // 补充:密码字段(前文未添加,用于登录校验,存储加密后的密码) private String password;}【重点说明】
1. 关联关系:User类关联Role类(一对一),Role类关联Permission类(一对多),对应数据库中的关联关系;
2. 密码字段:User类新增password字段,用于存储用户密码(注意:实际开发中必须存储加密后的密码,不能存储明文,后续会实现密码加密);
3. lombok注解:依旧使用@Data注解,自动生成getter、setter方法,简化代码。
2.4 步骤4:实现用户认证(登录),替换原有登录接口
用户认证是权限管理的第一步,核心是“验证用户身份合法性”。Spring Security提供了UserDetailsService接口,用于加载用户信息(从数据库查询用户),结合PasswordEncoder接口实现密码加密和校验,替代前文的简单登录接口,实现更安全的登录功能。
我们分3个小步骤实现:创建Mapper接口和映射文件→实现UserDetailsService接口→配置Spring Security核心类。
步骤4.1:创建Mapper接口和映射文件(查询用户、角色、权限)
需要创建RoleMapper、PermissionMapper接口,完善UserMapper接口,添加查询用户关联角色和权限的方法,用于Spring Security加载用户信息。
1. 完善UserMapper接口(com.example.springbootdemo.mapper.UserMapper):
package com.example.springbootdemo.mapper;import com.example.springbootdemo.entity.User;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import java.util.List;@Mapperpublic interface UserMapper { // 前文已有的方法(省略,保持不变) int addUser(User user); User getUserById(Integer id); List getAllUser(); int updateUserById(User user); int deleteUserById(Integer id); User getUserByEmail(String email); // 新增:根据用户名查询用户(Spring Security登录时,根据用户名加载用户) User getUserByName(@Param("name") String name); // 新增:根据用户ID查询用户关联的角色和权限(用于权限校验) User getUserWithRoleAndPerm(Integer userId);} 2. 创建RoleMapper接口(com.example.springbootdemo.mapper.RoleMapper):
package com.example.springbootdemo.mapper;import com.example.springbootdemo.entity.Role;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Param;import java.util.List;@Mapperpublic interface RoleMapper { // 根据角色ID查询角色关联的权限 Role getRoleWithPerm(Integer roleId); // 根据用户ID查询用户对应的角色 Role getRoleByUserId(@Param("userId") Integer userId);}3. 创建PermissionMapper接口(com.example.springbootdemo.mapper.PermissionMapper):
package com.example.springbootdemo.mapper;import com.example.springbootdemo.entity.Permission;import org.apache.ibatis.annotations.Mapper;import java.util.List;@Mapperpublic interface PermissionMapper { // 根据角色ID查询该角色拥有的所有权限 List getPermByRoleId(Integer roleId);} 4. 完善UserMapper.xml(resources/mapper/UserMapper.xml),添加新增方法的SQL:
<!--?xml version="1.0" encoding="UTF-8" ?-->
<!-- 前文已有的SQL(省略,保持不变) --> <!-- 新增:根据用户名查询用户 --> <!-- 新增:根据用户ID查询用户关联的角色和权限(一对一+一对多关联查询) --> <!-- 结果集映射:用户关联角色和权限 --> <!-- 一对一关联角色 --> <!-- 一对多关联权限 --> 5. 创建RoleMapper.xml(resources/mapper/RoleMapper.xml):
<!--?xml version="1.0" encoding="UTF-8" ?-->
<!-- 根据角色ID查询角色关联的权限 --> <!-- 根据用户ID查询用户对应的角色 --> <!-- 结果集映射:角色关联权限 --> 6. 创建PermissionMapper.xml(resources/mapper/PermissionMapper.xml):
<!--?xml version="1.0" encoding="UTF-8" ?-->
<!-- 根据角色ID查询该角色拥有的所有权限 --> 步骤4.2:实现UserDetailsService接口(加载用户信息)
UserDetailsService是Spring Security提供的核心接口,用于登录时加载用户信息(从数据库查询用户、角色、权限),我们需要实现该接口,重写loadUserByUsername方法,返回Spring Security需要的UserDetails对象(封装用户信息、角色、权限)。
操作步骤:在com.example.springbootdemo.service.impl包下,创建UserDetailsServiceImpl类,实现UserDetailsService接口:
package com.example.springbootdemo.service.impl;import com.example.springbootdemo.entity.Permission;import com.example.springbootdemo.entity.Role;import com.example.springbootdemo.entity.User;import com.example.springbootdemo.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.ArrayList;import java.util.List;/** * 实现UserDetailsService接口,用于Spring Security加载用户信息(登录校验) */@Servicepublic class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; /** * 登录时,Spring Security会自动调用该方法,根据用户名查询用户信息 * @param username 登录时输入的用户名 * @return UserDetails:Spring Security封装的用户信息(包含用户名、密码、角色、权限) * @throws UsernameNotFoundException 用户名不存在时抛出异常 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 根据用户名查询用户(从数据库查询) User user = userMapper.getUserByName(username); if (user == null) { // 用户名不存在,抛出异常,Spring Security会自动捕获,返回登录失败响应 throw new UsernameNotFoundException("用户名不存在"); } // 2. 查询该用户关联的角色和权限(从数据库查询) User userWithRoleAndPerm = userMapper.getUserWithRoleAndPerm(user.getId()); Role role = userWithRoleAndPerm.getRole(); List permissions = role.getPermissions(); // 3. 封装角色和权限(Spring Security要求角色以ROLE_开头,权限直接使用permName) List authorities = new ArrayList<>(); // 封装角色(格式:ROLE_角色名,如ROLE_USER、ROLE_ADMIN) authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName())); // 封装权限(格式:权限名,如user:query、user:add) for (Permission perm : permissions) { authorities.add(new SimpleGrantedAuthority(perm.getPermName())); } // 4. 返回Spring Security需要的UserDetails对象(用户名、加密后的密码、角色权限、是否可用等) return new org.springframework.security.core.userdetails.User( user.getName(), // 用户名 user.getPassword(), // 加密后的密码(Spring Security会自动校验密码) true, // 账户是否可用 true, // 账户是否过期 true, // 凭证是否过期 true, // 账户是否锁定 authorities // 角色和权限集合 ); }} 【重点解析+踩坑点】
1. 核心方法:loadUserByUsername方法,Spring Security登录时会自动调用该方法,根据用户名查询用户信息,若用户名不存在,抛出UsernameNotFoundException异常;
2. 角色格式:Spring Security要求角色必须以“ROLE_”开头(如ROLE_USER、ROLE_ADMIN),否则无法识别角色,权限可直接使用permName(如user:query);
3. 密码要求:返回的UserDetails对象中的密码,必须是加密后的密码(后续会实现密码加密存储),Spring Security会自动对比登录时输入的密码(加密后)与数据库中的密码;

4. 踩坑点:若未查询到用户(user == null),必须抛出UsernameNotFoundException异常,否则Spring Security会认为用户存在,导致登录校验异常。
步骤4.3:配置Spring Security核心类(SecurityConfig)
创建Spring Security的配置类,配置密码加密方式、用户认证服务(UserDetailsService)、登录接口、登录成功/失败处理等,替代Spring Security的默认配置,实现自定义登录逻辑。
操作步骤:在com.example.springbootdemo.config包下,创建SecurityConfig类,继承WebSecurityConfigurerAdapter(Spring Security的核心配置类):
package com.example.springbootdemo.config;import com.example.springbootdemo.common.Result;import com.example.springbootdemo.service.impl.UserDetailsServiceImpl;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.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.authentication.AuthenticationFailureHandler;import org.springframework.security.web.authentication.AuthenticationSuccessHandler;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * Spring Security核心配置类,配置认证、授权、登录等逻辑 */@Configuration@EnableWebSecurity // 开启Spring Security功能public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; /** * 配置密码加密方式(必须配置,Spring Security要求密码必须加密) * BCryptPasswordEncoder:Spring Security提供的加密算法,不可逆,安全性高 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置用户认证服务:指定使用我们自己实现的UserDetailsService,以及密码加密方式 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) // 指定用户信息加载服务 .passwordEncoder(passwordEncoder()); // 指定密码加密方式 } /** * 配置HTTP请求安全规则:哪些接口需要认证、哪些接口无需认证,登录接口、登录成功/失败处理等 */ @Override protected void configure(HttpSecurity http) throws Exception { http // 1. 关闭CSRF防护(跨站请求伪造,前后端分离项目可关闭,简化开发) .csrf().disable() // 2. 配置请求授权规则 .authorizeRequests() // 放行登录接口(无需认证即可访问) .antMatchers("/user/login").permitAll() // 其他所有接口都需要认证(登录后才能访问) .anyRequest().authenticated() .and() // 3. 配置登录相关逻辑 .formLogin() // 指定自定义登录接口路径(替代Spring Security默认的/login接口) .loginProcessingUrl("/user/login") // 登录请求的参数名(与前端登录表单的用户名、密码字段名一致) .usernameParameter("username") .passwordParameter("password") // 登录成功处理(返回统一JSON响应,替代默认的跳转页面) .successHandler(authenticationSuccessHandler()) // 登录失败处理(返回统一JSON响应,替代默认的跳转页面) .failureHandler(authenticationFailureHandler()) .and() // 4. 配置注销登录逻辑 .logout() // 指定注销接口路径 .logoutUrl("/user/logout") // 注销成功后返回的响应 .logoutSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=utf-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(Result.success("注销成功"))); }); } /** * 登录成功处理器:返回统一JSON响应(替代默认跳转) */ private AuthenticationSuccessHandler authenticationSuccessHandler() { return (request, response, authentication) -> { // 设置响应格式为JSON response.setContentType("application/json;charset=utf-8"); // 返回统一成功响应 String json = new ObjectMapper().writeValueAsString(Result.success("登录成功")); response.getWriter().write(json); }; } /** * 登录失败处理器:返回统一JSON响应(替代默认跳转) */ private AuthenticationFailureHandler authenticationFailureHandler() { return (request, response, exception) -> { // 设置响应格式为JSON response.setContentType("application/json;charset=utf-8"); // 返回统一失败响应,提示信息为异常信息 String json = new ObjectMapper().writeValueAsString(Result.fail(401, exception.getMessage())); response.getWriter().write(json); }; }}【重点解析+踩坑点】
1. 核心注解:@Configuration(标识为配置类)、@EnableWebSecurity(开启Spring Security功能),必须添加;
2. 密码加密:必须配置PasswordEncoder(本文使用BCryptPasswordEncoder),Spring Security从5.x版本开始,强制要求密码加密,否则会报错;
3. 登录接口配置:loginProcessingUrl("/user/login")指定自定义登录接口,替代Spring Security默认的/login接口,前端可通过该接口提交登录请求;
4. 登录成功/失败处理:通过successHandler和failureHandler,返回统一JSON响应(与前文的Result类一致),替代默认的页面跳转,贴合前后端分离开发场景;
5. 放行接口:antMatchers("/user/login").permitAll()表示登录接口无需认证即可访问,其他接口都需要认证(anyRequest().authenticated());
6. 踩坑点1:关闭CSRF防护(csrf().disable()),前后端分离项目中,CSRF防护会导致登录请求被拦截,无法正常登录;
7. 踩坑点2:登录请求的参数名(usernameParameter、passwordParameter)必须与前端登录表单的字段名一致,否则无法获取登录参数。
步骤5:实现密码加密存储(完善新增/修改用户接口)
前文我们在User类中新增了password字段,现在需要修改新增用户和修改用户的接口,将用户输入的明文密码加密后存储到数据库,避免明文密码泄露,同时适配Spring Security的密码校验逻辑。
操作步骤:修改UserServiceImpl类,在addUser和updateUserById方法中,添加密码加密逻辑:
package com.example.springbootdemo.service.impl;import com.example.springbootdemo.common.CustomException;import com.example.springbootdemo.entity.User;import com.example.springbootdemo.mapper.UserMapper;import com.example.springbootdemo.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Service;import java.util.List;@Servicepublic class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; // 注入密码加密器(SecurityConfig中配置的BCryptPasswordEncoder) @Autowired private PasswordEncoder passwordEncoder; // 新增用户(添加密码加密逻辑) @Override public int addUser(User user) { // 1. 校验用户名、密码不能为空 if (user.getName() == null || user.getName().isEmpty()) { throw new CustomException("用户名不能为空"); } if (user.getPassword() == null || user.getPassword().isEmpty()) { throw new CustomException("密码不能为空"); } // 2. 校验邮箱是否已存在 User existUser = userMapper.getUserByEmail(user.getEmail()); if (existUser != null) { throw new CustomException(400, "邮箱已存在,无法新增用户"); } // 3. 密码加密(将明文密码加密后存储) String encryptedPassword = passwordEncoder.encode(user.getPassword()); user.setPassword(encryptedPassword); // 4. 新增用户(默认给新增用户分配普通用户角色,role_id=1) user.setRoleId(1); return userMapper.addUser(user); } // 修改用户(添加密码加密逻辑,若密码不为空则加密) @Override public int updateUserById(User user) { if (user.getId() == null) { throw new CustomException("用户id不能为空"); } // 校验年龄不能为负数 if (user.getAge() != null && user.getAge() < 0) { throw new CustomException("年龄不能为负数"); } // 若密码不为空,则加密后更新 if (user.getPassword() != null && !user.getPassword().isEmpty()) { String encryptedPassword = passwordEncoder.encode(user.getPassword()); user.setPassword(encryptedPassword); } return userMapper.updateUserById(user); } // 其他方法不变(省略) @Override public User getUserById(Integer id) { User user = userMapper.getUserById(id); if (user == null) { throw new CustomException(404, "未查询到该用户"); } return user; } @Override public List getAllUser() { return userMapper.getAllUser(); } @Override public int deleteUserById(Integer id) { if (id == null) { throw new CustomException("用户id不能为空"); } User user = userMapper.getUserById(id); if (user == null) { throw new CustomException(404, "该用户不存在,无法删除"); } return userMapper.deleteUserById(id); }} 【重点修改说明】
1. 注入PasswordEncoder:通过@Autowired注入SecurityConfig中配置的BCryptPasswordEncoder对象,用于密码加密;
2. 新增用户加密:新增用户时,校验密码不能为空,将明文密码通过passwordEncoder.encode()方法加密后,存入数据库;
3. 修改用户加密:修改用户时,若密码不为空,则加密后更新;若密码为空,则不修改密码(避免误操作清空密码);
4. 角色分配:新增用户时,默认分配普通用户角色(role_id=1),后续可根据需求修改为手动分配角色。
步骤6:测试用户认证(登录)效果
启动Spring Boot项目,使用Postman访问登录接口,测试用户认证功能,步骤如下:
1. 准备测试数据:通过新增用户接口(POST http://localhost:8080/user/add),新增一个用户,请求体JSON:
{ "name": "赵六", "age": 25, "email": "zhaoliu@163.com", "password": "123456" // 明文密码,会自动加密存储}2. 访问登录接口:POST http://localhost:8080/user/login,请求体JSON(用户名、密码):
{ "username": "赵六", "password": "123456"}3. 测试场景:
- 登录成功:返回响应{"code":200,"message":"登录成功","data":null},说明用户认证成功;
- 用户名不存在:请求体{"username":"钱七","password":"123456"},返回响应{"code":401,"message":"用户名不存在","data":null};
- 密码错误:请求体{"username":"赵六","password":"654321"},返回响应{"code":401,"message":"Bad credentials","data":null}(Bad credentials是Spring Security默认的密码错误提示,后续可自定义);
- 未登录访问接口:访问GET http://localhost:8080/user/getAll,返回401未授权响应,说明接口需要认证才能访问。
若所有场景都符合预期,说明用户认证功能配置成功。
步骤7:实现角色授权(权限控制)
用户认证成功后,需要实现角色授权,即“不同角色访问不同接口”——普通用户(USER)只能访问查询接口,管理员(ADMIN)可以访问所有接口。我们通过Spring Security的@PreAuthorize注解,实现接口级别的权限控制,结合前文的异常处理,返回统一的权限不足响应。
操作步骤:分2个小步骤实现:开启方法级别的权限控制→给接口添加权限注解。
步骤7.1:开启方法级别的权限控制
在SecurityConfig类上添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解,开启Spring Security的方法级权限控制,允许使用@PreAuthorize、@PostAuthorize等注解。
// 在SecurityConfig类的类注解上添加以下注解@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级权限控制@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { // 原有代码不变(省略)}步骤7.2:给UserController接口添加权限注解
在UserController的各个接口上,添加@PreAuthorize注解,指定访问该接口需要的角色或权限,实现权限控制。
package com.example.springbootdemo.controller;import com.example.springbootdemo.common.Result;import com.example.springbootdemo.entity.User;import com.example.springbootdemo.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpSession;import java.util.List;@RestController@RequestMapping("/user")public class UserController { @Autowired private UserService userService; @Autowired private HttpSession session; // 1. 新增用户(只有管理员才能访问,要求角色为ADMIN) @PreAuthorize("hasRole('ADMIN')") // 角色控制:只有ROLE_ADMIN角色才能访问 @PostMapping("/add") public Result addUser(@RequestBody User user) { try { int result = userService.addUser(user); if (result > 0) { return Result.success("新增用户成功"); } else { return Result.fail("新增用户失败"); } } catch (Exception e) { throw e; } } // 2. 根据id查询用户(普通用户和管理员都能访问,要求有user:queryOne权限) @PreAuthorize("hasAuthority('user:queryOne')") // 权限控制:有user:queryOne权限才能访问 @GetMapping("/get/{id}") public Result getUserById(@PathVariable Integer id) { User user = userService.getUserById(id); return Result.success(user); } // 3. 查询所有用户(普通用户和管理员都能访问,要求有user:query权限) @PreAuthorize("hasAuthority('user:query')") @GetMapping("/getAll") public Result getAllUser() { List userList = userService.getAllUser(); return Result.success(userList); } // 4. 修改用户(只有管理员才能访问,要求角色为ADMIN) @PreAuthorize("hasRole('ADMIN')") @PutMapping("/update") public Result updateUserById(@RequestBody User user) { try { int result = userService.updateUserById(user); if (result > 0) { return Result.success("修改用户成功"); } else { return Result.fail("修改用户失败"); } } catch (Exception e) { throw e; } } // 5. 删除用户(只有管理员才能访问,要求角色为ADMIN) @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/delete/{id}") public Result deleteUserById(@PathVariable Integer id) { try { int result = userService.deleteUserById(id); if (result > 0) { return Result.success("删除用户成功"); } else { return Result.fail("删除用户失败"); } } catch (Exception e) { throw e; } } // 6. 登录接口(无需权限,已在SecurityConfig中放行) @PostMapping("/login") public Result login(@RequestParam String username, @RequestParam String password) { // 无需手动编写登录逻辑,Spring Security会自动调用UserDetailsServiceImpl的方法 return Result.success("登录成功"); } // 7. 注销接口(无需权限,登录后即可访问) @GetMapping("/logout") public Result logout() { // 无需手动编写注销逻辑,Spring Security会自动清理Session return Result.success("注销成功"); }} 【重点解析+注解说明】
1. @PreAuthorize注解:方法执行前进行权限校验,支持两种权限控制方式:
- hasRole('ADMIN'):校验用户是否拥有ROLE_ADMIN角色(注意:注解中写的是ADMIN,Spring Security会自动加上ROLE_前缀);
- hasAuthority('user:query'):校验用户是否拥有user:query权限(直接写权限名,与数据库中的perm_name一致);
2. 接口权限分配:
- 新增、修改、删除用户:只有管理员(ADMIN)才能访问;
- 查询用户(单个、所有):普通用户(USER)和管理员(ADMIN)都能访问(因为两者都拥有对应的查询权限);
3. 登录/注销接口:登录接口已在SecurityConfig中放行,注销接口登录后即可访问,无需额外添加权限注解。
步骤8:处理权限不足异常(结合全局异常处理)
当用户没有权限访问接口时,Spring Security会抛出AccessDeniedException异常,此时需要在全局异常处理类中捕获该异常,返回统一的权限不足响应(与前文的Result类格式一致),避免返回Spring Security默认的错误页面,贴合前后端分离开发场景。
操作步骤:修改全局异常处理类(GlobalExceptionHandler),新增AccessDeniedException异常的捕获方法:
package com.example.springbootdemo.common;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;import org.springframework.security.access.AccessDeniedException;/** * 全局异常处理类,统一捕获项目中所有异常,返回统一JSON响应 */@RestControllerAdvice // 标识为全局异常处理类,作用于所有@RestController注解的控制器public class GlobalExceptionHandler { // 1. 捕获自定义异常(CustomException),对应业务异常(如用户名为空、用户不存在等) @ExceptionHandler(CustomException.class) public Result handleCustomException(CustomException e) { // 返回统一失败响应,状态码和提示信息从自定义异常中获取 return Result.fail(e.getCode(), e.getMessage()); } // 2. 新增:捕获Spring Security权限不足异常(AccessDeniedException) @ExceptionHandler(AccessDeniedException.class) public Result handleAccessDeniedException(AccessDeniedException e) { // 返回统一权限不足响应,状态码建议使用403(Forbidden,禁止访问) return Result.fail(403, "权限不足,无法访问该接口"); } // 3. 捕获其他所有未定义的异常(兜底异常处理) @ExceptionHandler(Exception.class) public Result handleException(Exception e) { // 打印异常堆栈信息,便于开发调试(生产环境可注释或输出到日志文件) e.printStackTrace(); // 返回统一失败响应,提示通用错误信息 return Result.fail(500, "服务器内部错误,请联系管理员"); }}【重点说明+优化建议】
1. 异常捕获逻辑:@ExceptionHandler(AccessDeniedException.class)专门捕获权限不足异常,与其他异常(自定义异常、兜底异常)区分开,返回更精准的提示;
2. 状态码规范:权限不足建议使用403状态码(Forbidden),与401(未登录)区分开——401表示用户未登录,403表示用户已登录但没有对应权限;
3. 提示信息优化:可根据实际需求自定义提示信息,例如“您没有访问该接口的权限,请联系管理员申请权限”,更贴合用户体验;
4. 生产环境优化:生产环境中,建议将e.printStackTrace()替换为日志输出(如使用logback、log4j),避免控制台打印敏感信息,同时便于问题排查。
步骤9:测试角色授权(权限控制)效果
完成权限不足异常处理后,启动Spring Boot项目,使用Postman测试不同角色的权限访问效果,验证权限控制是否生效,步骤如下:
1. 准备测试账号(使用前文补充的测试数据): - 普通用户:张三(name=张三,role_id=1,密码需通过新增接口重新设置,明文密码加密后存储); - 管理员:李四(name=李四,role_id=2,密码同理重新设置); - 新增普通用户:赵六(前文测试新增,role_id=1,密码123456,加密后存储)。
2. 测试步骤:
① 登录普通用户(赵六):POST http://localhost:8080/user/login,请求体{"username":"赵六","password":"123456"},登录成功;
② 访问查询接口(GET http://localhost:8080/user/getAll):返回用户列表,说明普通用户拥有查询权限,符合预期;
③ 访问新增用户接口(POST http://localhost:8080/user/add):请求体{"name":"孙七","age":28,"email":"sunqi@163.com","password":"123456"},返回响应{"code":403,"message":"权限不足,无法访问该接口"},说明普通用户无新增权限,符合预期;
④ 退出登录(GET http://localhost:8080/user/logout),重新登录管理员(李四);
⑤ 访问新增用户接口:返回{"code":200,"message":"新增用户成功"},说明管理员拥有新增权限,符合预期;
⑥ 测试删除接口(DELETE http://localhost:8080/user/delete/1):返回{"code":200,"message":"删除用户成功"},说明管理员拥有删除权限;
⑦ 用普通用户登录后访问删除接口:返回403权限不足响应,验证权限控制生效。
3. 常见问题排查: - 若普通用户能访问管理员接口:检查UserController中@PreAuthorize注解是否添加正确,是否写错角色名(如ADMIN写成Admin); - 若管理员无对应权限:检查role_permission表中,管理员角色(role_id=2)是否关联了对应的权限(如user:add、user:delete); - 若权限不足时返回默认错误页面:检查GlobalExceptionHandler中是否正确捕获AccessDeniedException异常,是否添加@RestControllerAdvice注解。
实战总结:权限管理核心要点与拓展方向
至此,我们已完成Spring Security整合与权限管理的完整实战,从依赖导入、数据库设计,到用户认证、角色授权,再到异常处理,形成了“登录校验→权限控制→异常兜底”的完整安全体系,适配零基础学习者的实操需求,所有代码可直接复用至实际项目。
3.1 核心要点回顾(零基础必记)
1. 核心关系:权限管理的核心是“用户-角色-权限”三者的关联,本文简化为“一个用户对应一个角色,一个角色对应多个权限”,实际开发中可扩展为“多用户-多角色-多权限”(需新增user_role关联表);
2. 核心组件: - UserDetailsService:加载用户信息(从数据库查询),是用户认证的核心; - PasswordEncoder:密码加密工具,Spring Security强制要求密码加密,本文使用BCryptPasswordEncoder; - SecurityConfig:Spring Security核心配置类,配置认证、授权、登录/注销逻辑; - @PreAuthorize:方法级权限控制注解,实现接口级别的角色/权限校验; - 全局异常处理:捕获权限不足异常,返回统一JSON响应。
3. 踩坑重点: - 角色格式必须以“ROLE_”开头,否则Spring Security无法识别; - 前后端分离项目必须关闭CSRF防护,否则登录请求会被拦截; - 未查询到用户时,必须抛出UsernameNotFoundException异常,否则登录校验异常; - 密码必须加密存储,不能存储明文,否则密码校验失败。
3.2 拓展方向(进阶学习,可选)
本文实现的是基础权限管理功能,满足中小型项目需求,后续可根据实际开发场景,拓展以下功能,提升项目安全性和实用性:
1. 多用户-多角色关联:新增user_role关联表,实现一个用户拥有多个角色,更贴合实际业务场景; 2. 自定义密码错误提示:修改UserDetailsServiceImpl,自定义密码错误时的异常信息,替代默认的“Bad credentials”; 3. 登录失败限制:添加登录失败次数限制(如5次失败后锁定账户1小时),防止暴力破解; 4. 记住密码功能:在SecurityConfig中配置rememberMe,实现“记住密码”,下次无需重新登录; 5. JWT令牌认证:替换Session认证,使用JWT令牌实现无状态登录,适配分布式项目; 6. 权限动态分配:开发角色、权限管理接口,实现后台动态分配用户角色、角色权限,无需修改代码; 7. 接口级别的细粒度权限:结合数据库权限表,实现更细粒度的权限控制(如不同用户只能查看自己的相关数据)。
3.3 学习建议
权限管理是Spring Boot项目的核心安全功能,建议大家对照本文步骤,结合前序springboot-demo项目同步实操,重点关注“用户认证”和“角色授权”的核心逻辑,理解每一步代码的作用,避免盲目复制粘贴。实操过程中遇到问题,可重点检查数据库表关联、注解配置、异常捕获三个方面,大部分问题都能快速排查解决。
后续我们将基于本文的权限管理功能,继续学习Spring Boot进阶内容(如JWT、分布式权限、缓存整合等),逐步完善项目的安全体系和功能,助力大家从“零基础”走向“企业级开发”。