摘要:實現用戶認證本次,我們通過的授權機制,實現用戶授權。啟用注解默認的是不進行授權注解攔截的,添加注解以啟用注解的全局方法攔截。角色該角色對應菜單示例用戶授權代碼體現授權思路遍歷當前用戶的菜單,根據菜單中對應的角色名進行授權。
引言
上一次,使用Spring Security與Angular實現了用戶認證。Spring Security and Angular 實現用戶認證
本次,我們通過Spring Security的授權機制,實現用戶授權。
實現十分簡單,大家認真聽,都能聽得懂。
實現 權限設計前臺實現了菜單的權限控制,但后臺接口還沒進行保護,只要用戶登錄成功,什么接口都可以調用。
我們希望實現:用戶有什么菜單的權限,只能訪問后臺對應該菜單的接口。
比如,用戶有計算機組管理的菜單,就可以訪問計算機組相關的增刪改查接口,但是其他的接口都不允許訪問。
Spring Security的設計依據Spring Security的設計,用戶對應角色,角色對應后臺接口。這是沒什么問題的。
示例
某接口添加@Secured注解,內部添加權限表達式。
@GetMapping @Secured("ROLE_ADMIN") public ListgetAll() { return hostService.getAll(); }
然后再為用戶創建Spring Security中的角色。
這里我們為用戶添加ROLE_ADMIN的角色授權,與getAll方法上的@Secured("ROLE_ADMIN")注解中的參數一致,表示該用戶有權限訪問該方法,這就是授權。
private UserDetails createUser(User user) { logger.debug("初始化授權列表"); List不足authorities = new ArrayList<>(); logger.debug("角色授權"); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); logger.debug("構建用戶"); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); }
作為一款優秀的安全框架而言,Spring Security這樣設計是沒有任何問題的,我們只需要簡簡單單的幾行代碼就能實現接口的授權管理。
但是卻不符合我們的要求。
我們要求,在我們的系統中,用戶對應多角色。
但是我們的角色是要求可以進行動態配置的,今天有一個系統管理員的角色,明天可能又加一個教師的角色。
在用戶授權這方面,是可以實現動態配置的,因為用戶的權限列表是一個List,我可以從數據庫查當前用戶的角色,然后add進去。
private UserDetails createUser(User user) { logger.debug("初始化授權列表"); Listauthorities = new ArrayList<>(); logger.debug("角色授權"); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); logger.debug("構建用戶"); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); }
但是在接口級別,就無法實現動態配置了。大家想想,注解里,要求的參數必須是常量,就是我們想動態配置,也實現不了啊?
@GetMapping @Secured("ROLE_ADMIN") public ListgetAll() { return hostService.getAll(); }
所以,我們總結,因為注解配置的限制,所以在Spring Security中角色是靜態的。
重新設計我們的角色是動態的,而Spring Security中的角色是靜態的,所以不能將我們的角色直接映射到Spring Security中的角色,要映射也得拿一個我們系統中靜態的對象與之對應。
角色是動態的,這個不行了。但是我們的菜單是靜態的啊。
功能模塊是我們開發的,菜單就這么固定的幾個,用戶管理、角色管理、系統設置啥的,在我們開發期間就已經固定下來了,我們是不是可以使用菜單結合Spring Security進行授權呢?
認真看這張圖,看懂了這張圖,你應該就明白了我的設計思想。
角色是動態的,我不用它授權,我使用靜態的菜單進行授權。
靜態的菜單對應Spring Security中靜態的角色,角色再對應后臺接口,如此設計,就實現了我們的設想:用戶擁有哪個菜單的權限,就只擁有被該菜單調用的相應接口權限。
編碼設計好了,一起來寫代碼吧。
授權注解選擇Spring Security中有多種授權注解,個人經過對比之后選擇@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(); }
直接寫一個角色的字符串數組傳進去即可。
@Secured("ROLE_ADMIN") // 需要擁有`ROLE_ADMIN`角色才可訪問 @Secured({"ROLE_ADMIN", "ROLE_TEACHER"}) // 用戶擁有`ROLE_ADMIN`、`ROLE_TEACHER`二者之一即可訪問
注意:這里的字符串一定是以ROLE_開頭,Spring Security才把它當成角色的配置,否則無效。
啟用@Secured注解默認的Spring Security是不進行授權注解攔截的,添加注解@EnableGlobalMethodSecurity以啟用@Secured注解的全局方法攔截。
@EnableGlobalMethodSecurity(securedEnabled = true) // 啟用全局方法安全,采用@Secured方式菜單角色映射
在菜單中新建一個字段securityRoleName來聲明我們的系統菜單對應著哪個Spring Security角色。
// 該菜單在Spring Security環境下的角色名稱 @Column(nullable = false) private String securityRoleName;
建一個類,用于存放所有Spring Security角色的配置信息,供全局調用。
這里不能用枚舉,@Secured注解中要求必須是String數組,如果是枚舉,需要通過YunzhiSecurityRoleEnum.ROLE_MAIN.name()格式獲取字符串信息,但很遺憾,注解中要求必須是常量。
還記得上次自定義HTTP狀態碼的時候,吃了枚舉類無法擴展的虧,以后再也不用枚舉了。就算用枚舉,也會設計一個接口,枚舉實現該接口,不用枚舉聲明方法的參數類型,而使用接口聲明,方便擴展。
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用戶授權getAll() { return hostService.getAll(); }
代碼體現授權思路:遍歷當前用戶的菜單,根據菜單中對應的Security角色名進行授權。
private UserDetails createUser(User user) { logger.debug("獲取用戶的所有授權菜單"); Setmenus = webAppMenuService.getAllAuthMenuByUser(user); logger.debug("初始化授權列表"); List authorities = new ArrayList<>(); logger.debug("遍歷授權菜單,進行角色授權"); for (WebAppMenu menu : menus) { authorities.add(new SimpleGrantedAuthority(menu.getSecurityRoleName())); } logger.debug("構建用戶"); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); }
注:這里遇到了Hibernate惰性加載引起的錯誤,啟用事務防止Hibernate關閉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("構造角色"); List總結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("構造用戶"); 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("無授權用戶訪問host,斷言403"); this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL) .header(TOKEN_KEY, token)) .andExpect(status().isForbidden()); logger.debug("用戶授權Host菜單"); user.getRoleList().clear(); user.getRoleList().add(roleHost); userRepository.save(user); logger.debug("重新登錄, 重新授權"); token = this.loginWithUsernameAndPassword(username, password); logger.debug("授權Host用戶訪問,斷言200"); this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug("用戶授權Group菜單"); user.getRoleList().clear(); user.getRoleList().add(roleGroup); userRepository.save(user); logger.debug("重新登錄, 重新授權"); token = this.loginWithUsernameAndPassword(username, password); logger.debug("授權Group用戶訪問,斷言200"); this.mockMvc.perform(MockMvcRequestBuilders.get(HOST_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug("用戶授權Setting菜單"); user.getRoleList().clear(); user.getRoleList().add(roleSetting); userRepository.save(user); logger.debug("重新登錄, 重新授權"); token = this.loginWithUsernameAndPassword(username, password); logger.debug("授權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"); }
感謝開源社區,感謝Spring Security。五行代碼(不算注釋),一個注解。就解決了一直以來困擾我們的權限問題。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/11476.html
摘要:框架具有輕便,開源的優點,所以本譯見構建用戶管理微服務五使用令牌和來實現身份驗證往期譯見系列文章在賬號分享中持續連載,敬請查看在往期譯見系列的文章中,我們已經建立了業務邏輯數據訪問層和前端控制器但是忽略了對身份進行驗證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:給定一個作為方法參數傳遞的域對象實例,確保類要綁定合適的權限。對或或檢查與驗證。檢查是否在客戶端的權限范圍內。匹配默認的前綴字符串是的,如果匹配到則授權,如授權范圍。實現類輪詢所有配置的每個配置,并且如果全部是肯定才能授予訪問權限。 03.01-源碼-Spring Security Oauth2 @(技術-架構)[源碼, 權限, Security, Oauth2] Oauth2 是一個...
閱讀 3937·2021-11-22 09:34
閱讀 1506·2021-11-04 16:10
閱讀 1739·2021-10-11 10:59
閱讀 3285·2019-08-30 15:44
閱讀 2049·2019-08-30 13:17
閱讀 3459·2019-08-30 11:05
閱讀 755·2019-08-29 14:02
閱讀 2630·2019-08-26 13:34