摘要:實現(xiàn)用戶認證本次,我們通過的授權(quán)機制,實現(xiàn)用戶授權(quán)。啟用注解默認的是不進行授權(quán)注解攔截的,添加注解以啟用注解的全局方法攔截。角色該角色對應菜單示例用戶授權(quán)代碼體現(xiàn)授權(quán)思路遍歷當前用戶的菜單,根據(jù)菜單中對應的角色名進行授權(quán)。
引言
上一次,使用Spring Security與Angular實現(xiàn)了用戶認證。Spring Security and Angular 實現(xiàn)用戶認證
本次,我們通過Spring Security的授權(quán)機制,實現(xiàn)用戶授權(quán)。
實現(xiàn)十分簡單,大家認真聽,都能聽得懂。
實現(xiàn) 權(quán)限設計前臺實現(xiàn)了菜單的權(quán)限控制,但后臺接口還沒進行保護,只要用戶登錄成功,什么接口都可以調(diào)用。
我們希望實現(xiàn):用戶有什么菜單的權(quán)限,只能訪問后臺對應該菜單的接口。
比如,用戶有計算機組管理的菜單,就可以訪問計算機組相關(guān)的增刪改查接口,但是其他的接口都不允許訪問。
Spring Security的設計依據(jù)Spring Security的設計,用戶對應角色,角色對應后臺接口。這是沒什么問題的。
示例
某接口添加@Secured注解,內(nèi)部添加權(quán)限表達式。
@GetMapping @Secured("ROLE_ADMIN") public ListgetAll() { return hostService.getAll(); }
然后再為用戶創(chuàng)建Spring Security中的角色。
這里我們?yōu)橛脩籼砑?b>ROLE_ADMIN的角色授權(quán),與getAll方法上的@Secured("ROLE_ADMIN")注解中的參數(shù)一致,表示該用戶有權(quán)限訪問該方法,這就是授權(quán)。
private UserDetails createUser(User user) { logger.debug("初始化授權(quán)列表"); List不足authorities = new ArrayList<>(); logger.debug("角色授權(quán)"); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); logger.debug("構(gòu)建用戶"); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); }
作為一款優(yōu)秀的安全框架而言,Spring Security這樣設計是沒有任何問題的,我們只需要簡簡單單的幾行代碼就能實現(xiàn)接口的授權(quán)管理。
但是卻不符合我們的要求。
我們要求,在我們的系統(tǒng)中,用戶對應多角色。
但是我們的角色是要求可以進行動態(tài)配置的,今天有一個系統(tǒng)管理員的角色,明天可能又加一個教師的角色。
在用戶授權(quán)這方面,是可以實現(xiàn)動態(tài)配置的,因為用戶的權(quán)限列表是一個List,我可以從數(shù)據(jù)庫查當前用戶的角色,然后add進去。
private UserDetails createUser(User user) { logger.debug("初始化授權(quán)列表"); Listauthorities = new ArrayList<>(); logger.debug("角色授權(quán)"); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); logger.debug("構(gòu)建用戶"); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); }
但是在接口級別,就無法實現(xiàn)動態(tài)配置了。大家想想,注解里,要求的參數(shù)必須是常量,就是我們想動態(tài)配置,也實現(xiàn)不了???
@GetMapping @Secured("ROLE_ADMIN") public ListgetAll() { return hostService.getAll(); }
所以,我們總結(jié),因為注解配置的限制,所以在Spring Security中角色是靜態(tài)的。
重新設計我們的角色是動態(tài)的,而Spring Security中的角色是靜態(tài)的,所以不能將我們的角色直接映射到Spring Security中的角色,要映射也得拿一個我們系統(tǒng)中靜態(tài)的對象與之對應。
角色是動態(tài)的,這個不行了。但是我們的菜單是靜態(tài)的啊。
功能模塊是我們開發(fā)的,菜單就這么固定的幾個,用戶管理、角色管理、系統(tǒng)設置啥的,在我們開發(fā)期間就已經(jīng)固定下來了,我們是不是可以使用菜單結(jié)合Spring Security進行授權(quán)呢?
認真看這張圖,看懂了這張圖,你應該就明白了我的設計思想。
角色是動態(tài)的,我不用它授權(quán),我使用靜態(tài)的菜單進行授權(quán)。
靜態(tài)的菜單對應Spring Security中靜態(tài)的角色,角色再對應后臺接口,如此設計,就實現(xiàn)了我們的設想:用戶擁有哪個菜單的權(quán)限,就只擁有被該菜單調(diào)用的相應接口權(quán)限。
編碼設計好了,一起來寫代碼吧。
授權(quán)注解選擇Spring Security中有多種授權(quán)注解,個人經(jīng)過對比之后選擇@Secured注解,因為我覺得這個注解配置項更容易被人理解。
public @interface Secured { /** * Returns the list of security configuration attributes (e.g. ROLE_USER, ROLE_ADMIN). * * @return String[] The secure method attributes */ public String[]value(); }
直接寫一個角色的字符串數(shù)組傳進去即可。
@Secured("ROLE_ADMIN") // 需要擁有`ROLE_ADMIN`角色才可訪問 @Secured({"ROLE_ADMIN", "ROLE_TEACHER"}) // 用戶擁有`ROLE_ADMIN`、`ROLE_TEACHER`二者之一即可訪問
注意:這里的字符串一定是以ROLE_開頭,Spring Security才把它當成角色的配置,否則無效。
啟用@Secured注解默認的Spring Security是不進行授權(quán)注解攔截的,添加注解@EnableGlobalMethodSecurity以啟用@Secured注解的全局方法攔截。
@EnableGlobalMethodSecurity(securedEnabled = true) // 啟用全局方法安全,采用@Secured方式菜單角色映射
在菜單中新建一個字段securityRoleName來聲明我們的系統(tǒng)菜單對應著哪個Spring Security角色。
// 該菜單在Spring Security環(huán)境下的角色名稱 @Column(nullable = false) private String securityRoleName;
建一個類,用于存放所有Spring Security角色的配置信息,供全局調(diào)用。
這里不能用枚舉,@Secured注解中要求必須是String數(shù)組,如果是枚舉,需要通過YunzhiSecurityRoleEnum.ROLE_MAIN.name()格式獲取字符串信息,但很遺憾,注解中要求必須是常量。
還記得上次自定義HTTP狀態(tài)碼的時候,吃了枚舉類無法擴展的虧,以后再也不用枚舉了。就算用枚舉,也會設計一個接口,枚舉實現(xiàn)該接口,不用枚舉聲明方法的參數(shù)類型,而使用接口聲明,方便擴展。
package club.yunzhi.huasoft.security; /** * @author zhangxishuo on 2019-03-02 * Yunzhi Security 角色 * 該角色對應菜單 */ public class YunzhiSecurityRole { public static final String ROLE_MAIN = "ROLE_MAIN"; public static final String ROLE_HOST = "ROLE_HOST"; public static final String ROLE_GROUP = "ROLE_GROUP"; public static final String ROLE_USER = "ROLE_USER"; public static final String ROLE_ROLE = "ROLE_ROLE"; public static final String ROLE_SETTING = "ROLE_SETTING"; }
示例
@GetMapping @Secured({YunzhiSecurityRole.ROLE_HOST, YunzhiSecurityRole.ROLE_GROUP}) public List用戶授權(quán)getAll() { return hostService.getAll(); }
代碼體現(xiàn)授權(quán)思路:遍歷當前用戶的菜單,根據(jù)菜單中對應的Security角色名進行授權(quán)。
private UserDetails createUser(User user) { logger.debug("獲取用戶的所有授權(quán)菜單"); Setmenus = webAppMenuService.getAllAuthMenuByUser(user); logger.debug("初始化授權(quán)列表"); List authorities = new ArrayList<>(); logger.debug("遍歷授權(quán)菜單,進行角色授權(quán)"); for (WebAppMenu menu : menus) { authorities.add(new SimpleGrantedAuthority(menu.getSecurityRoleName())); } logger.debug("構(gòu)建用戶"); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); }
注:這里遇到了Hibernate惰性加載引起的錯誤,啟用事務防止Hibernate關(guān)閉Session,深層原理目前還在研究。
單元測試單元測試很簡單,供寫相同功能的人參考。
@Test public void authTest() throws Exception { logger.debug("獲取基礎菜單"); WebAppMenu hostMenu = webAppMenuRepository.findByRoute("/host"); WebAppMenu groupMenu = webAppMenuRepository.findByRoute("/group"); WebAppMenu settingMenu = webAppMenuRepository.findByRoute("/setting"); logger.debug("構(gòu)造角色"); List總結(jié)roleList = new ArrayList<>(); Role roleHost = new Role(); roleHost.setWebAppMenuList(Collections.singletonList(hostMenu)); roleList.add(roleHost); Role roleGroup = new Role(); roleGroup.setWebAppMenuList(Collections.singletonList(groupMenu)); roleList.add(roleGroup); Role roleSetting = new Role(); roleSetting.setWebAppMenuList(Collections.singletonList(settingMenu)); roleList.add(roleSetting); logger.debug("保存角色"); roleRepository.saveAll(roleList); logger.debug("構(gòu)造用戶"); User user = userService.getOneUnSavedUser(); logger.debug("獲取用戶名和密碼"); String username = user.getUsername(); String password = user.getPassword(); logger.debug("保存用戶"); userRepository.save(user); logger.debug("用戶登錄"); String token = this.loginWithUsernameAndPassword(username, password); logger.debug("無授權(quán)用戶訪問host,斷言403"); this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL) .header(TOKEN_KEY, token)) .andExpect(status().isForbidden()); logger.debug("用戶授權(quán)Host菜單"); user.getRoleList().clear(); user.getRoleList().add(roleHost); userRepository.save(user); logger.debug("重新登錄, 重新授權(quán)"); token = this.loginWithUsernameAndPassword(username, password); logger.debug("授權(quán)Host用戶訪問,斷言200"); this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug("用戶授權(quán)Group菜單"); user.getRoleList().clear(); user.getRoleList().add(roleGroup); userRepository.save(user); logger.debug("重新登錄, 重新授權(quán)"); token = this.loginWithUsernameAndPassword(username, password); logger.debug("授權(quán)Group用戶訪問,斷言200"); this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug("用戶授權(quán)Setting菜單"); user.getRoleList().clear(); user.getRoleList().add(roleSetting); userRepository.save(user); logger.debug("重新登錄, 重新授權(quán)"); token = this.loginWithUsernameAndPassword(username, password); logger.debug("授權(quán)Setting用戶訪問,斷言403"); this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL) .header(TOKEN_KEY, token)) .andExpect(status().isForbidden()); } private String loginWithUsernameAndPassword(String username, String password) throws Exception { logger.debug("用戶登錄"); byte[] encodedBytes = Base64.encodeBase64((username + ":" + password).getBytes()); MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header("Authorization", "Basic " + new String(encodedBytes))) .andExpect(status().isOk()) .andReturn(); logger.debug("從返回體中獲取token"); String json = mvcResult.getResponse().getContentAsString(); JSONObject jsonObject = JSON.parseObject(json); return jsonObject.getString("token"); }
感謝開源社區(qū),感謝Spring Security。五行代碼(不算注釋),一個注解。就解決了一直以來困擾我們的權(quán)限問題。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/11476.html
摘要:框架具有輕便,開源的優(yōu)點,所以本譯見構(gòu)建用戶管理微服務五使用令牌和來實現(xiàn)身份驗證往期譯見系列文章在賬號分享中持續(xù)連載,敬請查看在往期譯見系列的文章中,我們已經(jīng)建立了業(yè)務邏輯數(shù)據(jù)訪問層和前端控制器但是忽略了對身份進行驗證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:給定一個作為方法參數(shù)傳遞的域?qū)ο髮嵗_保類要綁定合適的權(quán)限。對或或檢查與驗證。檢查是否在客戶端的權(quán)限范圍內(nèi)。匹配默認的前綴字符串是的,如果匹配到則授權(quán),如授權(quán)范圍。實現(xiàn)類輪詢所有配置的每個配置,并且如果全部是肯定才能授予訪問權(quán)限。 03.01-源碼-Spring Security Oauth2 @(技術(shù)-架構(gòu))[源碼, 權(quán)限, Security, Oauth2] Oauth2 是一個...
閱讀 4221·2021-11-22 09:34
閱讀 1650·2021-11-04 16:10
閱讀 1821·2021-10-11 10:59
閱讀 3347·2019-08-30 15:44
閱讀 2127·2019-08-30 13:17
閱讀 3565·2019-08-30 11:05
閱讀 821·2019-08-29 14:02
閱讀 2697·2019-08-26 13:34