Spring Security 實現用戶受權

引言

上一次,使用Spring SecurityAngular實現了用戶認證。Spring Security and Angular 實現用戶認證java

本次,咱們經過Spring Security的受權機制,實現用戶受權。web

實現十分簡單,你們認真聽,都能聽得懂。spring

實現

權限設計

前臺實現了菜單的權限控制,但後臺接口還沒進行保護,只要用戶登陸成功,什麼接口均可以調用。數據庫

咱們但願實現:用戶有什麼菜單的權限,只能訪問後臺對應該菜單的接口。json

好比,用戶有計算機組管理的菜單,就能夠訪問計算機組相關的增刪改查接口,可是其餘的接口都不容許訪問。segmentfault

Spring Security的設計

依據Spring Security的設計,用戶對應角色,角色對應後臺接口。這是沒什麼問題的。數組

clipboard.png

示例安全

某接口添加@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中的角色,要映射也得拿一個咱們系統中靜態的對象與之對應。

角色是動態的,這個不行了。可是咱們的菜單是靜態的啊。

clipboard.png

功能模塊是咱們開發的,菜單就這麼固定的幾個,用戶管理、角色管理、系統設置啥的,在咱們開發期間就已經固定下來了,咱們是否是可使用菜單結合Spring Security進行受權呢?

clipboard.png

認真看這張圖,看懂了這張圖,你應該就明白了個人設計思想。

角色是動態的,我不用它受權,我使用靜態的菜單進行受權。

靜態的菜單對應Spring Security中靜態的角色,角色再對應後臺接口,如此設計,就實現了咱們的設想:用戶擁有哪一個菜單的權限,就只擁有被該菜單調用的相應接口權限。

clipboard.png

編碼

設計好了,一塊兒來寫代碼吧。

受權註解選擇

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

五行代碼(不算註釋),一個註解。就解決了一直以來困擾咱們的權限問題。

相關文章
相關標籤/搜索