上一次,使用Spring Security
與Angular
實現了用戶認證。Spring Security and Angular 實現用戶認證java
本次,咱們經過Spring Security
的受權機制,實現用戶受權。web
實現十分簡單,你們認真聽,都能聽得懂。spring
前臺實現了菜單的權限控制,但後臺接口還沒進行保護,只要用戶登陸成功,什麼接口均可以調用。數據庫
咱們但願實現:用戶有什麼菜單的權限,只能訪問後臺對應該菜單的接口。json
好比,用戶有計算機組管理的菜單,就能夠訪問計算機組相關的增刪改查接口,可是其餘的接口都不容許訪問。segmentfault
Spring Security
的設計依據Spring Security
的設計,用戶對應角色,角色對應後臺接口。這是沒什麼問題的。數組
示例安全
某接口添加@Secured
註解,內部添加權限表達式。mvc
@GetMapping @Secured("ROLE_ADMIN") public List<Host> getAll() { return hostService.getAll(); }
而後再爲用戶建立Spring Security
中的角色。app
這裏咱們爲用戶添加ROLE_ADMIN
的角色受權,與getAll
方法上的@Secured("ROLE_ADMIN")
註解中的參數一致,表示該用戶有權限訪問該方法,這就是受權。
private UserDetails createUser(User user) { logger.debug("初始化受權列表"); List<SimpleGrantedAuthority> 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("初始化受權列表"); List<SimpleGrantedAuthority> 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); }
可是在接口級別,就沒法實現動態配置了。你們想一想,註解裏,要求的參數必須是常量,就是咱們想動態配置,也實現不了啊?
@GetMapping @Secured("ROLE_ADMIN") public List<Host> getAll() { 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<Host> getAll() { return hostService.getAll(); }
代碼體現受權思路:遍歷當前用戶的菜單,根據菜單中對應的Security
角色名進行受權。
private UserDetails createUser(User user) { logger.debug("獲取用戶的全部受權菜單"); Set<WebAppMenu> menus = webAppMenuService.getAllAuthMenuByUser(user); logger.debug("初始化受權列表"); List<SimpleGrantedAuthority> 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<Role> 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
。五行代碼(不算註釋),一個註解。就解決了一直以來困擾咱們的權限問題。