RuoYi后台系统权限管理解析
⼀、前⾔
最近在学习spring curity,⾃⼰也了些⼩的demo。也看了⼏个优秀的后台管理的开源项⽬。今天聊⼀下若依系统的权限管理的详细流程。
⼆、权限管理模型
若依使⽤的也是当前最流⾏的RBAC模型。如果不了解RBAC的⼩伙伴可以去⽹上查⼀下,其实很好理解。若依这⾥⼤致可以认为是实现了RBAC0。简单来说,就是⽤户不直接拥有权限,⽽是添加⾓⾊作为中转,将权限赋予⾓⾊。然后再将⾓⾊赋予⽤户。权限可以是菜单权限或者是按钮权限等。
三、主要技术栈
1、后端
Springboot,SpringSecurity,JWT,Redis,Mybatis
2、前端
vue,vuex,router
四、数据库设计
与权限相关的表主要有三张,sys_ur,sys_role,sys_menu。次要的还有两张关联表sys_ur_role,sys_role_menu。下⾯依次看⼀下主要的表。
1、Ur表
上⾯就是ur表的基本结构,保存了⼀些⽤户的基本资料,以及⽤户状态标志位。没什么特别要说的地⽅。
2、Role表
上⾯是role表的基本结构,定义了⾓⾊的名称以及⾓⾊的标识字符串。还包括了其他模块的⼀些数据,⽐如数据权限的标识,这⾥不做讨论。
3、Menu表
这张表要特别说⼀下。
⾸先是动态菜单的实现。表⾥包括了前端⽣成动态路由router的数据。
其次就是perms字段。这个字段将权限管理的粒度细化到了按钮,也就是你可能可以进⼊某个页⾯。但是⽆法使⽤这个页⾯⾥的所有功能。菜单部分的权限是在渲染页⾯时就确定了,如果你没有某个菜单或⽬录的所有权限,那你的页⾯则不会出现这些⽬录。
五、基本流程
我们观察⼀下点击登录之后,前端⼀共发送了三个请求。
依次看⼀下这些请求都做了什么。
1、login
前后端分离的系统交互⼀般都是⽆状态登录,这⾥使⽤的是jwt实现。登录后续的所有请求都会借助token进⾏权限验证。
2、getInfo
登录成功后,需要获取⼀些公⽤状态,⽐如⽤户名称,⽤户头像信息等。这些状态都被保存在vuex中管理。其实这⾥不是很严谨,这⾥忽略了路由守卫的部分,但是感知不强,会在下⾯详细说⼀下。
3、getRouter
到这⾥就进⾏到⾸页渲染的最后⼀步,获取路由信息。
其实图中的过程不是很严谨,但是这样稍微更好理解⼀些。
这⼀步的⼯作主要由前端来完成,登录完成后,会跳转⾄⾸页。在⾸页渲染之前,路由守卫会做⼀些操作,这⼀部分我在另⼀篇⽂章⾥有详细描述。戳这⾥在这些操作⾥就包括上⾯的接⼝请求,以及这⾥的路由信息的请求。在获取到路由信息后,将信息转化为router对象,再动态挂载路由。然后在左边栏的页⾯部分,遍历router对象⽣成边栏。
六、具体实现(部分)
这⾥会贴⼀些我认为⽐较重要的代码进⾏说明,更多具体的限于篇幅也不搞太多。
1、SpringSecurity
这⾥就直接看配置类了鹦鹉的拼音
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// CSRF禁⽤,因为不使⽤ssion
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要ssion
.ssionManagement().ssionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
/
/ 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers("/profile/**").anonymous()
.antMatchers("/common/download**").anonymous()
.
antMatchers("/common/download/resource**").anonymous()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
// 除上⾯外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
/
/ 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UrnamePasswordAuthenticationFilter.class); // 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
作者已经给配置类做了⼀些注解,这⾥⽐较重要的是添加了jwt过滤器,⽽且是所有的请求都会被这个过滤器拦截,包括"/login", "/captchaImage"。除此之外,配置类⾥并没有声明登录接⼝。那么肯定在某个地⽅加⼊SpringSecurity的过滤链。
其实从Controller层顺藤摸⽠,很快就能看到这个⽅法。这个⽅法验证了⽤户是否合法,并且将⽤户信息保存进了Redis
public String login(String urname, String password, String code, String uuid)
{
// 通过UUID,还原登录前的秘钥
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
// 通过秘钥查询Redis中存储的验证信息
String captcha = CacheObject(verifyKey);
/
/ 删除验证信息
redisCache.deleteObject(verifyKey);
if (captcha == null)
{
<().dLogininfor(urname, Constants.LOGIN_FAIL, ssage("pire")));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCa(captcha))
{
<().dLogininfor(urname, Constants.LOGIN_FAIL, ssage("")));
adj throw new CaptchaException();
}
// ⽤户验证
Authentication authentication = null;
Authentication authentication = null;
try
{
// 该⽅法会去调⽤UrDetailsServiceImpl.loadUrByUrname
authentication = authenticationManager
.authenticate(new UrnamePasswordAuthenticationToken(urname, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
十天突破雅思写作{
<().dLogininfor(urname, Constants.LOGIN_FAIL,
throw new UrPasswordNotMatchException();
}
el
{
<().dLogininfor(urname, Constants.LOGIN_FAIL, e.getMessage())); throw new Message());
}
}
<().dLogininfor(urname, Constants.LOGIN_SUCCESS,
LoginUr loginUr = (LoginUr) Principal();
// ⽣成token
ateToken(loginUr);
}
1
2
3
4
5
6
7
8
vickers
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
29
30
31
32
33
34
35
回复英语
36
flag怎么读
37
38
39
40
41
42
43
44
就是在下⾯这个位置调⽤了authenticationManager,将⽤户名密码加⼊了整个验证链。⽽且作者也在此做了注释该⽅法会去调⽤UrDetailsServiceImpl.loadUrByUrname,也就是我们⾃定义的⽤户验证规则。
// ⽤户验证
Authentication authentication = null;
try
{
// 该⽅法会去调⽤UrDetailsServiceImpl.loadUrByUrname
authentication = authenticationManager
.authenticate(new UrnamePasswordAuthenticationToken(urname, password));
}官僚资本
1
2
3
分析器
proudof4
5
6
7
8
在这⾥会从数据库验证⽤户是否合法。到这基本上就算完成了完整的验证流程。
@Override
public UrDetails loadUrByUrname(String urname) throws UrnameNotFoundException
{
SysUr ur = urService.lectUrByUrName(urname);
if (StringUtils.isNull(ur))
{
log.info("登录⽤户:{} 不存在.", urname);
throw new UrnameNotFoundException("登录⽤户:" + urname + " 不存在");
}
el if (Code().DelFlag()))
{
log.info("登录⽤户:{} 已被删除.", urname);sandara
throw new BaException("对不起,您的账号:" + urname + " 已被删除");
}
el if (Code().Status()))
{
log.info("登录⽤户:{} 已被停⽤.", urname);
throw new BaException("对不起,您的账号:" + urname + " 已停⽤");
}