Spring Security 详解:从基础认证到多表权限实战(初学者指南)
Spring Security 详解:从基础认证到多表权限实战(初学者指南)
结合我们之前学习的 Spring Boot Web 开发、Thymeleaf 整合等知识,本文将聚焦Spring Security的核心能力 —— 从 “基础认证授权” 到 “多表关联权限控制”(用户 / 角色 / 菜单 / 权限表),全程拆解 “每一步做什么、代码怎么写、参数如何对应、预期效果是什么”,帮初学者彻底搞懂复杂场景下的权限验证逻辑,避免 “配置了但不生效” 的问题。
一、Spring Security 核心概念:先搞懂 “认证” 与 “授权”
在学实战前,必须明确两个核心概念,这是后续所有配置的基础:
概念 | 通俗理解 | 核心目标 | 实战场景案例 |
---|---|---|---|
认证(Authentication) | 验证 “你是谁”—— 确认用户身份合法性 | 防止非法用户登录 | 输入用户名密码,验证是否在数据库中存在且密码正确 |
授权(Authorization) | 验证 “你能干什么”—— 确认用户权限范围 | 防止合法用户越权操作 | 普通用户不能访问管理员菜单,编辑按钮只对有 update 权限的用户显示 |
攻击防护 | 防止伪造身份(如 CSRF 跨站请求伪造) | 保障请求合法性 | 禁用 CSRF(开发阶段),生产环境启用并配置令牌 |
二、基础实战:从 “默认认证” 到 “数据库单表认证”
先掌握基础认证流程,为后续多表权限打基础。文档中提到 3 种认证方式,我们按 “简单→复杂” 顺序拆解:
2.1 步骤 1:引入 Spring Security 依赖
只要引入依赖,Spring Security 会自动保护所有接口(默认拦截所有请求,要求登录):
<!-- Spring Security核心依赖:自动启用认证授权 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency><!-- 可选:Thymeleaf整合Security(前端权限控制需要) -->
<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency><!-- 数据库相关依赖(后续数据库认证需要) -->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.1</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
2.2 方式 1:默认认证(快速测试)
引入依赖后,Spring Security 会自动生成默认用户和随机密码:
- 默认用户:
user
; - 随机密码:启动日志中打印(格式:
Using generated security password: xxxx-xxxx-xxxx
); - 访问接口:访问任何接口(如
http://localhost:8080/hello
),会自动跳转默认登录页; - 登录:输入默认用户和随机密码,登录成功后才能访问接口。
缺点:仅用于测试,无法满足实际项目需求。
2.3 方式 2:配置文件认证(固定用户)
在application.yml
中配置固定用户名和密码,覆盖默认认证:
spring:datasource: # 数据库配置(后续数据库认证用,先配置)driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_db?serverTimezone=Asia/Shanghai&useSSL=falseusername: rootpassword: 123456security: # Security认证配置user:name: test # 固定用户名password: 123 # 固定密码(开发阶段用简单密码,生产环境需加密)
- 效果:启动项目后,日志不再打印随机密码,登录时用
test/123
; - 缺点:仅支持单个固定用户,无法满足多用户场景。
2.4 方式 3:数据库单表认证(自定义 UserDetailsService)
在实战项目中,我们经常 “通过数据库加载用户”,这是实际项目的基础,步骤如下:
2.4.1 1. 数据库表设计(单表:用户表)
CREATE TABLE `user` (`id` INT PRIMARY KEY AUTO_INCREMENT,`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',`password` VARCHAR(100) NOT NULL COMMENT '加密后的密码',`status` TINYINT DEFAULT 1 COMMENT '状态:1启用,0禁用'
);
-- 插入测试数据(密码123加密后:$2a$10$xxxxxx...,用BCrypt加密)
INSERT INTO user (username, password) VALUES ('zs', '$2a$10$G9h6k8j7l6m5n4b3v2c1d0a9s8d7f6g5h4j3k2l1m0n1b2v3c4d5e6f7g8h9j0k1l2m3n4b5v6c7d8e9f0');
2.4.2 2. 实体类(User)
package com.lh.security.entity;import lombok.Data;@Data
public class User {private Integer id;private String username; // 对应数据库usernameprivate String password; // 对应数据库password(加密后)private Integer status; // 账号状态
}
2.4.3 3. Mapper 层(UserMapper)
查询用户(按用户名,因为 Spring Security 默认按用户名认证):
package com.lh.security.mapper;import com.lh.security.entity.User;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;@Repository
public interface UserMapper {// 按用户名查询用户(Spring Security认证时会调用此方法)@Select("SELECT id, username, password, status FROM user WHERE username = #{username}")User selectByUsername(String username);
}
- 注意:启动类需加
@MapperScan("com.lh.security.mapper")
,否则 Mapper 无法扫描。
2.4.4 4. 实现 UserDetailsService(核心:加载用户信息)
Spring Security 通过UserDetailsService
接口加载用户信息,我们需要实现它,从数据库查询用户并封装成UserDetails
:
package com.lh.security.service;import com.lh.security.entity.User;
import com.lh.security.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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PasswordEncoder passwordEncoder; // 密码加密器,后续配置/*** 认证时自动调用:按用户名查询用户,并封装成UserDetails* @param username 前端输入的用户名* @return UserDetails Spring Security需要的用户详情对象*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 从数据库查询用户User dbUser = userMapper.selectByUsername(username);if (dbUser == null) {// 用户名不存在,抛出异常(Spring Security会自动返回“用户名或密码错误”)throw new UsernameNotFoundException("用户名不存在");}if (dbUser.getStatus() == 0) {// 账号禁用throw new RuntimeException("账号已禁用");}// 2. 封装权限(单表场景先模拟一个权限,多表场景会从数据库查)List<GrantedAuthority> authorities = new ArrayList<>();// 角色需要加“ROLE_”前缀,权限直接用标识(如"user:select")authorities.add(new SimpleGrantedAuthority("ROLE_user")); // 角色:普通用户authorities.add(new SimpleGrantedAuthority("user:select")); // 权限:查询// 3. 封装成UserDetails(Spring Security提供的实现类)// 注意:第二个参数是数据库中加密后的密码,第三个是权限列表return new org.springframework.security.core.userdetails.User(dbUser.getUsername(),dbUser.getPassword(), // 数据库存加密后的密码,无需再次加密authorities);}
}
2.4.5 5. 配置 Security(密码加密器 + 认证管理器)
package com.lh.security.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;@Configuration
@EnableWebSecurity // 启用Spring Security
public class SecurityConfig {// 1. 配置密码加密器(必须!Spring Security要求密码必须加密)@Beanpublic PasswordEncoder passwordEncoder() {// BCrypt加密算法:不可逆,每次加密结果不同,但能验证return new BCryptPasswordEncoder();}// 2. 配置认证管理器(用于认证用户,自动关联UserDetailsService)@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}// 3. 配置Security过滤器链(授权规则、登录登出、CSRF等)@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 授权规则配置.authorizeHttpRequests(auth -> auth.requestMatchers("/login.html", "/css/**", "/js/**").permitAll() // 登录页和静态资源允许匿名访问.anyRequest().authenticated() // 其他所有请求需要认证(登录后才能访问))// 登录配置.formLogin(form -> form.loginPage("/login.html") // 自定义登录页面路径.loginProcessingUrl("/login") // 登录请求提交路径(与登录页form的action对应).defaultSuccessUrl("/main.html") // 登录成功后跳转路径.permitAll() // 登录相关路径允许匿名访问)// 登出配置(默认路径是/logout,登出后跳转登录页).logout(logout -> logout.logoutSuccessUrl("/login.html") // 登出成功后跳转登录页.permitAll())// 禁用CSRF(开发阶段方便测试,生产环境需启用并配置令牌).csrf(csrf -> csrf.disable());return http.build();}
}
2.4.6 6. 自定义登录页面(login.html)
放在src/main/resources/templates
目录,action 需与loginProcessingUrl("/login")
对应:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录页</title>
</head>
<body><!-- action="/login" 对应Security配置的loginProcessingUrl --><form th:action="@{/login}" method="post"><div><label>用户名:</label><input type="text" name="username" required> <!-- name必须是username --></div><div><label>密码:</label><input type="password" name="password" required> <!-- name必须是password --></div><!-- 错误提示:param.error是Spring Security自动传递的错误参数 --><p style="color: red" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></p><button type="submit">登录</button></form>
</body>
</html>
2.4.7 7. 测试效果
- 启动项目,访问
http://localhost:8080/main.html
,自动跳转/login.html
; - 输入正确用户名(zs)和密码(123),登录成功跳转
/main.html
; - 输入错误密码,页面显示 “Bad credentials”(密码错误);
- 未登录直接访问
/main.html
,被拦截跳转登录页。
三、进阶实战:多表权限控制(用户 / 角色 / 菜单 / 权限)
这是实际项目的核心场景!一般我们可以设计7张表(用户、角色、菜单、权限 + 3 张中间表),我们详细拆解 “从数据库设计到前后端权限控制” 的完整流程。
3.1 第一步:明确多表结构与关系(核心前提)
先确认每张表的字段和关联关系,避免后续 SQL 和实体类设计错误:
表名 | 核心字段 | 作用 | 关联关系 |
---|---|---|---|
user(用户表) | id, username, password, status | 存储用户基本信息 | 与 role 多对多,通过 user_role 中间表关联 |
role(角色表) | id, rname, rdesc | 存储角色(如 “管理员”“普通用户”) | 与 user 多对多(user_role),与 menu 多对多(role_menu),与 permission 多对多(role_permission) |
menu(菜单表) | id, mname, murl, mlevel, msort | 存储前端菜单(如 “商品管理”“订单管理”) | 与 role 多对多,通过 role_menu 中间表关联 |
permission(权限表) | id, pname, pcode | 存储操作权限(如 “insert”“delete”) | 与 role 多对多,通过 role_permission 中间表关联 |
user_role(中间表) | id, user_id, role_id | 关联用户和角色 | user_id 关联 user.id,role_id 关联 role.id |
role_menu(中间表) | id, role_id, menu_id | 关联角色和菜单 | role_id 关联 role.id,menu_id 关联 menu.id |
role_permission(中间表) | id, role_id, permission_id | 关联角色和权限 | role_id 关联 role.id,permission_id 关联 permission.id |
测试数据插入:
-- 1. 权限表
INSERT INTO permission (pname, pcode) VALUES
('新增', 'insert'), ('删除', 'delete'), ('修改', 'update'), ('查询', 'select');-- 2. 角色表
INSERT INTO role (rname, rdesc) VALUES
('管理员', '拥有所有权限'), ('普通用户', '拥有查询和新增权限');-- 3. 角色权限中间表
INSERT INTO role_permission (role_id, permission_id) VALUES
(1,1), (1,2), (1,3), (1,4), -- 管理员有所有权限
(2,1), (2,4); -- 普通用户有新增和查询权限-- 4. 菜单表
INSERT INTO menu (mname, murl, mlevel, msort) VALUES
('商品管理', '/goods', 1, 1),
('订单管理', '/order', 1, 2),
('用户管理', '/user', 1, 3); -- 管理员能看到所有菜单,普通用户只能看商品管理-- 5. 角色菜单中间表
INSERT INTO role_menu (role_id, menu_id) VALUES
(1,1), (1,2), (1,3), -- 管理员能看所有菜单
(2,1); -- 普通用户只能看商品管理-- 6. 用户表(密码123加密后)
INSERT INTO user (username, password, status) VALUES
('admin', '$2a$10$G9h6k8j7l6m5n4b3v2c1d0a9s8d7f6g5h4j3k2l1m0n1b2v3c4d5e6f7g8h9j0k1l2m3n4b5v6c7d8e9f0', 1), -- 管理员
('user1', '$2a$10$G9h6k8j7l6m5n4b3v2c1d0a9s8d7f6g5h4j3k2l1m0n1b2v3c4d5e6f7g8h9j0k1l2m3n4b5v6c7d8e9f0', 1); -- 普通用户-- 7. 用户角色中间表
INSERT INTO user_role (user_id, role_id) VALUES
(1,1), -- admin关联管理员角色
(2,2); -- user1关联普通用户角色
3.2 第二步:实体类设计(含关联关系)
实体类需包含表间的关联关系,比如 User 包含 Role 集合,Role 包含 Menu 和 Permission 集合:
3.2.1 1. Permission 实体类
package com.lh.security.entity;import lombok.Data;@Data
public class Permission {private Integer id;private String pname; // 权限名称(如“新增”)private String pcode; // 权限标识(如“insert”,与前端sec:authorize对应)
}
3.2.2 2. Menu 实体类
package com.lh.security.entity;import lombok.Data;@Data
public class Menu {private Integer id;private String mname; // 菜单名称(如“商品管理”)private String murl; // 菜单跳转路径(如“/goods”,与前端href对应)private Integer mlevel; // 菜单级别(1级菜单、2级菜单)private Integer msort; // 菜单排序
}
3.2.3 3. Role 实体类
package com.lh.security.entity;import lombok.Data;
import java.util.List;@Data
public class Role {private Integer id;private String rname; // 角色名称(如“管理员”)private String rdesc; // 角色描述// 关联关系:一个角色有多个菜单、多个权限private List<Menu> menus;private List<Permission> permissions;
}
3.2.4 4. User 实体类
package com.lh.security.entity;import lombok.Data;
import java.util.List;@Data
public class User {private Integer id;private String username;private String password;private Integer status;// 关联关系:一个用户有多个角色private List<Role> roles;
}
3.3 第三步:Mapper 层(关联查询用户的角色、菜单、权限)
核心是 “按用户名查询用户时,关联查询其角色,角色关联的菜单和权限”,需要写关联 SQL:
3.3.1 1. UserMapper(核心:关联查询)
package com.lh.security.mapper;import com.lh.security.entity.User;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Many;
import org.springframework.stereotype.Repository;@Repository
public interface UserMapper {/*** 按用户名查询用户,并关联查询角色、菜单、权限* @param username 用户名* @return 包含角色、菜单、权限的User对象*/@Select("SELECT id, username, password, status FROM user WHERE username = #{username}")@Results({// 1. 映射用户基本字段@Result(column = "id", property = "id"),@Result(column = "username", property = "username"),@Result(column = "password", property = "password"),@Result(column = "status", property = "status"),// 2. 关联查询角色(通过user.id查user_role,再查role)@Result(column = "id", // 用user的id作为参数查角色property = "roles", // 映射到User的roles集合many = @Many(select = "com.zh.security.mapper.RoleMapper.selectByUserId"))})User selectByUsernameWithRolesMenusPermissions(String username);
}
3.3.2 2. RoleMapper(查询角色的菜单和权限)
package com.lh.security.mapper;import com.lh.security.entity.Role;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Many;
import org.springframework.stereotype.Repository;
import java.util.List;@Repository
public interface RoleMapper {/*** 按用户id查询角色,并关联查询菜单和权限* @param userId 用户id* @return 包含菜单和权限的Role集合*/@Select("SELECT r.id, r.rname, r.rdesc " +"FROM role r " +"JOIN user_role ur ON r.id = ur.role_id " +"WHERE ur.user_id = #{userId}")@Results({// 1. 映射角色基本字段@Result(column = "id", property = "id"),@Result(column = "rname", property = "rname"),@Result(column = "rdesc", property = "rdesc"),// 2. 关联查询菜单(通过role.id查role_menu,再查menu)@Result(column = "id",property = "menus",many = @Many(select = "com.zh.security.mapper.MenuMapper.selectByRoleId")),// 3. 关联查询权限(通过role.id查role_permission,再查permission)@Result(column = "id",property = "permissions",many = @Many(select = "com.zh.security.mapper.PermissionMapper.selectByRoleId"))})List<Role> selectByUserId(Integer userId);
}
3.3.3 3. MenuMapper 和 PermissionMapper
// MenuMapper.java
@Repository
public interface MenuMapper {// 按角色id查询菜单@Select("SELECT m.id, m.mname, m.murl, m.mlevel, m.msort " +"FROM menu m " +"JOIN role_menu rm ON m.id = rm.menu_id " +"WHERE rm.role_id = #{roleId} " +"ORDER BY m.msort")List<Menu> selectByRoleId(Integer roleId);
}// PermissionMapper.java
@Repository
public interface PermissionMapper {// 按角色id查询权限@Select("SELECT p.id, p.pname, p.pcode " +"FROM permission p " +"JOIN role_permission rp ON p.id = rp.permission_id " +"WHERE rp.role_id = #{roleId}")List<Permission> selectByRoleId(Integer roleId);
}
3.4 第四步:升级 UserDetailsService(封装多表权限)
修改MyUserDetailsService
,将查询到的角色和权限封装成GrantedAuthority
,注意角色需加 “ROLE_” 前缀:
@Service
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 关联查询用户+角色+菜单+权限User dbUser = userMapper.selectByUsernameWithRolesMenusPermissions(username);if (dbUser == null) {throw new UsernameNotFoundException("用户名不存在");}if (dbUser.getStatus() == 0) {throw new RuntimeException("账号已禁用");}// 2. 封装GrantedAuthority(角色+权限)List<GrantedAuthority> authorities = new ArrayList<>();for (Role role : dbUser.getRoles()) {// 2.1 封装角色:必须加“ROLE_”前缀(Spring Security识别角色的约定)authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRname()));// 2.2 封装权限:直接用permission的pcode(如“insert”“delete”)for (Permission perm : role.getPermissions()) {authorities.add(new SimpleGrantedAuthority(perm.getPcode()));}}// 3. 封装UserDetails(额外:将用户的角色和菜单存入Session,供前端使用)// 这里用自定义UserDetails实现类,方便携带额外信息(如roles、menus)return new CustomUserDetails(dbUser.getUsername(),dbUser.getPassword(),authorities,dbUser.getRoles() // 携带角色(含菜单));}// 自定义UserDetails实现类:携带角色和菜单信息public static class CustomUserDetails implements UserDetails {private final String username;private final String password;private final Collection<? extends GrantedAuthority> authorities;private List<Role> roles; // 额外携带的角色信息(含菜单)public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, List<Role> roles) {this.username = username;this.password = password;this.authorities = authorities;this.roles = roles;}// Getter(roles需要暴露给外部)public List<Role> getRoles() {return roles;}// 实现UserDetails的抽象方法@Override public String getUsername() { return username; }@Override public String getPassword() { return password; }@Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }@Override public boolean isAccountNonExpired() { return true; } // 账号未过期@Override public boolean isAccountNonLocked() { return true; } // 账号未锁定@Override public boolean isCredentialsNonExpired() { return true; } // 密码未过期@Override public boolean isEnabled() { return true; } // 账号启用}
}
3.5 第五步:升级 Security 配置(基于权限的授权规则)
在SecurityConfig
中添加基于权限的路径授权规则(如/goods/insert
需要 “insert” 权限):
@Configuration
@EnableWebSecurity
public class SecurityConfig {// 密码加密器和认证管理器配置不变...@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth// 1. 匿名访问路径.requestMatchers("/login.html", "/css/**", "/js/**").permitAll()// 2. 基于权限的路径规则(顺序:具体路径在前,模糊路径在后).requestMatchers("/goods/insert").hasAuthority("insert") // 新增商品需要insert权限.requestMatchers("/goods/delete").hasAuthority("delete") // 删除商品需要delete权限.requestMatchers("/goods/update").hasAuthority("update") // 修改商品需要update权限.requestMatchers("/goods/**").hasAuthority("select") // 商品相关其他路径需要select权限.requestMatchers("/user/**").hasRole("管理员") // 用户管理需要“管理员”角色(自动加ROLE_前缀)// 3. 其他所有路径需要认证.anyRequest().authenticated())// 登录、登出配置不变....formLogin(form -> form.loginPage("/login.html").loginProcessingUrl("/login").defaultSuccessUrl("/main.html").permitAll()).logout(logout -> logout.logoutSuccessUrl("/login.html").permitAll()).csrf(csrf -> csrf.disable());return http.build();}
}
3.6 第六步:前端权限控制(菜单显示 + 按钮显示)
结合 Thymeleaf 的sec
命名空间,实现 “菜单是否显示”(基于角色菜单)和 “按钮是否显示”(基于角色权限):
3.6.1 1. 登录成功页(main.html)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head><meta charset="UTF-8"><title>首页</title><style>.menu { list-style: none; padding: 0; margin: 20px 0; }.menu li { display: inline-block; margin-right: 20px; }.menu a { text-decoration: none; color: #333; }.menu a:hover { color: #1890ff; }.btn { margin-right: 10px; padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; }.btn-insert { background: #4CAF50; color: white; }.btn-delete { background: #f44336; color: white; }.btn-update { background: #ff9800; color: white; }.btn-select { background: #2196F3; color: white; }</style>
</head>
<body><h1>欢迎,<span sec:authentication="name"></span>!</h1><a href="/logout">退出登录</a><!-- 1. 菜单显示控制:基于角色拥有的菜单 --><h3>系统菜单</h3><ul class="menu" th:each="role : ${session.loginUser.roles}"><li th:each="menu : ${role.menus}"><a th:href="@{${menu.murl}}" th:text="${menu.mname}"></a></li></ul><!-- 2. 按钮显示控制:基于角色拥有的权限 --><h3>商品管理操作</h3><button class="btn btn-insert" sec:authorize="hasAuthority('insert')">新增商品</button><button class="btn btn-delete" sec:authorize="hasAuthority('delete')">删除商品</button><button class="btn btn-update" sec:authorize="hasAuthority('update')">修改商品</button><button class="btn btn-select" sec:authorize="hasAuthority('select')">查询商品</button><!-- 3. 角色和权限信息显示(调试用) --><div style="margin-top: 30px; color: #666;"><p>当前角色:<span sec:authentication="authorities"></span></p><p>客户端IP:<span sec:authentication="details.remoteAddress"></span></p></div>
</body>
</html>
3.6.2 2. 控制器(将用户信息存入 Session)
在登录成功跳转的/main.html
控制器中,将自定义 UserDetails 中的角色菜单n存入 Sessio,供前端使用:
package com.lh.security.controller;import com.lh.security.service.MyUserDetailsService;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.http.HttpSession;@Controller
public class MainController {@RequestMapping("/main.html")public String toMain(Authentication authentication, HttpSession session) {// 1. 获取自定义UserDetails(含角色和菜单)MyUserDetailsService.CustomUserDetails userDetails = (MyUserDetailsService.CustomUserDetails) authentication.getPrincipal();// 2. 将用户信息(含角色菜单)存入Session,供前端使用session.setAttribute("loginUser", userDetails);return "main"; // 跳转到main.html}// 商品管理页面(示例)@RequestMapping("/goods")public String toGoods() {return "goods"; // 对应templates/goods.html}// 订单管理页面(示例)@RequestMapping("/order")public String toOrder() {return "order";}// 用户管理页面(示例)@RequestMapping("/user")public String toUser() {return "user";}
}
3.7 第七步:测试与预期效果
3.7.1 1. 管理员账号(admin/123)登录
- 菜单显示:商品管理、订单管理、用户管理(所有菜单);
- 按钮显示:新增、删除、修改、查询(所有按钮);
- 路径访问:能访问
/user/**
(用户管理)、/goods/insert
(新增商品); - 预期效果:拥有所有权限,无功能限制。
3.7.2 2. 普通用户账号(user1/123)登录
- 菜单显示:仅显示商品管理(无订单、用户管理);
- 按钮显示:仅显示新增、查询(无删除、修改);
- 路径访问:
- 访问
/goods/insert
(新增商品):成功; - 访问
/goods/delete
(删除商品):被拦截,跳转到登录页(无 delete 权限); - 访问
/user
(用户管理):被拦截,跳转到登录页(无 “管理员” 角色);
- 访问
- 预期效果:仅能使用商品管理的新增和查询功能,符合角色权限配置。
四、关键参数对应关系(初学者必记)
多表权限场景中,参数对应错误是最常见的问题,必须明确以下对应关系:
后端参数 / 配置 | 前端参数 / 配置 | 数据库字段 |
---|---|---|
GrantedAuthority ("ROLE_管理员") | sec:authorize="hasRole (' 管理员 ')" | role.rname = "管理员" |
GrantedAuthority("insert") | sec:authorize="hasAuthority('insert')" | permission.pcode = "insert" |
CustomUserDetails.roles | ${session.loginUser.roles} | user_role 关联的 role 集合 |
Role.menus | ${role.menus} | role_menu 关联的 menu 集合 |
Menu.murl | th:href="@{${menu.murl}}" | menu.murl = "/goods" |
loginProcessingUrl("/login") | form th:action="@{/login}" | 无(配置约定) |
五、初学者常见问题与解决方案
问题描述 | 解决方案 |
---|---|
密码正确但登录失败(Bad credentials) | 1. 数据库存的是明文密码,未用 PasswordEncoder 加密;2. 加密时用的算法与配置的 PasswordEncoder 不一致(必须用 BCrypt) |
角色权限不生效(hasRole/hasAuthority 返回 false) | 1. 角色未加 “ROLE_” 前缀;2. 权限标识(如 “insert”)与数据库 pcode 不一致;3. UserDetails 的 authorities 未正确封装 |
菜单不显示(${session.loginUser.roles} 为空) | 1. Mapper 层关联查询 SQL 错误,未查到角色菜单;2. 控制器未将 CustomUserDetails 存入 Session;3. 前端 Thymeleaf 表达式错误 |
路径授权规则不生效(如 /user/** 允许普通用户访问) | 授权规则顺序错误,具体路径(如 /user/)应放在模糊路径(如 /)前面;2. hasRole 参数未去掉 “ROLE_” 前缀(hasRole ("管理员") 对应 ROLE_管理员) |
六、总结
Spring Security 多表权限控制的核心逻辑是 “从数据库查询用户的角色、菜单、权限,封装成 Spring Security 能识别的格式(GrantedAuthority),再通过配置和前端标签实现权限控制”,关键步骤可总结为:
- 数据库设计:明确用户 - 角色 - 菜单 - 权限的多对多关系;
- 关联查询:Mapper 层通过多表连接查询用户的完整权限信息;
- 封装权限:UserDetailsService 将角色(加 ROLE_前缀)和权限封装成 GrantedAuthority;
- 配置规则:SecurityConfig 定义路径与权限的对应关系;
- 前端控制:用 sec:authorize 控制菜单和按钮的显示。
通过本文的实战步骤,初学者可以掌握从基础认证到复杂多表权限的完整实现,后续可进一步学习 CSRF 防护、记住我功能、OAuth2.0 集成等高级特性,逐步完善权限体系。