摘要:實(shí)現(xiàn)用戶認(rèn)證本次,我們通過的授權(quán)機(jī)制,實(shí)現(xiàn)用戶授權(quán)。啟用注解默認(rèn)的是不進(jìn)行授權(quán)注解攔截的,添加注解以啟用注解的全局方法攔截。角色該角色對(duì)應(yīng)菜單示例用戶授權(quán)代碼體現(xiàn)授權(quán)思路遍歷當(dāng)前用戶的菜單,根據(jù)菜單中對(duì)應(yīng)的角色名進(jìn)行授權(quán)。
引言
上一次,使用Spring Security與Angular實(shí)現(xiàn)了用戶認(rèn)證。Spring Security and Angular 實(shí)現(xiàn)用戶認(rèn)證
本次,我們通過Spring Security的授權(quán)機(jī)制,實(shí)現(xiàn)用戶授權(quán)。
實(shí)現(xiàn)十分簡單,大家認(rèn)真聽,都能聽得懂。
實(shí)現(xiàn) 權(quán)限設(shè)計(jì)前臺(tái)實(shí)現(xiàn)了菜單的權(quán)限控制,但后臺(tái)接口還沒進(jìn)行保護(hù),只要用戶登錄成功,什么接口都可以調(diào)用。
我們希望實(shí)現(xiàn):用戶有什么菜單的權(quán)限,只能訪問后臺(tái)對(duì)應(yīng)該菜單的接口。
比如,用戶有計(jì)算機(jī)組管理的菜單,就可以訪問計(jì)算機(jī)組相關(guān)的增刪改查接口,但是其他的接口都不允許訪問。
Spring Security的設(shè)計(jì)依據(jù)Spring Security的設(shè)計(jì),用戶對(duì)應(yīng)角色,角色對(duì)應(yīng)后臺(tái)接口。這是沒什么問題的。
示例
某接口添加@Secured注解,內(nèi)部添加權(quán)限表達(dá)式。
@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這樣設(shè)計(jì)是沒有任何問題的,我們只需要簡簡單單的幾行代碼就能實(shí)現(xiàn)接口的授權(quán)管理。
但是卻不符合我們的要求。
我們要求,在我們的系統(tǒng)中,用戶對(duì)應(yīng)多角色。
但是我們的角色是要求可以進(jìn)行動(dòng)態(tài)配置的,今天有一個(gè)系統(tǒng)管理員的角色,明天可能又加一個(gè)教師的角色。
在用戶授權(quán)這方面,是可以實(shí)現(xiàn)動(dòng)態(tài)配置的,因?yàn)橛脩舻臋?quán)限列表是一個(gè)List,我可以從數(shù)據(jù)庫查當(dāng)前用戶的角色,然后add進(jìn)去。
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); }
但是在接口級(jí)別,就無法實(shí)現(xiàn)動(dòng)態(tài)配置了。大家想想,注解里,要求的參數(shù)必須是常量,就是我們想動(dòng)態(tài)配置,也實(shí)現(xiàn)不了啊?
@GetMapping @Secured("ROLE_ADMIN") public ListgetAll() { return hostService.getAll(); }
所以,我們總結(jié),因?yàn)樽⒔馀渲玫南拗疲栽?b>Spring Security中角色是靜態(tài)的。
重新設(shè)計(jì)我們的角色是動(dòng)態(tài)的,而Spring Security中的角色是靜態(tài)的,所以不能將我們的角色直接映射到Spring Security中的角色,要映射也得拿一個(gè)我們系統(tǒng)中靜態(tài)的對(duì)象與之對(duì)應(yīng)。
角色是動(dòng)態(tài)的,這個(gè)不行了。但是我們的菜單是靜態(tài)的啊。
功能模塊是我們開發(fā)的,菜單就這么固定的幾個(gè),用戶管理、角色管理、系統(tǒng)設(shè)置啥的,在我們開發(fā)期間就已經(jīng)固定下來了,我們是不是可以使用菜單結(jié)合Spring Security進(jìn)行授權(quán)呢?
認(rèn)真看這張圖,看懂了這張圖,你應(yīng)該就明白了我的設(shè)計(jì)思想。
角色是動(dòng)態(tài)的,我不用它授權(quán),我使用靜態(tài)的菜單進(jìn)行授權(quán)。
靜態(tài)的菜單對(duì)應(yīng)Spring Security中靜態(tài)的角色,角色再對(duì)應(yīng)后臺(tái)接口,如此設(shè)計(jì),就實(shí)現(xiàn)了我們的設(shè)想:用戶擁有哪個(gè)菜單的權(quán)限,就只擁有被該菜單調(diào)用的相應(yīng)接口權(quán)限。
編碼設(shè)計(jì)好了,一起來寫代碼吧。
授權(quán)注解選擇Spring Security中有多種授權(quán)注解,個(gè)人經(jīng)過對(duì)比之后選擇@Secured注解,因?yàn)槲矣X得這個(gè)注解配置項(xiàng)更容易被人理解。
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(); }
直接寫一個(gè)角色的字符串?dāng)?shù)組傳進(jìn)去即可。
@Secured("ROLE_ADMIN") // 需要擁有`ROLE_ADMIN`角色才可訪問 @Secured({"ROLE_ADMIN", "ROLE_TEACHER"}) // 用戶擁有`ROLE_ADMIN`、`ROLE_TEACHER`二者之一即可訪問
注意:這里的字符串一定是以ROLE_開頭,Spring Security才把它當(dāng)成角色的配置,否則無效。
啟用@Secured注解默認(rèn)的Spring Security是不進(jìn)行授權(quán)注解攔截的,添加注解@EnableGlobalMethodSecurity以啟用@Secured注解的全局方法攔截。
@EnableGlobalMethodSecurity(securedEnabled = true) // 啟用全局方法安全,采用@Secured方式菜單角色映射
在菜單中新建一個(gè)字段securityRoleName來聲明我們的系統(tǒng)菜單對(duì)應(yīng)著哪個(gè)Spring Security角色。
// 該菜單在Spring Security環(huán)境下的角色名稱 @Column(nullable = false) private String securityRoleName;
建一個(gè)類,用于存放所有Spring Security角色的配置信息,供全局調(diào)用。
這里不能用枚舉,@Secured注解中要求必須是String數(shù)組,如果是枚舉,需要通過YunzhiSecurityRoleEnum.ROLE_MAIN.name()格式獲取字符串信息,但很遺憾,注解中要求必須是常量。
還記得上次自定義HTTP狀態(tài)碼的時(shí)候,吃了枚舉類無法擴(kuò)展的虧,以后再也不用枚舉了。就算用枚舉,也會(huì)設(shè)計(jì)一個(gè)接口,枚舉實(shí)現(xiàn)該接口,不用枚舉聲明方法的參數(shù)類型,而使用接口聲明,方便擴(kuò)展。
package club.yunzhi.huasoft.security; /** * @author zhangxishuo on 2019-03-02 * Yunzhi Security 角色 * 該角色對(duì)應(yīng)菜單 */ 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)思路:遍歷當(dāng)前用戶的菜單,根據(jù)菜單中對(duì)應(yīng)的Security角色名進(jìn)行授權(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)菜單,進(jì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惰性加載引起的錯(cuò)誤,啟用事務(wù)防止Hibernate關(guān)閉Session,深層原理目前還在研究。
單元測試單元測試很簡單,供寫相同功能的人參考。
@Test public void authTest() throws Exception { logger.debug("獲取基礎(chǔ)菜單"); 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。五行代碼(不算注釋),一個(gè)注解。就解決了一直以來困擾我們的權(quán)限問題。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/11476.html
摘要:框架具有輕便,開源的優(yōu)點(diǎn),所以本譯見構(gòu)建用戶管理微服務(wù)五使用令牌和來實(shí)現(xiàn)身份驗(yàn)證往期譯見系列文章在賬號(hào)分享中持續(xù)連載,敬請(qǐng)查看在往期譯見系列的文章中,我們已經(jīng)建立了業(yè)務(wù)邏輯數(shù)據(jù)訪問層和前端控制器但是忽略了對(duì)身份進(jìn)行驗(yàn)證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護(hù)REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:給定一個(gè)作為方法參數(shù)傳遞的域?qū)ο髮?shí)例,確保類要綁定合適的權(quán)限。對(duì)或或檢查與驗(yàn)證。檢查是否在客戶端的權(quán)限范圍內(nèi)。匹配默認(rèn)的前綴字符串是的,如果匹配到則授權(quán),如授權(quán)范圍。實(shí)現(xiàn)類輪詢所有配置的每個(gè)配置,并且如果全部是肯定才能授予訪問權(quán)限。 03.01-源碼-Spring Security Oauth2 @(技術(shù)-架構(gòu))[源碼, 權(quán)限, Security, Oauth2] Oauth2 是一個(gè)...
閱讀 3904·2021-11-22 09:34
閱讀 1490·2021-11-04 16:10
閱讀 1721·2021-10-11 10:59
閱讀 3271·2019-08-30 15:44
閱讀 2036·2019-08-30 13:17
閱讀 3445·2019-08-30 11:05
閱讀 744·2019-08-29 14:02
閱讀 2618·2019-08-26 13:34