若依RBAC权限控制SpringSecurity(自用)
SpringSecurity介绍
(这里是结合若依仅做了SpringSecurity核心配置的简单介绍,如果想要系统性掌握SpringSecurity请移步更专业的文章或官方文档)
Spring Security是一个功能强大的Java安全框架,它提供了全面的安全认证和授权的支持。
与RBAC模型结合使用时,Spring Security能够实现灵活的权限控制。
我们来看下它的二大核心概念,认证和授权。让我们用一个简单的例子来领会它们的意义。
1)认证(Authentication)想象一下,小智同学去图书馆借书。门口的图书管理员会要求小智出示借书证。这个借书证上有小智的照片和姓名,管理员通过它来确认小智的身份。这个过程,就是一个认证的实例。在Spring Security的世界里,认证就像用户登录时提交的用户名和密码,系统通过这些信息来验证“你是谁”。
Spring Security不仅支持传统的用户名和密码认证,还支持OAuth2、JWT等现代认证方式。
2)授权(Authorization) 现在,假设小智同学已经踏入了图书馆的大门,但当小智想进入只有持特定权限才能进入的参考书籍区时,管理员会再次检查小智的借书证,看是否有相应的权限标记。如果有,那么小智就能进入这个区域;如果没有,那么小智只能遗憾地止步。这就是授权的精髓。在Spring Security中,授权是确认用户在通过认证之后,是否有权限执行某些操作或访问特定资源。
SpringSecurity配置
Spring Security的配置类是实现安全控制的核心部分
开启Spring Security各种功能,以确保Web应用程序的安全性,包括认证、授权、会话管理、过滤器添加等。
package com.dkd.framework.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.dkd.framework.config.properties.PermitAllUrlProperties;
import com.dkd.framework.security.filter.JwtAuthenticationTokenFilter;
import com.dkd.framework.security.handle.AuthenticationEntryPointImpl;
import com.dkd.framework.security.handle.LogoutSuccessHandlerImpl;/*** spring security配置** @author ruoyi*/
// 开启方法级别的权限控制 ==> @PreAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{/*** 自定义用户认证逻辑*/@Autowiredprivate UserDetailsService userDetailsService;/*** 认证失败处理类*/@Autowiredprivate AuthenticationEntryPointImpl unauthorizedHandler;/*** 退出处理类*/@Autowiredprivate LogoutSuccessHandlerImpl logoutSuccessHandler;/*** token认证过滤器*/@Autowiredprivate JwtAuthenticationTokenFilter authenticationTokenFilter;/*** 跨域过滤器*/@Autowiredprivate CorsFilter corsFilter;/*** 允许匿名访问的地址*/@Autowiredprivate PermitAllUrlProperties permitAllUrl;/*** 解决 无法直接注入 AuthenticationManager** @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception{return super.authenticationManagerBean();}/*** anyRequest | 匹配所有请求路径* access | SpringEl表达式结果为true时可以访问* anonymous | 匿名可以访问* denyAll | 用户不能访问* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问* hasRole | 如果有参数,参数表示角色,则其角色可以访问* permitAll | 用户可以任意访问* rememberMe | 允许通过remember-me登录的用户访问* authenticated | 用户登录后可访问*/@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception{// 配置URL访问授权规则ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();// 遍历无需认证即可访问的URL列表,设置这些URL对所有用户可访问permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());// 配置Web应用程序规则httpSecurity// CSRF(跨站请求伪造)禁用,因为不使用session.csrf().disable()// 禁用HTTP响应标头.headers().cacheControl().disable().and()// 认证失败处理类.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 过滤请求.authorizeRequests()// 对于登录login 注册register 验证码captchaImage 允许匿名访问.antMatchers("/login", "/register", "/captchaImage").permitAll()// 静态资源,可匿名访问.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated().and().headers().frameOptions().disable();// 添加Logout filterhttpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);// 添加JWT filterhttpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 添加CORS filterhttpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);}/*** 强散列哈希加密实现*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}/*** 身份认证接口*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception{auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());}
}
用户登录流程
管理员在登录页面,输入用户名和密码以及验证码后,点击登录按钮,向后端发送请求,后端通过springSecurity认证管理器进行登录校验
此功能涉及前端相关的代码位于views/login.vue
这个视图组件中,负责实现用户登录界面和交互逻辑。
后端处理逻辑则在dkd-admin
模块的SysLoginController中,负责接收前端的请求,处理登录逻辑,并返回token令牌
前端
-
点击
login.vue
中的登录按钮 -
调用
login.vue
中的handleLogin
方法 -
调用
store/mondles/user.js
中的login
方法,将返回结果存入useUserStore对象中(用于管理用户相关的状态和操作) -
调用
api/login.js
中的login
方法 -
调用
utils/request.js
中的service
实例基于axios发送ajax请求(.env.development文件设置了统一请求路径前缀)
后端
SysLoginController
在ruoyi-admin
模块中com.ruoyi.web.controller.system.SysLoginController
类的login
方法接收前端登录请求
SysLoginService
在ruoyi-framework
模块中com.ruoyi.framework.web.service.SysLoginService
类的login
方法处理登录逻辑
-
验证码校验
-
登录前置校验
-
SS认证管理器用户校验,调用执行UserDetailsServiceImpl.loadUserByUsername
-
认证通过后,创建登录用户对象LoginUser包括用户ID、部门ID、用户信息和用户权限信息
-
-
登录成功,记录日志
-
修改用户表更新登录信息
-
生成token
具体的执行的流程如下图:
获取用户角色和权限
超级管理员登录帝可得系统后,可以查看区域管理所有的权限按钮,实现增删改查操作
而财务人员登录帝可得系统后,仅可以查看区域列表,其他权限没有
1)首先创建一个财务人员的角色,权限字符串 Accountant,仅设置区域管理查询权限
2)再创建一个用户,昵称为黑马彭于晏,用户名hmpyy,密码admin123 角色为财务人员
前端
-
在全局
permission.js
中的router.beforeEach
方法用于在用户导航到不同路由之前进行一些预处理 -
调用
store/mondles/user.js
中的getInfo
方法,将返回结果存入useUserStore对象中(用于管理用户相关的状态和操作) -
调用
api/login.js
中的getInfo
方法
后端
SysLoginController
在ruoyi-admin
模块中com.ruoyi.web.controller.system.SysLoginController
类的getInfo
方法接收前端获取用户信息请求
SysPermissionService
在ruoyi-framework
模块中com.ruoyi.framework.web.service.SysPermissionService
类
-
getRolePermission查询该用户角色集合
-
getMenuPermission查询该用户权限(菜单)集合
具体的执行的流程如下图:
页面权限
前端封装了一个指令权限,能简单快速的实现按钮级别的权限判断。
使用权限字符串 v-hasPermi:@/directive/permission/hasPermi.js
// 单个
<el-button v-hasPermi="['system:user:add']">存在权限字符串才能看到</el-button>
// 多个
<el-button v-hasPermi="['system:user:add', 'system:user:edit']">包含权限字符串才能看到</el-button>
使用角色字符串 v-hasRole@/directive/permission/hasRole.js
// 单个
<el-button v-hasRole="['admin']">管理员才能看到</el-button>
// 多个
<el-button v-hasRole="['role1', 'role2']">包含角色才能看到</el-button>
获取动态菜单路由
在上个小节我们创建了“黑马彭于晏”这个用户,并为他分配了特定的角色权限。这意味着,当他登录帝可得系统时,看到的侧边栏菜单将根据他的角色权限而有所不同。而超级管理员是可以查看所有菜单的
实现此功能的前端代码位于src/permission.js
文件。它在登录成功后,会在跳转到新路由之前,去查询当前用户有权访问的动态菜单的路由列表。
后端处理逻辑则在dkd-admin
模块的SysLoginController中,它负责接收前端发来的请求,处理查询,并构建起一个完整的菜单树结构,然后返回给前端。
前端
-
在全局
permission.js
中的router.beforeEach
方法用于在用户导航到不同路由之前进行一些预处理 -
调用
store/mondles/permission.js
中的generateRoutes
方法,将返回结果存入usePermissionStore对象中 -
调用
api/menu.js
中的getRouters
方法
后端
SysLoginController
在ruoyi-admin
模块中com.ruoyi.web.controller.system.SysLoginController
类的getRouters
方法接收前端获取路由信息请求
ISysMenuService
在ruoyi-system
模块中com.ruoyi.web.system.service.ISysMenuService
类
-
selectMenuTreeByUserId根据用户ID查询菜单树信息(递归生成父子菜单)
-
buildMenus构建前端路由所需要的菜单路由格式RouterVo
具体的执行的流程如下图:
路由菜单加载
-
用户登录成功后,通过路由
router/index.js
跳转到首页并加载layout布局组件 -
在
layout/index.vue
中加载sidbar侧边栏 -
在
layout/components/Sidebar/index.vue
中遍历动态路由菜单在页面显示 -
用户点击菜单后会根据路由的path跳转到对应的视图组件在<app-main />显示
权限注解
源码分析
在若依框架中,权限的验证最核心的是使用的Spring Security的提供的权限注解@PreAuthorize
-
@PreAuthorize 是 Spring Security 框架中提供的一个安全注解,用于实现基于注解的访问控制。它允许开发者在方法级别上声明特定的安全约束,以确保只有满足指定条件的用户才能调用该方法
-
当 @PreAuthorize 注解被应用于某个方法时,Spring Security 在该方法执行前会先对当前认证的用户进行权限检查。如果检查通过,方法调用得以继续;否则,框架会抛出相应的权限异常(如 AccessDeniedException),阻止方法执行。
若依框架中的权限控制代码,如下:
@PreAuthorize("@ss.hasPermi('manage:order:list')")
public TableDataInfo list(Order order)
{startPage();List<Order> list = orderService.selectOrderList(order);return getDataTable(list);
}
-
@PreAuthorize是Spring Security框架的权限注解,在执行方法前执行
-
@ss.hasPermi('manage:order:list')
-
其中的ss是指的一个spring管理的bean
-
位置:ruoyi-framework模块中的com.ruoyi.framework.web.service.PermissionService
-
-
hasPermi 是PermissionService类中的一个方法,判断是否拥有该权限
-
manage:order:list为方法的参数
-
注意:在SecurityConfig类中添加
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@PreAuthorize才能生效
权限控制流程:
权限方法
@PreAuthorize
注解用于配置接口要求用户拥有某些权限才可访问,它拥有如下方法
方法 | 参数 | 描述 |
---|---|---|
hasPermi | String | 验证用户是否具备某权限 |
lacksPermi | String | 验证用户是否不具备某权限,与 hasPermi逻辑相反 |
hasAnyPermi | String | 验证用户是否具有以下任意一个权限 |
hasRole | String | 判断用户是否拥有某个角色 |
lacksRole | String | 验证用户是否不具备某角色,与 hasRole逻辑相反 |
hasAnyRoles | String | 验证用户是否具有以下任意一个角色,多个逗号分隔 |
使用示例
1)数据权限示例。
// 符合system:user:list权限要求
@PreAuthorize("@ss.hasPermi('system:user:list')")// 不符合system:user:list权限要求
@PreAuthorize("@ss.lacksPermi('system:user:list')")// 符合system:user:add或system:user:edit权限要求即可
@PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")
编程式判断是否有资源权限 :
if (SecurityUtils.hasPermi("sys:user:edit"))
{System.out.println("当前用户有编辑用户权限");
}
2)角色权限示例。
// 属于user角色
@PreAuthorize("@ss.hasRole('user')")// 不属于user角色
@PreAuthorize("@ss.lacksRole('user')")// 属于user或者admin之一
@PreAuthorize("@ss.hasAnyRoles('user,admin')")
编程式判断是否有角色权限 :
if (SecurityUtils.hasRole("admin"))
{System.out.println("当前用户有admin角色权限");
}
权限提示:超级管理员拥有所有权限,不受权限约束。
公开接口
如果有些接口是不需要验证权限可以公开访问的,这个时候就需要我们给接口放行。
使用注解方式,只需要在Controller
的类或方法上加入@Anonymous
该注解即可
// @PreAuthorize("@ss.xxxx('....')") 注释或删除掉原有的权限注解
@Anonymous
@GetMapping("/list")
public List<SysXxxx> list(SysXxxx xxxx)
{return xxxxList;
}