当前位置: 首页 > news >正文

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 会自动生成默认用户和随机密码:

  1. 默认用户user
  2. 随机密码:启动日志中打印(格式:Using generated security password: xxxx-xxxx-xxxx);
  3. 访问接口:访问任何接口(如http://localhost:8080/hello),会自动跳转默认登录页;
  4. 登录:输入默认用户和随机密码,登录成功后才能访问接口。

缺点:仅用于测试,无法满足实际项目需求。

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  # 固定密码(开发阶段用简单密码,生产环境需加密)
  1. 效果:启动项目后,日志不再打印随机密码,登录时用test/123
  2. 缺点仅支持单个固定用户,无法满足多用户场景。

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. 测试效果
  1. 启动项目,访问http://localhost:8080/main.html,自动跳转/login.html
  2. 输入正确用户名(zs)和密码(123),登录成功跳转/main.html
  3. 输入错误密码,页面显示 “Bad credentials”(密码错误);
  4. 未登录直接访问/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.murlth: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),再通过配置和前端标签实现权限控制”,关键步骤可总结为:

  1. 数据库设计:明确用户 - 角色 - 菜单 - 权限的多对多关系;
  2. 关联查询:Mapper 层通过多表连接查询用户的完整权限信息;
  3. 封装权限:UserDetailsService 将角色(加 ROLE_前缀)和权限封装成 GrantedAuthority;
  4. 配置规则:SecurityConfig 定义路径与权限的对应关系;
  5. 前端控制:用 sec:authorize 控制菜单和按钮的显示。

通过本文的实战步骤,初学者可以掌握从基础认证到复杂多表权限的完整实现,后续可进一步学习 CSRF 防护、记住我功能、OAuth2.0 集成等高级特性,逐步完善权限体系。

http://www.dtcms.com/a/446953.html

相关文章:

  • 惠州市企业网站seo点击软件小型公司网络搭建
  • 廊坊网站群发关键词seo外包大型公司
  • 前端密码加密方案全解析
  • 厦门市建设局加装电梯公示网站一站式服务平台官网
  • 济南济南网站建设网站权重分散
  • wordpress主题 水墨知乎关键词排名优化
  • 手机wap购物网站模板wordpress站点名称的影响
  • Termux Debian Arm64 源码编译安装坚果云,全平台同步笔记内容(也适用树莓派)
  • UWB | 原理 / 应用 / 算法
  • 厦门中信网站吕梁市住房与城乡建设厅网站
  • 做网站小程序多少钱建筑模拟器2022下载
  • 泉州丰泽建设局网站平面设计课程总结
  • Spring AI alibaba 智能体理论
  • 从零起步学习Redis || 第七章:Redis持久化方案的实现及底层原理解析(RDB快照与AOF日志)
  • python如何拼接图片
  • 云县网站建设找那家咸阳网站推广
  • 网站域名一年大概多少软件开发培训机构去哪个学校
  • VPS如何做镜像网站全网推广网站
  • 老年实训室建设介绍:打造“教-学-练”一体化老年实训建设样板
  • 布吉做棋牌网站建设哪家技术好淘外网站怎么做
  • 吃透链表进阶OJ:从 “怕踩坑” 到 “能讲透”
  • 国内做的比较大的外贸电商网站肇庆做网站设计
  • 重庆梁平网站建设哪家好crm系统排行榜
  • 备案信息修改网站负责人政务服务网站建设情况汇报
  • 南昌网站建设制作陕西省住建厅官网
  • 进步主义的异化:个人权利申索如何蜕变成圣母主义和功利主义
  • node-dommatrix
  • 人工智能赋能传统医疗设施设备改造:未来展望与伦理挑战
  • angular网站模板下载公众号必备50个模板
  • 移动端教学视频网站开发怎么制作网站?