Spring Security 框架 实践小项目(实现不同用户登录显示不同菜单以及每个菜单不同权限)
目录
一、效果展示
二、项目搭建
1.数据库准备
2.创建一个springBoot项目
2.创建包
3.添加依赖
4.连接数据库
三、身份验证(Authentication)
四、访问控制(Authorization)
五、其他文件
mapper
登录html
登录跳转
菜单html
菜单跳转(以个人信息为例)
个人信息html
一、效果展示
具体每个界面没有制作,以标签形式展示每个用户登录菜单不同,菜单的权限也不同
二、项目搭建
1.数据库准备
以下是四个表(user
用户表、role
角色表、menu
菜单表和 rm
角色菜单关系表)的SQL建表语句
-- 1. 创建角色表 role
CREATE TABLE IF NOT EXISTS `role` (`rid` INT NOT NULL AUTO_INCREMENT COMMENT '角色ID',`rname` VARCHAR(50) NOT NULL COMMENT '角色名称:老板、店员、客人',PRIMARY KEY (`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';-- 2. 创建菜单表 menu
CREATE TABLE IF NOT EXISTS `menu` (`mid` INT NOT NULL AUTO_INCREMENT COMMENT '菜单ID',`mname` VARCHAR(50) NOT NULL COMMENT '菜单名称',`murl` VARCHAR(100) COMMENT '菜单路由路径',PRIMARY KEY (`mid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';-- 3. 创建用户表 user
CREATE TABLE IF NOT EXISTS `user` (`suid` INT NOT NULL AUTO_INCREMENT COMMENT '用户ID',`rid` INT NOT NULL COMMENT '角色ID,关联 role 表',`suname` VARCHAR(50) NOT NULL COMMENT '用户名',`supwd` VARCHAR(100) NOT NULL COMMENT '密码(建议存储加密后的)',PRIMARY KEY (`suid`),FOREIGN KEY (`rid`) REFERENCES `role`(`rid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';-- 4. 创建角色-菜单-权限关联表 rm
CREATE TABLE IF NOT EXISTS `rm` (`id` INT NOT NULL AUTO_INCREMENT COMMENT '自增主键',`rid` INT NOT NULL COMMENT '角色ID',`mid` INT NOT NULL COMMENT '菜单ID',`permissions` VARCHAR(200) DEFAULT '' COMMENT '权限标识,逗号分隔,如:view,add,edit,delete',PRIMARY KEY (`id`),UNIQUE KEY `uk_rid_mid` (`rid`, `mid`), -- 防止重复配置同一角色同一菜单FOREIGN KEY (`rid`) REFERENCES `role`(`rid`) ON DELETE CASCADE,FOREIGN KEY (`mid`) REFERENCES `menu`(`mid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-菜单-权限关联表';
模拟数据
-- 插入角色
INSERT INTO `role` (`rname`) VALUES
('老板'),
('店员'),
('客人');-- 插入菜单
INSERT INTO `menu` (`mname`, `murl`) VALUES
('个人信息', '/profile'),
('所有用户', '/user'),
('商城首页', '/mall'),
('商品管理', '/product'),
('订单管理', '/order'),
('财务报表', '/finance');-- 插入用户(密码仅为演示,实际应加密)
INSERT INTO `user` (`suid`, `rid`, `suname`, `supwd`) VALUES
(1, 1, 'boss', '123456'),
(2, 2, 'clerk', '123456'),
(3, 3, 'guest', '123456');-- 插入角色菜单权限
INSERT INTO `rm` (`rid`, `mid`, `permissions`) VALUES
-- 老板
(1, 1, 'view,edit'),
(1, 2, 'view,add,edit,delete'),
(1, 3, 'view'),
(1, 4, 'view,add,edit,delete'),
(1, 5, 'view,add,edit,delete'),
(1, 6, 'view,export'),
-- 店员
(2, 1, 'view,edit'),
(2, 3, 'view'),
(2, 4, 'view,add,edit'),
(2, 5, 'view,add,edit,delete'),
-- 客人
(3, 1, 'view'),
(3, 3, 'view');
2.创建一个springBoot项目
2.创建包
3.添加依赖
<!--spring-security插件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--thymeleaf插件依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity6</artifactId></dependency>
4.连接数据库
application.yml文件
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: rooturl: jdbc:mysql://localhost:3306/mybatis0401?useSSL=false&serverTimezone=Asia/Shanghai
三、身份验证(Authentication)
在MyUserServiceImpl文件中实现
@Component
public class MyUserServiceImpl implements UserDetailsService {@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 从数据库查询用户User loginUser = userMapper.findUserByName(username);if (loginUser == null) {throw new UsernameNotFoundException("用户名或密码错误");}// 2. 查询角色Role role = userMapper.findRoleByRid(loginUser.getRid());if (role == null) {throw new UsernameNotFoundException("用户角色不存在");}// 3. 构造权限列表(用于 Spring Security 后端拦截,如 @PreAuthorize)List<GrantedAuthority> authorities = new ArrayList<>();// 查询角色菜单权限List<Menu> menus = userMapper.findMenusAndPermissionsByRid(loginUser.getRid());for (Menu menu : menus) {String permissions = menu.getPermissions();// 如 "user:add,user:delete"if (permissions != null && !"".equals(permissions)) {String[] permissionArray = permissions.split(",");// 以“,”分割for (String permission : permissionArray) {authorities.add(new SimpleGrantedAuthority(permission));}}}// 4. 创建 UserDetailsUserDetails userDetails =new org.springframework.security.core.userdetails.User(username,passwordEncoder.encode(loginUser.getSupwd()),authorities);System.out.println("数据库密码" + loginUser.getSupwd());System.out.println("加载用户: " + username + ", 角色: " + role.getRname() + ", 权限: " + authorities);return userDetails;}
}
四、访问控制(Authorization)
@Configuration
public class SecurityConfig {//密码加密@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder() {};}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/login.html","/common/**").permitAll().anyRequest().authenticated() //login.html不需要被认证,其他资源都需要被认证).formLogin(formLogin -> formLogin.loginPage("/login.html") //指定登录页面.loginProcessingUrl("/login") //指定自定义的form表单请求的路径,会执行UserDetailsService的实现类.defaultSuccessUrl("/main.html") //指定登录成功后跳转的路径.permitAll() //必须允许所有用户访问登录页)//禁用csrf
//在 Spring Security 中,CSRF 指的是 跨站请求伪造。它是一种常见的 Web 安全攻击类型,而 Spring Security 提供了强大的内置机制来防御这种攻击。.csrf(csrf -> csrf.disable())//表示该页面可以在相同域名页面的frame中展示。.headers(header -> header.frameOptions(frameOptions -> frameOptions.sameOrigin()));return http.build();}@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {return web -> web.ignoring().requestMatchers("/images/**", "/css/**","/js/**","/webjars/**");}
}
五、其他文件
mapper
@Mapper
public interface UserMapper {// 根据用户名查询用户信息@Select("select * from user where suname = #{name}")public User findUserByName(String name);//通过用户rid查询角色名@Select("select * from role where rid = #{rid}")public Role findRoleByRid(Integer rid);//查询所有用户@Select("select * from user")List<User> findAll();//根据角色ID查询该角色能访问的所有菜单 + 权限(核心!)@Select("""SELECT m.mid, m.mname, m.murl, rm.permissionsFROM menu mJOIN rm ON m.mid = rm.midWHERE rm.rid = #{rid}ORDER BY m.mid""")List<Menu> findMenusAndPermissionsByRid(Integer rid);}
登录html
样式就不显示了,各位自行设计或交给AI
<body>
<div class="login-container"><h2>系统登录</h2><!-- 登录错误提示 --><div class="error" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION?.message}">用户名或密码错误</div><!-- 登出成功提示 --><div class="error" th:if="${param.logout}" style="color: green; background-color: #e6ffe6;">您已成功退出登录</div><!-- 登录表单 --><form th:action="@{/login}" method="post"><div class="form-group"><label for="username">用户名:</label><input type="text" id="username" name="username" placeholder="请输入用户名" required></div><div class="form-group"><label for="password">密码:</label><input type="password" id="password" name="password" placeholder="请输入密码" required></div><div class="form-group"><input type="submit" value="登录"></div></form>
</div>
</body>
登录跳转
@Controller
public class LoginController {@Autowiredprivate UserMapper userMapper;// 页面跳转@RequestMapping("/login.html")public String login(){return "login";}@RequestMapping("/main.html")public String main(HttpServletRequest request, Model model) {// 1. 获取当前登录用户Authentication authentication = SecurityContextHolder.getContext().getAuthentication();UserDetails userDetails = (UserDetails) authentication.getPrincipal();// 2. 从数据库查询完整用户信息(含 rid)User loginUser = userMapper.findUserByName(userDetails.getUsername());System.out.println("用户 " + loginUser.getSuname() +"密码"+ loginUser.getSupwd());if (loginUser == null) {return "redirect:/login.html";}// 3. 查询该用户角色能访问的菜单+权限List<Menu> menus = userMapper.findMenusAndPermissionsByRid(loginUser.getRid());System.out.println("用户 " + loginUser.getSuname() + " 登录,菜单权限: " + menus);// 4. 存入 session 和 modelHttpSession session = request.getSession();session.setAttribute("user", loginUser);session.setAttribute("menus", menus);model.addAttribute("menus", menus);model.addAttribute("user", loginUser);System.out.println("用户 " + loginUser.getSuname() + " 登录,菜单权限: " + menus);return "main"; // 跳转到 main.html}@RequestMapping("/welcome")public String welcome() {System.out.println("欢迎页面");return "/common/welcome";}
}
菜单html
<!-- 顶栏 -->
<div class="header">🖥️ 后台管理系统
</div><!-- 主体容器 -->
<div class="container"><!-- 左侧菜单 --><div class="sidebar"><div class="menu-title">📌 导航菜单</div><!-- 菜单项 --><div th:each="menu : ${menus}" class="menu-item"><!-- 点击菜单,右侧 iframe 跳转 --><a th:href="${menu.murl}"class="menu-link"th:text="${menu.mname}"target="main-frame"></a><!-- 权限标签 --><div class="permission-tags" th:if="${menu.permissions != null and menu.permissions != ''}"><span th:each="perm : ${#strings.listSplit(menu.permissions, ',')}"th:with="permClean=${#strings.trim(perm)}"th:if="${permClean != ''}"class="permission-tag"th:text="${#strings.replace(#strings.replace(permClean, 'add', '新增'), 'delete', '删除').replace('edit', '编辑').replace('view', '查看')}"></span></div></div><!-- 退出按钮 --><div class="logout-item"><a href="/logout" class="logout-link">🚪 退出系统</a></div></div><!-- 右侧功能区(iframe) -->
<div class="main-content"><div id="loading-message" style="display: none; padding: 20px; text-align: center; color: #1890ff;">正在加载页面,请稍候...</div><!--打开默认界面--><iframe src="/welcome" name="main-frame"onload="hideLoading()"onerror="showError()"style="width: 100%; height: 100%; border: none; display: block;"></iframe><iframe src="/menu-link" name="main-frame"onload="hideLoading()"onerror="showError()"style="width: 100%; height: 100%; border: none; display: block;"></iframe><div id="error-message" style="display: none; padding: 20px; color: #ff4d4f; text-align: center;">页面加载失败,请检查链接是否正确</div>
</div>
菜单跳转(以个人信息为例)
@Controller
public class UserController {@AutowiredUserService userSercive;//个人信息@RequestMapping("/profile")public String profile(Model model , HttpServletRequest request) {// 从 session 中获取当前登录用户User user = (User) request.getSession().getAttribute("user");if (user == null) {return "redirect:/login.html";}//获取角色Role role = userSercive.findRoleByRid(user.getRid());model.addAttribute("user", user);model.addAttribute("role", role);System.out.println("user:"+user + "role:" + role);return "common/profile";}/*。。。。。。。。。。。。。。* 。。。。。。。。。。。* 。。。。。。。。。* 。。。。。。。* 。。。。。* 。。。。* 。。* 。* */}
个人信息html
<body><div class="card"><div class="card-title">👤 个人信息</div><h1>用户信息</h1><!-- 用户基本信息 --><div class="info-group"><span class="info-label">用户名:</span><span class="info-value" th:text="${user.suname}"></span></div><div class="info-group"><span class="info-label">用户账号:</span><span class="info-value" th:text="${user.suid}"></span></div><div class="info-group"><span class="info-label">所属角色:</span><!--从model传过来的role中获取--><span class="info-value" th:text="${role.rname}"></span></div><!-- 修改密码区域 --><div class="section-title">🔒 修改密码</div><!-- 成功/错误消息(通过重定向参数传递) --><div th:if="${param.success}" class="success-msg">✅ 密码修改成功,请妥善保管!</div><div th:if="${param.error}" class="error-msg">❌ <span th:text="${param.error[0]}">修改失败</span></div><form th:action="@{/profile/updatePassword}" method="post"><div class="form-group"><label>原密码:</label><input type="password" name="oldPassword" required placeholder="请输入原密码"></div><div class="form-group"><label>新密码:</label><input type="password" name="newPassword" required minlength="6" placeholder="至少6位字符"></div><div class="form-group"><label>确认新密码:</label><input type="password" name="confirmPassword" required minlength="6" placeholder="请再次输入新密码"></div><button type="submit" class="btn">💾 保存修改</button></form>
</div></body>