前後端分離模式下的許可權設計方案
前後端分離模式下,所有的互動場景都變成了資料,傳統業務系統中的許可權控制方案在前端已經不再適用,因此引發了我對許可權的重新思考與設計。
許可權控制到底控制的是什麼?
在理解許可權控制之前,需要明白兩個概念:資源和許可權。什麼是資源,對於一個系統來說,系統內部的所有資訊都可以理解為這個系統的資源。頁面是資源、資料是資源、按鈕是資源、圖片是資源、甚至頁面上一條分割線也可理解為是這個系統的資源。而許可權就是訪問某個資源所需要的標識。無論系統的許可權如何設計,在使用者登入時,都可以計算得出使用者所擁有的許可權標識集合,也就確定了該使用者能訪問哪些系統資源,這就是我理解的許可權控制的本質。於是我們可以得出:許可權控制是控制登入使用者對於系統資源的訪問。
前後端分離模式下,前後端在許可權控制中各自的職責是什麼?
在弄清前後端在許可權控制中各自的職責是什麼之前,需要理解前後端各自在系統中的職責。這個還是很好理解:
- 服務端:提供資料介面。
- 前端:路由控制、頁面渲染。
由於前端負責與使用者互動,使用者所能操作的資源入口都是由前端進行控制,那麼前端的許可權控制就包括:
- 前端路由的許可權控制,過濾非法請求,使用者只能訪問許可權範圍內的頁面資源。
- 頁面內元件的許可權控制,根據使用者的許可權控制頁面元件的渲染。包括各種按鈕、表格、分割線等。
隨著前端元件化的快速發展,使用者所看到的一切均可理解為元件,頁面是個大元件,其內部由各個小元件拼湊而來,那麼前端許可權控制最終落地到對元件的許可權控制。
<元件 permissionName='xxx' />
前端可以渲染出使用者許可權範圍內的各種系統資源,但是不能保證資料介面的安全性,某些比較喜歡折騰的使用者完全可以越過前端的頁面訪問我們系統的資料介面,那麼服務端的許可權控制最終落地到對介面的許可權驗證。
實現思路
引上文,系統的一切資源均可進行許可權控制,實際上也可以做到,但在我們實際的操作過程中,往往不需要細化到分割線那種程度。這裡以按鈕級許可權控制為例做實現說明,如果有更細粒度的許可權需求,此思路依然可行。
- 前端路由許可權控制。使用者登入時拿到使用者擁有的許可權標識集合,在前端儲存。路由變化時,進行許可權判斷,通過則渲染對應頁面元件,否則渲染403元件。示例程式碼:
let hasPermission = permission.check(current.permissionName); <div className={styles.content}> {hasPermission ? children : <Exception type={403}/>} </div>
- 封裝bird-button許可權按鈕元件,傳入按鈕所需許可權名,內部進行許可權判斷,通過則渲染按鈕。
<BirdButton permissionName={'sys'} type='primary'>測試按鈕</BirdButton>
- 服務端。服務端許可權驗證很好理解。使用攔截器驗證當前請求的許可權。程式碼示例:
public class SsoAuthorizeInterceptor extends HandlerInterceptorAdapter { @Autowired private TicketHandler ticketHandler; @Autowired private SsoAuthorizeManager authorizeManager; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) return false; HandlerMethod handlerMethod = (HandlerMethod) handler; SsoAuthorize authorize = handlerMethod.getMethodAnnotation(SsoAuthorize.class); if (authorize != null) { TicketInfo ticketInfo = ticketHandler.getTicket(request); if (ticketInfo == null) { throw new UnAuthorizedException("使用者資訊已失效."); } String[] requirePermissions = authorize.permissions(); if(requirePermissions.length==0)return true; boolean isCheckAll = authorize.isCheckAll(); UserPermissionChecker permissionChecker = authorizeManager.getUserPermissionChecker(); if(!permissionChecker.hasPermissions(ticketInfo.getUserId(),requirePermissions,isCheckAll)){ throw new ForbiddenException("使用者沒有當前操作的許可權."); } } return true; } }
原始碼地址
本部落格涉及到的前端許可權控制思路均已在https://github.com/liuxx001/bird-front專案中實現,專案中除了按鈕級許可權方案還提供了後臺業務系統開發中常用的資料元件,包括:
- 下拉選擇器:bird-selector。[檢視文件]
- 全自動資料表格:bird-grid。[檢視文件]
- 全自動樹表:bird-tree-grid。[檢視文件]
- 資料樹:bird-tree。[檢視文件]
- 全自動錶單:bird-form。[檢視文件]
- 許可權按鈕:bird-button。[檢視文件]
所有業務元件的理念均是結合服務端介面進行元件的封裝,兼顧靈活性的同時保證更優的業務開發速度。