認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(四)

引言: 本文系《認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)》系列的完結(jié)篇盆顾,前面三篇已經(jīng)將認(rèn)證鑒權(quán)與API權(quán)限控制的流程和主要細(xì)節(jié)講解完孩饼。本文比較長消请,對這個(gè)系列進(jìn)行收尾汹桦,主要內(nèi)容包括對授權(quán)和鑒權(quán)流程之外的endpoint以及Spring Security過濾器部分踩坑的經(jīng)歷朽合。歡迎閱讀本系列文章。

1. 前文回顧

首先還是照例對前文進(jìn)行回顧扔水。在第一篇 認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(一)介紹了該項(xiàng)目的背景以及技術(shù)調(diào)研與最后選型痛侍。第二篇認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(二)畫出了簡要的登錄和校驗(yàn)的流程圖,并重點(diǎn)講解了用戶身份的認(rèn)證與token發(fā)放的具體實(shí)現(xiàn)魔市。第三篇認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(三)先介紹了資源服務(wù)器配置主届,以及其中涉及的配置類,后面重點(diǎn)講解了token以及API級(jí)別的鑒權(quán)待德。

本文將會(huì)講解剩余的兩個(gè)內(nèi)置端點(diǎn):注銷和刷新token君丁。注銷token端點(diǎn)的處理與Spring Security默認(rèn)提供的有些'/logout'有些區(qū)別,不僅清空SpringSecurityContextHolder中的信息将宪,還要增加對存儲(chǔ)token的清空绘闷。另一個(gè)刷新token端點(diǎn)其實(shí)和之前的請求授權(quán)是一樣的API橡庞,只是參數(shù)中的grant_type不一樣。

除了以上兩個(gè)內(nèi)置端點(diǎn)簸喂,后面將會(huì)重點(diǎn)講下幾種Spring Security過濾器毙死。API級(jí)別的操作權(quán)限校驗(yàn)本來設(shè)想是通過Spring Security的過濾器實(shí)現(xiàn)燎潮,特地把這邊學(xué)習(xí)了一遍喻鳄,踩了一遍坑。

最后是本系列的總結(jié)确封,并對于存在的不足和后續(xù)工作進(jìn)行論述除呵。

2. 其他端點(diǎn)

2.1 注銷端點(diǎn)

在第一篇中提到了Auth系統(tǒng)內(nèi)置的注銷端點(diǎn) /logout,如果還記得第三篇資源服務(wù)器的配置爪喘,下面的關(guān)于/logout配置一定不陌生颜曾。

            //...
                .and().logout()
                .logoutUrl("/logout")
                .clearAuthentication(true)
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .addLogoutHandler(customLogoutHandler());

上面配置的主要作用是:

  • 設(shè)置注銷的URL
  • 清空Authentication信息
  • 設(shè)置注銷成功的處理方式
  • 設(shè)置自定義的注銷處理方式

當(dāng)然在LogoutConfigurer中還有更多的設(shè)置選項(xiàng),筆者此處列出項(xiàng)目所需要的配置項(xiàng)秉剑。這些配置項(xiàng)圍繞著LogoutFilter過濾器泛豪。順帶講一下Spring Security的過濾器。其使用了springSecurityFillterChian作為了安全過濾的入口侦鹏,各種過濾器按順序具體如下:

  • SecurityContextPersistenceFilter:與SecurityContext安全上下文信息有關(guān)
  • HeaderWriterFilter:給http響應(yīng)添加一些Header
  • CsrfFilter:防止csrf攻擊诡曙,默認(rèn)開啟
  • LogoutFilter:處理注銷的過濾器
  • UsernamePasswordAuthenticationFilter:表單認(rèn)證過濾器
  • RequestCacheAwareFilter:緩存request請求
  • SecurityContextHolderAwareRequestFilter:此過濾器對ServletRequest進(jìn)行了一次包裝,使得request具有更加豐富的API
  • AnonymousAuthenticationFilter:匿名身份過濾器
  • SessionManagementFilter:session相關(guān)的過濾器略水,常用來防止session-fixation protection attack价卤,以及限制同一用戶開啟多個(gè)會(huì)話的數(shù)量
  • ExceptionTranslationFilter:異常處理過濾器
  • FilterSecurityInterceptor:web應(yīng)用安全的關(guān)鍵Filter

各種過濾器簡單標(biāo)注了作用,在下一節(jié)重點(diǎn)講其中的幾個(gè)過濾器渊涝。注銷過濾器排在靠前的位置慎璧,我們一起看下LogoutFilter的UML類圖。

logoutFilter
logoutFilter

類圖和我們之前配置時(shí)的思路是一致的跨释,HttpSecurity創(chuàng)建了LogoutConfigurer胸私,我們在這邊配置了LogoutConfigurer的一些屬性。同時(shí)LogoutConfigurer根據(jù)這些屬性創(chuàng)建了LogoutFilter鳖谈。

LogoutConfigurer的配置盖文,第一和第二點(diǎn)就不用再詳細(xì)解釋了,一個(gè)是設(shè)置端點(diǎn)蚯姆,另一個(gè)是清空認(rèn)證信息五续。
對于第三點(diǎn),配置注銷成功的處理方式龄恋。由于項(xiàng)目是前后端分離疙驾,客戶端只需要知道執(zhí)行成功該API接口的狀態(tài),并不用返回具體的頁面或者繼續(xù)向下傳遞請求郭毕。因此它碎,這邊配置了默認(rèn)的HttpStatusReturningLogoutSuccessHandler,成功直接返回狀態(tài)碼200。
對于第四點(diǎn)配置扳肛,自定義注銷處理的方法傻挂。這邊需要借助TokenStore,對token進(jìn)行操作挖息。TokenStore在之前文章的配置中已經(jīng)講過金拒,使用的是JdbcTokenStore。首先校驗(yàn)請求的合法性套腹,如果合法則對其進(jìn)行操作绪抛,先后移除refreshTokenexistingAccessToken

public class CustomLogoutHandler implements LogoutHandler {

    //...

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        //確定注入了tokenStore
        Assert.notNull(tokenStore, "tokenStore must be set");
       //獲取頭部的認(rèn)證信息
        String token = request.getHeader("Authorization");
        Assert.hasText(token, "token must be set");
        //校驗(yàn)token是否符合JwtBearer格式
        if (isJwtBearerToken(token)) {
            token = token.substring(6);
            OAuth2AccessToken existingAccessToken = tokenStore.readAccessToken(token);
            OAuth2RefreshToken refreshToken;
            if (existingAccessToken != null) {
                if (existingAccessToken.getRefreshToken() != null) {
                    LOGGER.info("remove refreshToken!", existingAccessToken.getRefreshToken());
                    refreshToken = existingAccessToken.getRefreshToken();
                    tokenStore.removeRefreshToken(refreshToken);
                }
                LOGGER.info("remove existingAccessToken!", existingAccessToken);
                tokenStore.removeAccessToken(existingAccessToken);
            }
            return;
        } else {
            throw new BadClientCredentialsException();
        }

    }

    //...
}

執(zhí)行如下請求:

method: get
url: http://localhost:9000/logout
header:
{
    Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=
}

注銷成功則會(huì)返回200电禀,將token和SecurityContextHolder進(jìn)行清空幢码。

2.2 刷新端點(diǎn)

在第一篇就已經(jīng)講過,由于token的時(shí)效一般不會(huì)很長尖飞,而refresh_ token一般周期會(huì)很長症副,為了不影響用戶的體驗(yàn),可以使用refresh_ token去動(dòng)態(tài)的刷新token政基。刷新token主要與RefreshTokenGranter有關(guān)贞铣,CompositeTokenGranter管理一個(gè)List列表,每一種grantType對應(yīng)一個(gè)具體的真正授權(quán)者腋么,refresh_ token對應(yīng)的granter就是RefreshTokenGranter咕娄,而granter內(nèi)部則是通過grantType來區(qū)分是否是各自的授權(quán)類型。執(zhí)行如下請求:

method: post 
url: http://localhost:12000/oauth/token?grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZXJfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZhLTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5NjU1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoiZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb4tTux6OQWouQJ2nn1DeE
header:
{
    Authorization: Basic ZnJvbnRlbmQ6ZnJvbnRlbmQ=
}

在refresh_ token正確的情況下珊擂,其返回的response和/oauth/token得到正常的響應(yīng)是一樣的圣勒。具體的代碼可以參閱第二篇的講解。

3. Spring Security過濾器

在上一節(jié)我們介紹了內(nèi)置的兩個(gè)端點(diǎn)的實(shí)現(xiàn)細(xì)節(jié)摧扇,還提到了HttpSecurity過濾器圣贸,因?yàn)樽N端點(diǎn)的實(shí)現(xiàn)就是通過過濾器的作用。核心的過濾器主要有:

  • FilterSecurityInterceptor
  • UsernamePasswordAuthenticationFilter
  • SecurityContextPersistenceFilter
  • ExceptionTranslationFilter

這一節(jié)將重點(diǎn)介紹其中的UsernamePasswordAuthenticationFilterFilterSecurityInterceptor扛稽。

3.1 UsernamePasswordAuthenticationFilter

筆者在剛開始看關(guān)于過濾器的文章吁峻,對于UsernamePasswordAuthenticationFilter有不少的文章介紹。如果只是引入Spring-Security在张,必然會(huì)與/login端點(diǎn)熟悉用含。SpringSecurity強(qiáng)制要求我們的表單登錄頁面必須是以POST方式向/login URL提交請求,而且要求用戶名和密碼的參數(shù)名必須是username和password帮匾。如果不符合啄骇,則不能正常工作。原因在于瘟斜,當(dāng)我們調(diào)用了HttpSecurity對象的formLogin方法時(shí)缸夹,其最終會(huì)給我們注冊一個(gè)過濾器UsernamePasswordAuthenticationFilter痪寻。看一下該過濾器的源碼虽惭。

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    //用戶名橡类、密碼
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

    //post請求/login
    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    //實(shí)現(xiàn)抽象類AbstractAuthenticationProcessingFilter的抽象方法,嘗試驗(yàn)證
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);
        
        //···

        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        //···
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
        implements ApplicationEventPublisherAware, MessageSourceAware {
    //...
    
    //調(diào)用requiresAuthentication芽唇,判斷請求是否需要authentication顾画,如果需要?jiǎng)t調(diào)用attemptAuthentication
    //有三種結(jié)果可能返回:
    //1.Authentication對象
    //2. AuthenticationException
    //3. Authentication對象為空
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        //不需要校驗(yàn),繼續(xù)傳遞
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        Authentication authResult;

        try {
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed authentication
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        //...
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        successfulAuthentication(request, response, chain, authResult);
    }

    //實(shí)際執(zhí)行的authentication披摄,繼承類必須實(shí)現(xiàn)該抽象方法
    public abstract Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException, IOException,
            ServletException;
    //成功authentication的默認(rèn)行為
    protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        //...
    }
    //失敗authentication的默認(rèn)行為
    protected void unsuccessfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
    //...           
    }

    ...
    //設(shè)置AuthenticationManager
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }
    ...
}

UsernamePasswordAuthenticationFilter因?yàn)槔^承了AbstractAuthenticationProcessingFilter才擁有過濾器的功能亲雪。AbstractAuthenticationProcessingFilter要求設(shè)置一個(gè)authenticationManager勇凭,authenticationManager的實(shí)現(xiàn)類將實(shí)際處理請求的認(rèn)證疚膊。AbstractAuthenticationProcessingFilter將攔截符合過濾規(guī)則的request,并試圖執(zhí)行認(rèn)證虾标。子類必須實(shí)現(xiàn) attemptAuthentication 方法寓盗,這個(gè)方法執(zhí)行具體的認(rèn)證。
認(rèn)證之后的處理和上注銷的差不多璧函。如果認(rèn)證成功傀蚌,將會(huì)把返回的Authentication對象存放在SecurityContext,并調(diào)用SuccessHandler蘸吓,也可以設(shè)置指定的URL和指定自定義的處SuccessHandler善炫。如果認(rèn)證失敗,默認(rèn)會(huì)返回401代碼給客戶端库继,也可以設(shè)置URL箩艺,指定自定義的處理FailureHandler。

基于UsernamePasswordAuthenticationFilter自定義的AuthenticationFilte還是挺多案例的宪萄,這邊推薦一篇博文Spring Security(五)--動(dòng)手實(shí)現(xiàn)一個(gè)IP_Login艺谆,寫得比較詳細(xì)。

3.2 FilterSecurityInterceptor

FilterSecurityInterceptor是filterchain中比較復(fù)雜拜英,也是比較核心的過濾器静汤,主要負(fù)責(zé)web應(yīng)用安全授權(quán)的工作。首先看下對于自定義的FilterSecurityInterceptor配置居凶。

    @Override
    public void configure(HttpSecurity http) throws Exception {
        
        ...
        //添加CustomSecurityFilter虫给,過濾器的順序放在FilterSecurityInterceptor
        http.antMatcher("/oauth/check_token").addFilterAt(customSecurityFilter(), FilterSecurityInterceptor.class);

    }
    //提供實(shí)例化的自定義過濾器
    @Bean
    public CustomSecurityFilter customSecurityFilter() {
        return new CustomSecurityFilter();
    }

從上述配置可以看到,在FilterSecurityInterceptor的位置注冊了CustomSecurityFilter侠碧,對于匹配到/oauth/check_token抹估,則會(huì)調(diào)用該進(jìn)入該過濾器。下圖為FilterSecurityInterceptor的類圖舆床,在其中還添加了CustomSecurityFilter和相關(guān)實(shí)現(xiàn)的接口的類棋蚌,方便讀者對比著看嫁佳。

FilterSecurityInterceptor
FilterSecurityInterceptor

CustomSecurityFilter是模仿FilterSecurityInterceptor實(shí)現(xiàn),繼承AbstractSecurityInterceptor和實(shí)現(xiàn)Filter接口谷暮。整個(gè)過程需要依賴AuthenticationManager蒿往、AccessDecisionManagerFilterInvocationSecurityMetadataSource
AuthenticationManager是認(rèn)證管理器湿弦,實(shí)現(xiàn)用戶認(rèn)證的入口瓤漏;AccessDecisionManager是訪問決策器,決定某個(gè)用戶具有的角色颊埃,是否有足夠的權(quán)限去訪問某個(gè)資源蔬充;FilterInvocationSecurityMetadataSource是資源源數(shù)據(jù)定義,即定義某一資源可以被哪些角色訪問班利。
從上面的類圖中可以看到自定義的CustomSecurityFilter同時(shí)又實(shí)現(xiàn)了
AccessDecisionManagerFilterInvocationSecurityMetadataSource饥漫。分別為SecureResourceFilterInvocationDefinitionSourceSecurityAccessDecisionManager。下面分析下主要的配置罗标。

//通過一個(gè)實(shí)現(xiàn)的filter庸队,對HTTP資源進(jìn)行安全處理
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    //被filter chain真實(shí)調(diào)用的方法,通過invoke代理
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    //代理的方法
    public void invoke(FilterInvocation fi) throws IOException, ServletException    {
        //...省略
    }
}

上述代碼是FilterSecurityInterceptor中的實(shí)現(xiàn)闯割,具體實(shí)現(xiàn)細(xì)節(jié)就沒列出了彻消,我們這邊重點(diǎn)在于對自定義的實(shí)現(xiàn)進(jìn)行講解。

public class CustomSecurityFilter extends AbstractSecurityInterceptor implements Filter {
   
    @Autowired
    SecureResourceFilterInvocationDefinitionSource invocationSource;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private SecurityAccessDecisionManager decisionManager;

    //設(shè)置父類中的屬性
    @PostConstruct
    public void init() {
        super.setAccessDecisionManager(decisionManager);
        super.setAuthenticationManager(authenticationManager);
    }
    //主要的過濾方法宙拉,與原來的一致
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //logger.info("doFilter in Security ");
        //構(gòu)造一個(gè)FilterInvocation宾尚,封裝request, response, chain
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //beforeInvocation會(huì)調(diào)用SecureResourceDataSource中的邏輯,類似于aop中的before 
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            //執(zhí)行下一個(gè)攔截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());            
        } finally {
            //完成后續(xù)工作谢澈,類似于aop中的after 
            super.afterInvocation(token, null);
        }
    }
    
    //...
    
    //資源源數(shù)據(jù)定義煌贴,設(shè)置為自定義的SecureResourceFilterInvocationDefinitionSource
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return invocationSource;
    }
}

上面自定義的CustomSecurityFilter,與我們之前的講解是一樣的流程澳化。主要依賴的三個(gè)接口都有在實(shí)現(xiàn)中實(shí)例化注入崔步。看下父類的beforeInvocation方法缎谷,其中省略了一些不重要的代碼片段井濒。

protected InterceptorStatusToken beforeInvocation(Object object) {  
    //根據(jù)SecurityMetadataSource獲取配置的權(quán)限屬性  
    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);  
    //...  
    //判斷是否需要對認(rèn)證實(shí)體重新認(rèn)證,默認(rèn)為否  
    Authentication authenticated = authenticateIfRequired();  
  
    // Attempt authorization  
    try {  
        //決策管理器開始決定是否授權(quán)列林,如果授權(quán)失敗,直接拋出AccessDeniedException  
        this.accessDecisionManager.decide(authenticated, object, attributes);  
    }  
    catch (AccessDeniedException accessDeniedException) {  
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,  
                accessDeniedException));  
  
        throw accessDeniedException;  
    }  
}  

上面代碼可以看出希痴,第一步是根據(jù)SecurityMetadataSource獲取配置的權(quán)限屬性者甲,accessDecisionManager會(huì)用到權(quán)限列表信息。然后判斷是否需要對認(rèn)證實(shí)體重新認(rèn)證砌创,默認(rèn)為否虏缸。第二步是接著決策管理器開始決定是否授權(quán)鲫懒,如果授權(quán)失敗,直接拋出AccessDeniedException刽辙。

(1). 獲取配置的權(quán)限屬性

public class SecureResourceFilterInvocationDefinitionSource implements FilterInvocationSecurityMetadataSource, InitializingBean {
    private PathMatcher matcher;
    //map保存配置的URL對應(yīng)的權(quán)限集
    private static Map<String, Collection<ConfigAttribute>> map = new HashMap<>();

    //根據(jù)傳入的對象URL進(jìn)行循環(huán)
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        logger.info("getAttributes");
        //應(yīng)該做instanceof
        FilterInvocation filterInvocation = (FilterInvocation) o;
        //String method = filterInvocation.getHttpRequest().getMethod();
        String requestURI = filterInvocation.getRequestUrl();
        //循環(huán)資源路徑窥岩,當(dāng)訪問的Url和資源路徑url匹配時(shí),返回該Url所需要的權(quán)限
        for (Iterator<Map.Entry<String, Collection<ConfigAttribute>>> iterator = map.entrySet().iterator(); iter.hasNext(); ) {
            Map.Entry<String, Collection<ConfigAttribute>> entry = iterator.next();
            String url = entry.getKey();

            if (matcher.match(url, requestURI)) {
                return map.get(requestURI);
            }
        }
        return null;
    }
    
    //... 
    
    //設(shè)置權(quán)限集宰缤,即上述的map
    @Override
    public void afterPropertiesSet() throws Exception {
        logger.info("afterPropertiesSet");
        //用來匹配訪問資源路徑
        this.matcher = new AntPathMatcher();
        //可以有多個(gè)權(quán)限
        Collection<ConfigAttribute> atts = new ArrayList<>();
        ConfigAttribute c1 = new SecurityConfig("ROLE_ADMIN");
        atts.add(c1);
        map.put("/oauth/check_token", atts);
    }
}

上面是getAttributes()實(shí)現(xiàn)的具體細(xì)節(jié)颂翼,將請求的URL取出進(jìn)行匹配事先設(shè)定的受限資源,最后返回需要的權(quán)限慨灭、角色朦乏。系統(tǒng)在啟動(dòng)的時(shí)候就會(huì)讀取到配置的map集合,對于攔截到請求進(jìn)行匹配氧骤。代碼中注釋比較詳細(xì)呻疹,這邊不多說。

(2). 決策管理器

public class SecurityAccessDecisionManager implements AccessDecisionManager {
    //...
    
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        logger.info("decide url and permission");
        //集合為空
        if (collection == null) {
            return;
        }

        Iterator<ConfigAttribute> ite = collection.iterator();
        //判斷用戶所擁有的權(quán)限语淘,是否符合對應(yīng)的Url權(quán)限诲宇,如果實(shí)現(xiàn)了UserDetailsService际歼,則用戶權(quán)限是loadUserByUsername返回用戶所對應(yīng)的權(quán)限
        while (ite.hasNext()) {
            ConfigAttribute ca = ite.next();
            String needRole = ca.getAttribute();
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                logger.info("GrantedAuthority: {}", ga);
                if (needRole.equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        logger.error("AccessDecisionManager: no right!");
        throw new AccessDeniedException("no right!");
    }
    
    //...
}

上面的代碼是決策管理器的實(shí)現(xiàn)惶翻,其邏輯也比較簡單,將請求所具有的權(quán)限與設(shè)定的受限資源所需的進(jìn)行匹配鹅心,如果具有則返回吕粗,否則拋出沒有正確的權(quán)限異常。默認(rèn)提供的決策管理器有三種旭愧,分別為AffirmativeBased颅筋、ConsensusBased、UnanimousBased输枯,篇幅有限议泵,我們這邊不再擴(kuò)展了。

補(bǔ)充一下桃熄,所具有的權(quán)限是通過之前配置的認(rèn)證方式先口,有password認(rèn)證和client認(rèn)證兩種。我們之前在授權(quán)服務(wù)器中配置了withClientDetails瞳收,所以用frontend身份驗(yàn)證獲得的權(quán)限是我們預(yù)先配置在數(shù)據(jù)庫中的authorities碉京。

4. 總結(jié)

Auth系統(tǒng)主要功能是授權(quán)認(rèn)證和鑒權(quán)。項(xiàng)目微服務(wù)化后螟深,原有的單體應(yīng)用基于HttpSession認(rèn)證鑒權(quán)不能滿足微服務(wù)架構(gòu)下的需求谐宙。每個(gè)微服務(wù)都需要對訪問進(jìn)行鑒權(quán),每個(gè)微應(yīng)用都需要明確當(dāng)前訪問用戶以及其權(quán)限界弧,尤其當(dāng)有多個(gè)客戶端凡蜻,包括web端搭综、移動(dòng)端等等,單體應(yīng)用架構(gòu)下的鑒權(quán)方式就不是特別合適了划栓。權(quán)限服務(wù)作為基礎(chǔ)的公共服務(wù)设凹,也需要微服務(wù)化。

筆者的設(shè)計(jì)中茅姜,Auth服務(wù)一方面進(jìn)行授權(quán)認(rèn)證闪朱,另一方面是基于token進(jìn)行身份合法性和API級(jí)別的權(quán)限校驗(yàn)。對于某個(gè)服務(wù)的請求钻洒,經(jīng)過網(wǎng)關(guān)會(huì)調(diào)用Auth服務(wù)奋姿,對token合法性進(jìn)行驗(yàn)證。同時(shí)筆者根據(jù)當(dāng)前項(xiàng)目的整體情況素标,存在部分遺留服務(wù)称诗,這些遺留服務(wù)又沒有足夠的時(shí)間和人力立馬進(jìn)行微服務(wù)改造,而且還需要繼續(xù)運(yùn)行头遭。為了適配當(dāng)前新的架構(gòu)寓免,采取的方案就是對這些遺留服務(wù)的操作API,在Auth服務(wù)進(jìn)行API級(jí)別的操作權(quán)限鑒定计维。API級(jí)別的操作權(quán)限校驗(yàn)需要的上下文信息需要結(jié)合業(yè)務(wù)袜香,與客戶端進(jìn)行商定,應(yīng)該在token能取到相應(yīng)信息鲫惶,傳遞給Auth服務(wù)蜈首,不過應(yīng)盡量減少在header取上下文校驗(yàn)的信息。

筆者將本次開發(fā)Auth系統(tǒng)所涉及的大部分代碼及源碼進(jìn)行了解析欠母,至于沒有講到的一些內(nèi)容和細(xì)節(jié)欢策,讀者可以自行擴(kuò)展。

5. 不足與后續(xù)工作

5.1 存在的不足

  • API級(jí)別操作權(quán)限校驗(yàn)的通用性

    (1). 對于API級(jí)別操作權(quán)限校驗(yàn)赏淌,需要在網(wǎng)關(guān)處調(diào)用時(shí)構(gòu)造相應(yīng)的上下文信息踩寇。上下文信息基本依賴于 token中的payload,如果信息太多引起token太長六水,導(dǎo)致每次客戶端的請求頭部長度變長俺孙。

    (2). 并不是所有的操作接口都能覆蓋到,這個(gè)問題是比較嚴(yán)重的缩擂,根據(jù)上下文集合很可能出現(xiàn)好多接口 的權(quán)限沒法鑒定鼠冕,最后的結(jié)果就是API級(jí)別操作權(quán)限校驗(yàn)失敗的是絕對沒有權(quán)限訪問該接口,而通過不一定能訪問胯盯,因?yàn)樵摻涌谏婕暗降纳舷挛母緵]法完全得到懈费。我們的項(xiàng)目在現(xiàn)階段,定義的最小上下文集合能勉強(qiáng)覆蓋到博脑,但是對于后面擴(kuò)增的服務(wù)接口真的是不樂觀憎乙。

    (3). 每個(gè)服務(wù)的每個(gè)接口都在Auth服務(wù)注冊其所需要的權(quán)限票罐,太過麻煩,Auth服務(wù)需要額外維護(hù)這樣的信息泞边。

  • 網(wǎng)關(guān)處調(diào)用Auth服務(wù)帶來的系統(tǒng)吞吐量瓶頸

    (1). 這個(gè)其實(shí)很容易理解该押,Auth服務(wù)作為公共的基礎(chǔ)服務(wù),大多數(shù)服務(wù)接口都會(huì)需要鑒權(quán)阵谚,Auth服務(wù)需要經(jīng)過復(fù)雜蚕礼。

    (2). 網(wǎng)關(guān)調(diào)用Auth服務(wù),阻塞調(diào)用梢什,只有等Auth服務(wù)返回校驗(yàn)結(jié)果奠蹬,才會(huì)做進(jìn)一步處理。雖說Auth服務(wù)可以多實(shí)例部署嗡午,但是并發(fā)量大了之后囤躁,其瓶頸明顯可見,嚴(yán)重可能會(huì)造成整個(gè)系統(tǒng)的不可用荔睹。

5.2 后續(xù)工作

  • 從整個(gè)系統(tǒng)設(shè)計(jì)角度來講狸演,API級(jí)別操作權(quán)限后期將會(huì)分散在各個(gè)服務(wù)的接口上,由各個(gè)接口負(fù)責(zé)其所需要的權(quán)限僻他、身份等宵距。Spring Security對于接口級(jí)別的權(quán)限校驗(yàn)也是支持的,之所以采用這樣的做法中姜,也是為了兼容新服務(wù)和遺留的服務(wù)消玄,主要是針對遺留服務(wù),新的服務(wù)采用的是分散在各個(gè)接口之上丢胚。
  • 將API級(jí)別操作權(quán)限分散到各個(gè)服務(wù)接口之后,相應(yīng)的能提升Auth服務(wù)的響應(yīng)受扳。網(wǎng)關(guān)能夠及時(shí)的對請求進(jìn)行轉(zhuǎn)發(fā)或者拒絕携龟。
  • API級(jí)別操作權(quán)限所需要的上下文信息對各個(gè)接口真的設(shè)計(jì)的很復(fù)雜,這邊我們確實(shí)花了時(shí)間勘高,同時(shí)管理移動(dòng)服務(wù)的好幾百操作接口所對應(yīng)的權(quán)限峡蟋,非常煩。华望!

本文的源碼地址:
GitHub:https://github.com/keets2012/Auth-service
碼云: https://gitee.com/keets/Auth-Service

訂閱最新文章蕊蝗,歡迎關(guān)注我的公眾號(hào)

微信公眾號(hào)

參考

  1. 配置表單登錄
  2. Spring Security3源碼分析-FilterSecurityInterceptor分析
  3. Core Security Filters
  4. Spring Security(四)--核心過濾器源碼分析

相關(guān)閱讀

認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(一)
認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(二)
認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(三)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赖舟,隨后出現(xiàn)的幾起案子蓬戚,更是在濱河造成了極大的恐慌,老刑警劉巖宾抓,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件子漩,死亡現(xiàn)場離奇詭異豫喧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)幢泼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門紧显,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缕棵,你說我怎么就攤上這事孵班。” “怎么了招驴?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵重父,是天一觀的道長。 經(jīng)常有香客問我忽匈,道長房午,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任丹允,我火速辦了婚禮郭厌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雕蔽。我一直安慰自己折柠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布批狐。 她就那樣靜靜地躺著扇售,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嚣艇。 梳的紋絲不亂的頭發(fā)上承冰,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音食零,去河邊找鬼困乒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贰谣,可吹牛的內(nèi)容都是我干的娜搂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼吱抚,長吁一口氣:“原來是場噩夢啊……” “哼百宇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起秘豹,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對情侶失蹤携御,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體因痛,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡婚苹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸵膏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膊升。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖谭企,靈堂內(nèi)的尸體忽然破棺而出廓译,到底是詐尸還是另有隱情,我是刑警寧澤债查,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布非区,位于F島的核電站,受9級(jí)特大地震影響盹廷,放射性物質(zhì)發(fā)生泄漏征绸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一俄占、第九天 我趴在偏房一處隱蔽的房頂上張望管怠。 院中可真熱鬧,春花似錦缸榄、人聲如沸渤弛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽她肯。三九已至,卻和暖如春鹰贵,著一層夾襖步出監(jiān)牢的瞬間晴氨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工砾莱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瑞筐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓腊瑟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缭乘。 傳聞我的和親對象是個(gè)殘疾皇子杰标,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容