SpringSecurity——基於Spring、SpringMVC和MyBatis自定義SpringSecurity權限認證規則


 

本文在SpringMVC和MyBatis項目框架的基礎上整合Spring Security作為權限管理。並且完全實現一套自定義的權限管理規則。


1.權限管理
在本例中所使用的權限管理的思路如下圖所示,在系統中存在着許多帳號,同時存在着許多資源,在一個Web系統中一個典型的資源就是訪問頁面的URL,控制了這個就能夠直接控制用戶的訪問權。

由於資源非常多,直接針對資源與用戶進行設置關系會比較繁瑣,因此針對同一類或者同一組的資源打個包,稱為一組權限,這樣將權限分配給用戶的時候,一組權限中的資源也就都分配給用戶了。

這個只是一個非常簡單的權限管理方案,並且只能適用於較小的項目,因為此處給出這個只是為了便於理解自定義的Spring Security認證規則。

2.Spring Security的認證規則
要編寫自定義的認證規則,首先需要對Spring Security中的認證規則有一定的了解,下面簡單介紹下Spring Security的認證規則。

1)在Spring Security中每個URL都是一個資源,當系統啟動的時候,Spring Security會根據配置將所有的URL與訪問這個URL所需要的權限的映射數據加載到Spring Security中。

2)當一個請求訪問一個資源時,Spring Security會判斷這個URL是否需要權限驗證,如果不需要,那么直接訪問即可。

3)如果這個URL需要進行權限驗證,那么Spring Security會檢查當前請求來源所屬用戶是否登錄,如果沒有登錄,則跳轉到登錄頁面,進行登錄操作,並加載這個用戶的相關信息

4)如果登錄,那么判斷這個用戶所擁有的權限是否包含訪問這個URL所需要的權限,如果有則允許訪問

5)如果沒有權限,那么就給出相應的提示信息

3.自定義認證規則思路
根據上面一小節介紹的Spring Security認證的過程,我們相應的就能夠分析出對於這個過程我們如果要修改的話,需要進行哪些方面的改動。

3.1.自定義SecurityMetadataSource
在Spring Security中的 SecurityMetadataSource 處於上面的步驟一中,也就是用於加載URL與權限對應關系的,對於這個我們需要自己進行定義

package com.oolong.customsecurity;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
import org.apache.log4j.LogManager;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
 
/**
 * 加載URL與權限資源,並提供根據URL匹配權限的方法
 * @author weilu2
 * @date 2016年12月17日 上午11:18:52
 *
 */
@Component
public class CustomSecurityMetadataSource 
        implements FilterInvocationSecurityMetadataSource {
 
    private Map<String, List<ConfigAttribute>> resources;
    
    public CustomSecurityMetadataSource() {
        loadAuthorityResources();
    }
    
    private void loadAuthorityResources() {
        // 此處在創建時從數據庫中初始化權限數據
        // 將權限與資源數據整理成 Map<resource, List<Authority>> 的形式
        // 注意:加載URL資源時,需要對資源進行排序,要由精確到粗略進行排序,讓精確的URL優先匹配
        resources = new HashMap<>();
        
        // 此處先偽造一些數據
        List<ConfigAttribute> authorityList = new ArrayList<>();
        ConfigAttribute auth = new SecurityConfig("AUTH_WELCOME");
        authorityList.add(auth);
        resources.put("/welcome", authorityList);
    }
    
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        
        String url = ((FilterInvocation) object).getRequestUrl();
        
        Set<String> keys = resources.keySet();
        
        for (String k : keys) {
            if (url.indexOf(k) >= 0) {
                return resources.get(k);
            }
        }
        return null;
    }
 
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        // TODO Auto-generated method stub
        return null;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

}

在這個類中,實現了FilterInvocationSecurityMetadataSource接口,這個接口中的 getAttributes(Object object)方法能夠根據請求的URL,獲取這個URL所需要的權限,那么我們就可以在這個類初始化的時候將所有需要的權限加載進來,然后根據我們的規則進行獲取,因此這里還需要編寫一個加載數據的方法 loadAuthorityResources(),並且在構造函數中調用。

此處加載資源為了簡化,只是隨意填充了一些數據,實際可以從數據庫中獲取。

3.2.自定義AccessDecisionManager
編寫自定義的決策管理器,決策管理器是Spring Security用來決定對於一個用戶的請求是否基於通過的中心控制。

package com.oolong.customsecurity;
 
import java.util.Collection;
import java.util.Iterator;
 
import org.apache.log4j.LogManager;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
 
/**
 * 進行決策,根據URL獲得訪問這個資源所需要的權限,然后在與當前用戶所擁有的權限進行對比
 * 如果當前用戶擁有相關權限,就直接返回,否則拋出 AccessDeniedException異常
 * @author weilu2
 * @date 2016年12月17日 上午11:30:40
 *
 */
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
 
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        
        LogManager.getLogger("CustomAccessDecisionManager").info("decide invoke");
        
        if (configAttributes == null) {
            return;
        }
        
        if (configAttributes.size() <= 0) {
            return;
        }
        
        Iterator<ConfigAttribute> authorities = configAttributes.iterator();
        String needAuthority = null;
        
        while(authorities.hasNext()) {
            ConfigAttribute authority = authorities.next();
            
            if (authority == null || (needAuthority = authority.getAttribute()) == null) {
                continue;
            }
 
            LogManager.getLogger("CustomAccessDecisionManager").info("decide == " + needAuthority);
            
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needAuthority.equals(ga.getAuthority().trim())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("No Authority");
    }
 
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

}

決策管理器最重要的就是這個 decide()方法,Spring Security會將當前登錄用戶信息包裝到一個 Authentication對象中,並傳入這個方法;並且調用 SecurityMetadataSource.getAttributes() 方法獲取這個URL相關的權限以參數 Collection<ConfigAttribute> 的形式傳入這個方法。

然后這個decide方法獲取到這兩個信息之后就可以進行對比決策了。如果當前用戶允許登錄,那么直接return即可。如果當前用戶不許運行登錄,則拋出一個 AccessDeniedException異常。


3.3.自定義 UserDetailsService 和 AuthenticationProvider

前面說過,要進行驗證,除了有URL與權限的映射關系,還需要有用戶的權限信息。要編寫自定義的用戶數據加載,就需要實現這兩個接口。

3.3.1.UserDetailsService

package com.oolong.customsecurity;
 
import java.util.ArrayList;
import java.util.List;
 
import org.apache.log4j.LogManager;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
 
import com.oolong.model.AccountInfoModel;
import com.oolong.model.AuthorityModel;
 
@Component
public class CustomUserDetailsService implements UserDetailsService {
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 
        LogManager.getLogger("CustomUserDetailsService").info("loadUserByUsername invoke");
 
        // 提供到數據庫查詢該用戶的權限信息
        // 關於角色和權限的轉換關系在此處處理,根據用戶與角色的關系、角色與權限的關系,
        // 將用戶與權限的管理整理出來
        
        // 此處偽造一些數據
        // 偽造權限
        AuthorityModel authority = new AuthorityModel("AUTH_WELCOME");
        List<AuthorityModel> authorities = new ArrayList<>();
        authorities.add(authority);
        
        AccountInfoModel account = new AccountInfoModel("oolong", "12345");
        account.setAuthorities(authorities);
        
        return account;
    }
}
 

 

3.3.2.AuthenticationProvider
AuthenticationProvider用於包裝UserDetailsService,並將其提供給 Spring Security使用。這個接口中最重要的是實現 retrieveUser() 方法,這個請參考接口的說明進行實現,此處不再贅述。

package com.oolong.customsecurity;
 
import org.apache.log4j.LogManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
 
/**
 * 這兩個方法用於添加額外的檢查功能,此處不需要添加,因此空着,直接實現這個抽象類即可。
 * @author weilu2
 * @date 2016年12月17日 下午12:20:27
 *
 */
@Component
public class CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
 
    @Autowired
    private UserDetailsService userDetailsService;
    
    public UserDetailsService getUserDetailService() {
        return this.userDetailsService;
    }
 
    public void setUserDetailService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
 
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        
    }
 
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
 
        LogManager.getLogger("CustomUserDetailsAuthenticationProvider").info("retrieveUser invoke");
        
        if (userDetailsService == null) {
            throw new AuthenticationServiceException("");
        }
        
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        
        if (userDetails == null) {
            throw new UsernameNotFoundException(username);
        }
        
        if (userDetails.getUsername().equals(authentication.getPrincipal().toString()) 
                && userDetails.getPassword().equals(authentication.getCredentials().toString())) {
            return userDetails;
        }
        
        throw new BadCredentialsException(username + authentication.getCredentials());
    }
}

 

 

3.4.UserDetails和GrantedAuthority
這兩個接口非常簡單,請參考源碼,此處不再贅述

4.配置
上面編寫的這些自定義的實現都有了,但是僅僅這樣是沒有用的,如何配置能夠讓它們起作用呢?

package com.oolong.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
 
import com.oolong.customsecurity.CustomAccessDecisionManager;
import com.oolong.customsecurity.CustomSecurityMetadataSource;
import com.oolong.customsecurity.CustomUserDetailsAuthenticationProvider;
import com.oolong.customsecurity.TempHook;
 
@Configuration
@ComponentScan(basePackageClasses={TempHook.class})
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private CustomUserDetailsAuthenticationProvider customAuthenticationProvider;
    
    @Autowired
    private CustomAccessDecisionManager customAccessDecisionManager;
    
    @Autowired
    private CustomSecurityMetadataSource customSecurityMetadataSource;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(customFilterSecurityInterceptor(), ExceptionTranslationFilter.class);
        http.formLogin();
    }
    
    @Bean
    public FilterSecurityInterceptor customFilterSecurityInterceptor() {
        FilterSecurityInterceptor fsi = new FilterSecurityInterceptor();
        fsi.setAccessDecisionManager(customAccessDecisionManager);
        fsi.setSecurityMetadataSource(customSecurityMetadataSource);
        
        return fsi;
    }
}

在Spring MVC中,Spring Security是通過過濾器發揮作用的,因此我們就愛那個決策管理器與數據加載放到一個過濾器中,然后將這個過濾器插入到系統的過濾器鏈中。

此外,我們向系統中提供了一個用於檢索用戶的 AuthenticationProvicer。

還有,別忘記了,告訴系統,如果用戶沒有權限應該怎么辦,http.formLogin(),告訴Spring Security要跳轉到表單登錄頁面。

參考

[1] 源碼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM