RuoYi后台系统权限管理解析
一、前言
最近在學(xué)習(xí)spring security,自己也了些小的demo。也看了幾個(gè)優(yōu)秀的后臺(tái)管理的開(kāi)源項(xiàng)目。今天聊一下若依系統(tǒng)的權(quán)限管理的詳細(xì)流程。
二、權(quán)限管理模型
若依使用的也是當(dāng)前最流行的RBAC模型。如果不了解RBAC的小伙伴可以去網(wǎng)上查一下,其實(shí)很好理解。若依這里大致可以認(rèn)為是實(shí)現(xiàn)了RBAC0。簡(jiǎn)單來(lái)說(shuō),就是用戶不直接擁有權(quán)限,而是添加角色作為中轉(zhuǎn),將權(quán)限賦予角色。然后再將角色賦予用戶。權(quán)限可以是菜單權(quán)限或者是按鈕權(quán)限等。
三、主要技術(shù)棧
1、后端
Springboot,SpringSecurity,JWT,Redis,Mybatis
2、前端
vue,vuex,router
四、數(shù)據(jù)庫(kù)設(shè)計(jì)
與權(quán)限相關(guān)的表主要有三張,sys_user,sys_role,sys_menu。次要的還有兩張關(guān)聯(lián)表sys_user_role,sys_role_menu。下面依次看一下主要的表。
1、User表
上面就是user表的基本結(jié)構(gòu),保存了一些用戶的基本資料,以及用戶狀態(tài)標(biāo)志位。沒(méi)什么特別要說(shuō)的地方。
2、Role表
上面是role表的基本結(jié)構(gòu),定義了角色的名稱以及角色的標(biāo)識(shí)字符串。還包括了其他模塊的一些數(shù)據(jù),比如數(shù)據(jù)權(quán)限的標(biāo)識(shí),這里不做討論。
3、Menu表
這張表要特別說(shuō)一下。
首先是動(dòng)態(tài)菜單的實(shí)現(xiàn)。表里包括了前端生成動(dòng)態(tài)路由router的數(shù)據(jù)。
其次就是perms字段。這個(gè)字段將權(quán)限管理的粒度細(xì)化到了按鈕,也就是你可能可以進(jìn)入某個(gè)頁(yè)面。但是無(wú)法使用這個(gè)頁(yè)面里的所有功能。菜單部分的權(quán)限是在渲染頁(yè)面時(shí)就確定了,如果你沒(méi)有某個(gè)菜單或目錄的所有權(quán)限,那你的頁(yè)面則不會(huì)出現(xiàn)這些目錄。
五、基本流程
我們觀察一下點(diǎn)擊登錄之后,前端一共發(fā)送了三個(gè)請(qǐng)求。
依次看一下這些請(qǐng)求都做了什么。
1、login
前后端分離的系統(tǒng)交互一般都是無(wú)狀態(tài)登錄,這里使用的是jwt實(shí)現(xiàn)。登錄后續(xù)的所有請(qǐng)求都會(huì)借助token進(jìn)行權(quán)限驗(yàn)證。
2、getInfo
登錄成功后,需要獲取一些公用狀態(tài),比如用戶名稱,用戶頭像信息等。這些狀態(tài)都被保存在vuex中管理。其實(shí)這里不是很嚴(yán)謹(jǐn),這里忽略了路由守衛(wèi)的部分,但是感知不強(qiáng),會(huì)在下面詳細(xì)說(shuō)一下。
3、getRouter
到這里就進(jìn)行到首頁(yè)渲染的最后一步,獲取路由信息。
其實(shí)圖中的過(guò)程不是很嚴(yán)謹(jǐn),但是這樣稍微更好理解一些。
這一步的工作主要由前端來(lái)完成,登錄完成后,會(huì)跳轉(zhuǎn)至首頁(yè)。在首頁(yè)渲染之前,路由守衛(wèi)會(huì)做一些操作,這一部分我在另一篇文章里有詳細(xì)描述。戳這里在這些操作里就包括上面的接口請(qǐng)求,以及這里的路由信息的請(qǐng)求。在獲取到路由信息后,將信息轉(zhuǎn)化為router對(duì)象,再動(dòng)態(tài)掛載路由。然后在左邊欄的頁(yè)面部分,遍歷router對(duì)象生成邊欄。
六、具體實(shí)現(xiàn)(部分)
這里會(huì)貼一些我認(rèn)為比較重要的代碼進(jìn)行說(shuō)明,更多具體的限于篇幅也不搞太多。
1、SpringSecurity
這里就直接看配置類了
@Override
? ? protected void configure(HttpSecurity httpSecurity) throws Exception
? ? {
? ? ? ? httpSecurity
? ? ? ? ? ? ? ? // CSRF禁用,因?yàn)椴皇褂胹ession
? ? ? ? ? ? ? ? .csrf().disable()
? ? ? ? ? ? ? ? // 認(rèn)證失敗處理類
? ? ? ? ? ? ? ? .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
? ? ? ? ? ? ? ? // 基于token,所以不需要session
? ? ? ? ? ? ? ? .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
? ? ? ? ? ? ? ? // 過(guò)濾請(qǐng)求
? ? ? ? ? ? ? ? .authorizeRequests()
? ? ? ? ? ? ? ? // 對(duì)于登錄login 驗(yàn)證碼captchaImage 允許匿名訪問(wèn)
? ? ? ? ? ? ? ? .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()
? ? ? ? ? ? ? ? // 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
? ? ? ? ? ? ? ? .anyRequest().authenticated()
? ? ? ? ? ? ? ? .and()
? ? ? ? ? ? ? ? .headers().frameOptions().disable();
? ? ? ? httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
? ? ? ? // 添加JWT filter
? ? ? ? httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.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
作者已經(jīng)給配置類做了一些注解,這里比較重要的是添加了jwt過(guò)濾器,而且是所有的請(qǐng)求都會(huì)被這個(gè)過(guò)濾器攔截,包括"/login", "/captchaImage"。除此之外,配置類里并沒(méi)有聲明登錄接口。那么肯定在某個(gè)地方加入SpringSecurity的過(guò)濾鏈。
其實(shí)從Controller層順藤摸瓜,很快就能看到這個(gè)方法。這個(gè)方法驗(yàn)證了用戶是否合法,并且將用戶信息保存進(jìn)了Redis
public String login(String username, String password, String code, String uuid)
? ? {
? ? ? ? // 通過(guò)UUID,還原登錄前的秘鑰
? ? ? ? String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
? ? ? ? // 通過(guò)秘鑰查詢Redis中存儲(chǔ)的驗(yàn)證信息
? ? ? ? String captcha = redisCache.getCacheObject(verifyKey);
? ? ? ? // 刪除驗(yàn)證信息
? ? ? ? redisCache.deleteObject(verifyKey);
? ? ? ? if (captcha == null)
? ? ? ? {
? ? ? ? ? ? AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
? ? ? ? ? ? throw new CaptchaExpireException();
? ? ? ? }
? ? ? ? if (!code.equalsIgnoreCase(captcha))
? ? ? ? {
? ? ? ? ? ? AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
? ? ? ? ? ? throw new CaptchaException();
? ? ? ? }
? ? ? ? // 用戶驗(yàn)證
? ? ? ? Authentication authentication = null;
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? // 該方法會(huì)去調(diào)用UserDetailsServiceImpl.loadUserByUsername
? ? ? ? ? ? authentication = authenticationManager
? ? ? ? ? ? ? ? ? ? .authenticate(new UsernamePasswordAuthenticationToken(username, password));
? ? ? ? }
? ? ? ? catch (Exception e)
? ? ? ? {
? ? ? ? ? ? if (e instanceof BadCredentialsException)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
? ? ? ? ? ? ? ? throw new UserPasswordNotMatchException();
? ? ? ? ? ? }
? ? ? ? ? ? else
? ? ? ? ? ? {
? ? ? ? ? ? ? ? AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
? ? ? ? ? ? ? ? throw new CustomException(e.getMessage());
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
? ? ? ? LoginUser loginUser = (LoginUser) authentication.getPrincipal();
? ? ? ? // 生成token
? ? ? ? return tokenService.createToken(loginUser);
? ? }
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
41
42
43
44
就是在下面這個(gè)位置調(diào)用了authenticationManager,將用戶名密碼加入了整個(gè)驗(yàn)證鏈。而且作者也在此做了注釋該方法會(huì)去調(diào)用UserDetailsServiceImpl.loadUserByUsername,也就是我們自定義的用戶驗(yàn)證規(guī)則。
? ? ? ? // 用戶驗(yàn)證
? ? ? ? Authentication authentication = null;
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? // 該方法會(huì)去調(diào)用UserDetailsServiceImpl.loadUserByUsername
? ? ? ? ? ? authentication = authenticationManager
? ? ? ? ? ? ? ? ? ? .authenticate(new UsernamePasswordAuthenticationToken(username, password));
? ? ? ? }
1
2
3
4
5
6
7
8
在這里會(huì)從數(shù)據(jù)庫(kù)驗(yàn)證用戶是否合法。到這基本上就算完成了完整的驗(yàn)證流程。
? ? @Override
? ? public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
? ? {
? ? ? ? SysUser user = userService.selectUserByUserName(username);
? ? ? ? if (StringUtils.isNull(user))
? ? ? ? {
? ? ? ? ? ? log.info("登錄用戶:{} 不存在.", username);
? ? ? ? ? ? throw new UsernameNotFoundException("登錄用戶:" + username + " 不存在");
? ? ? ? }
? ? ? ? else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
? ? ? ? {
? ? ? ? ? ? log.info("登錄用戶:{} 已被刪除.", username);
? ? ? ? ? ? throw new BaseException("對(duì)不起,您的賬號(hào):" + username + " 已被刪除");
? ? ? ? }
? ? ? ? else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
? ? ? ? {
? ? ? ? ? ? log.info("登錄用戶:{} 已被停用.", username);
? ? ? ? ? ? throw new BaseException("對(duì)不起,您的賬號(hào):" + username + " 已停用");
? ? ? ? }
? ? ? ? return createLoginUser(user);
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2、JWT
在配置類中只定義了一個(gè)jwt過(guò)濾器。將這個(gè)過(guò)濾器添加到了UsernamePasswordAuthenticationFilter過(guò)濾器之前。這部分我感覺(jué)沒(méi)有特別難理解的部分了,主要就是一些業(yè)務(wù)邏輯。
@Override
? ? protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
? ? ? ? ? ? throws ServletException, IOException
? ? {
? ? ? ? LoginUser loginUser = tokenService.getLoginUser(request);
? ? ? ? if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
? ? ? ? {
? ? ? ? ? ? tokenService.verifyToken(loginUser);
? ? ? ? ? ? UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
? ? ? ? ? ? authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
? ? ? ? ? ? SecurityContextHolder.getContext().setAuthentication(authenticationToken);
? ? ? ? }
? ? ? ? chain.doFilter(request, response);
? ? }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
七、總結(jié)
以上都是個(gè)人對(duì)項(xiàng)目的理解,如有錯(cuò)誤還請(qǐng)指正。如果有其他問(wèn)題,請(qǐng)?jiān)谠u(píng)論區(qū)提出討論。如果這篇文章幫到了你,請(qǐng)點(diǎn)個(gè)贊鼓勵(lì)一下我這個(gè)菜鳥(niǎo)。
?
總結(jié)
以上是生活随笔為你收集整理的RuoYi后台系统权限管理解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: RuoYi-Vue————权限管理
- 下一篇: RuoYi(若依开源框架)-前后台分离版