Spring Security 架構(gòu)與源碼分析以及基于Spring Security實(shí)現(xiàn)前后端分離項(xiàng)目權(quán)限控制

Spring Security 架構(gòu)與源碼分析

Spring Security 主要實(shí)現(xiàn)了Authentication(認(rèn)證蝎毡,解決who are you? ) 和 Access Control(訪問控制锋拖,也就是what are you allowed to do决采?谅猾,也稱為Authorization)盯滚。Spring Security在架構(gòu)上將認(rèn)證與授權(quán)分離筋帖,并提供了擴(kuò)展點(diǎn)迅耘。

核心對象

主要代碼在spring-security-core包下面贱枣。要了解Spring Security,需要先關(guān)注里面的核心對象颤专。

SecurityContextHolder, SecurityContext 和 Authentication

SecurityContextHolder 是 SecurityContext的存放容器纽哥,默認(rèn)使用ThreadLocal 存儲(chǔ),意味SecurityContext在相同線程中的方法都可用栖秕。
SecurityContext主要是存儲(chǔ)應(yīng)用的principal信息昵仅,在Spring Security中用Authentication 來表示。

獲取principal:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

在Spring Security中累魔,可以看一下Authentication定義:

public interface Authentication extends Principal, Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * 通常是密碼
     */
    Object getCredentials();

    /**
     * Stores additional details about the authentication request. These might be an IP
     * address, certificate serial number etc.
     */
    Object getDetails();

    /**
     * 用來標(biāo)識是否已認(rèn)證摔笤,如果使用用戶名和密碼登錄,通常是用戶名 
     */
    Object getPrincipal();

    /**
     * 是否已認(rèn)證
     */
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

在實(shí)際應(yīng)用中,通常使用UsernamePasswordAuthenticationToken

public abstract class AbstractAuthenticationToken implements Authentication,
        CredentialsContainer {
        }
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
}

一個(gè)常見的認(rèn)證過程通常是這樣的垦写,創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken吕世,然后交給authenticationManager認(rèn)證(后面詳細(xì)說明),認(rèn)證通過則通過SecurityContextHolder存放Authentication信息梯投。

 UsernamePasswordAuthenticationToken authenticationToken =
            new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());

Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);

UserDetails與UserDetailsService

UserDetails 是Spring Security里的一個(gè)關(guān)鍵接口命辖,他用來表示一個(gè)principal况毅。

public interface UserDetails extends Serializable {
    /**
     * 用戶的授權(quán)信息,可以理解為角色
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * 用戶密碼
     *
     * @return the password
     */
    String getPassword();

    /**
     * 用戶名 
     *   */
    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

UserDetails提供了認(rèn)證所需的必要信息尔艇,在實(shí)際使用里尔许,可以自己實(shí)現(xiàn)UserDetails,并增加額外的信息终娃,比如email味廊、mobile等信息。

在Authentication中的principal通常是用戶名棠耕,我們可以通過UserDetailsService來通過principal獲取UserDetails:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

GrantedAuthority

在UserDetails里說了余佛,GrantedAuthority可以理解為角色,例如 ROLE_ADMINISTRATOR or ROLE_HR_SUPERVISOR窍荧。

小結(jié)

  • SecurityContextHolder, 用來訪問 SecurityContext.
  • SecurityContext, 用來存儲(chǔ)Authentication .
  • Authentication, 代表憑證.
  • GrantedAuthority, 代表權(quán)限.
  • UserDetails, 用戶信息.
  • UserDetailsService,獲取用戶信息.

Authentication認(rèn)證

AuthenticationManager

實(shí)現(xiàn)認(rèn)證主要是通過AuthenticationManager接口辉巡,它只包含了一個(gè)方法:

public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

authenticate()方法主要做三件事:

  1. 如果驗(yàn)證通過,返回Authentication(通常帶上authenticated=true)蕊退。
  2. 認(rèn)證失敗拋出AuthenticationException
  3. 如果無法確定郊楣,則返回null

AuthenticationException是運(yùn)行時(shí)異常,它通常由應(yīng)用程序按通用方式處理,用戶代碼通常不用特意被捕獲和處理這個(gè)異常瓤荔。

AuthenticationManager的默認(rèn)實(shí)現(xiàn)是ProviderManager净蚤,它委托一組AuthenticationProvider實(shí)例來實(shí)現(xiàn)認(rèn)證。
AuthenticationProviderAuthenticationManager類似茉贡,都包含authenticate,但它有一個(gè)額外的方法supports者铜,以允許查詢調(diào)用方是否支持給定Authentication類型:

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

ProviderManager包含一組AuthenticationProvider腔丧,執(zhí)行authenticate時(shí),遍歷Providers作烟,然后調(diào)用supports愉粤,如果支持,則執(zhí)行遍歷當(dāng)前provider的authenticate方法拿撩,如果一個(gè)provider認(rèn)證成功衣厘,則break。

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }

        prepareException(lastException, authentication);

        throw lastException;
    }

從上面的代碼可以看出压恒, ProviderManager有一個(gè)可選parent影暴,如果parent不為空,則調(diào)用parent.authenticate(authentication)

AuthenticationProvider

AuthenticationProvider有多種實(shí)現(xiàn)探赫,大家最關(guān)注的通常是DaoAuthenticationProvider型宙,繼承于AbstractUserDetailsAuthenticationProvider,核心是通過UserDetails來實(shí)現(xiàn)認(rèn)證,DaoAuthenticationProvider默認(rèn)會(huì)自動(dòng)加載伦吠,不用手動(dòng)配妆兑。

先來看AbstractUserDetailsAuthenticationProvider魂拦,看最核心的authenticate

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        // 必須是UsernamePasswordAuthenticationToken
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        //  獲取用戶名
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        // 從緩存獲取
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
               // retrieveUser 抽象方法,獲取用戶
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }
  
            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            // 預(yù)先檢查搁嗓,DefaultPreAuthenticationChecks芯勘,檢查用戶是否被lock或者賬號是否可用
            preAuthenticationChecks.check(user);
            
            // 抽象方法,自定義檢驗(yàn)
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }
      
        // 后置檢查 DefaultPostAuthenticationChecks腺逛,檢查isCredentialsNonExpired
        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
   
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

上面的檢驗(yàn)主要基于UserDetails實(shí)現(xiàn)荷愕,其中獲取用戶和檢驗(yàn)邏輯由具體的類去實(shí)現(xiàn),默認(rèn)實(shí)現(xiàn)是DaoAuthenticationProvider屉来,這個(gè)類的核心是讓開發(fā)者提供UserDetailsService來獲取UserDetails以及 PasswordEncoder來檢驗(yàn)密碼是否有效:

private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;

看具體的實(shí)現(xiàn)路翻,retrieveUser,直接調(diào)用userDetailsService獲取用戶:

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;

        try {
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        }
        catch (UsernameNotFoundException notFound) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
                        presentedPassword, null);
            }
            throw notFound;
        }
        catch (Exception repositoryProblem) {
            throw new InternalAuthenticationServiceException(
                    repositoryProblem.getMessage(), repositoryProblem);
        }

        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }

再來看驗(yàn)證:

protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        Object salt = null;

        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
        // 獲取用戶密碼
        String presentedPassword = authentication.getCredentials().toString();
        // 比較passwordEncoder后的密碼是否和userdetails的密碼一致
        if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
                presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

小結(jié):要自定義認(rèn)證,使用DaoAuthenticationProvider茄靠,只需要為其提供PasswordEncoder和UserDetailsService就可以了茂契。

定制 Authentication Managers

Spring Security提供了一個(gè)Builder類AuthenticationManagerBuilder,借助它可以快速實(shí)現(xiàn)自定義認(rèn)證慨绳。

看官方源碼說明:

SecurityBuilder used to create an AuthenticationManager . Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService , and adding AuthenticationProvider's.

AuthenticationManagerBuilder可以用來Build一個(gè)AuthenticationManager掉冶,可以創(chuàng)建基于內(nèi)存的認(rèn)證、LDAP認(rèn)證脐雪、 JDBC認(rèn)證厌小,以及添加UserDetailsService和AuthenticationProvider。

簡單使用:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {


  public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
        this.authenticationManagerBuilder = authenticationManagerBuilder;
        this.userDetailsService = userDetailsService;
        this.tokenProvider = tokenProvider;
        this.corsFilter = corsFilter;
        this.problemSupport = problemSupport;
    }

    @PostConstruct
    public void init() {
        try {
            authenticationManagerBuilder
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
        } catch (Exception e) {
            throw new BeanInitializationException("Security configuration failed", e);
        }
    }

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
            .antMatchers("/api/register").permitAll()
            .antMatchers("/api/activate").permitAll()
            .antMatchers("/api/authenticate").permitAll()
            .antMatchers("/api/account/reset-password/init").permitAll()
            .antMatchers("/api/account/reset-password/finish").permitAll()
            .antMatchers("/api/profile-info").permitAll()
            .antMatchers("/api/**").authenticated()
            .antMatchers("/management/health").permitAll()
            .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/v2/api-docs/**").permitAll()
            .antMatchers("/swagger-resources/configuration/ui").permitAll()
            .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN)
        .and()
            .apply(securityConfigurerAdapter());

    }
}

授權(quán)與訪問控制

一旦認(rèn)證成功战秋,我們可以繼續(xù)進(jìn)行授權(quán)璧亚,授權(quán)是通過AccessDecisionManager來實(shí)現(xiàn)的≈牛框架有三種實(shí)現(xiàn)癣蟋,默認(rèn)是AffirmativeBased,通過AccessDecisionVoter決策狰闪,有點(diǎn)像ProviderManager委托給AuthenticationProviders來認(rèn)證疯搅。

public void decide(Authentication authentication, Object object,
            Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        // 遍歷DecisionVoter 
        for (AccessDecisionVoter voter : getDecisionVoters()) {
            // 投票
            int result = voter.vote(authentication, object, configAttributes);

            if (logger.isDebugEnabled()) {
                logger.debug("Voter: " + voter + ", returned: " + result);
            }

            switch (result) {
            case AccessDecisionVoter.ACCESS_GRANTED:
                return;

            case AccessDecisionVoter.ACCESS_DENIED:
                deny++;

                break;

            default:
                break;
            }
        }
       
        // 一票否決
        if (deny > 0) {
            throw new AccessDeniedException(messages.getMessage(
                    "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        }

        // To get this far, every AccessDecisionVoter abstained
        checkAllowIfAllAbstainDecisions();
    }

來看AccessDecisionVoter:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object,
        Collection<ConfigAttribute> attributes);

object是用戶要訪問的資源,ConfigAttribute則是訪問object要滿足的條件埋泵,通常payload是字符串幔欧,比如ROLE_ADMIN 。所以我們來看下RoleVoter的實(shí)現(xiàn)丽声,其核心就是從authentication提取出GrantedAuthority礁蔗,然后和ConfigAttribute比較是否滿足條件。


public boolean supports(ConfigAttribute attribute) {
        if ((attribute.getAttribute() != null)
                && attribute.getAttribute().startsWith(getRolePrefix())) {
            return true;
        }
        else {
            return false;
        }
    }
    
public boolean supports(Class<?> clazz) {
        return true;
    }


public int vote(Authentication authentication, Object object,
            Collection<ConfigAttribute> attributes) {
        if(authentication == null) {
            return ACCESS_DENIED;
        }
        int result = ACCESS_ABSTAIN;
        
        // 獲取GrantedAuthority信息
        Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);

        for (ConfigAttribute attribute : attributes) {
            if (this.supports(attribute)) {
                // 默認(rèn)拒絕訪問
                result = ACCESS_DENIED;

                // Attempt to find a matching granted authority
                for (GrantedAuthority authority : authorities) {
                     // 判斷是否有匹配的 authority
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        // 可訪問
                        return ACCESS_GRANTED;
                    }
                }
            }
        }

        return result;
    }

這里要疑問雁社,ConfigAttribute哪來的瘦麸?其實(shí)就是上面ApplicationSecurity的configure里的。

web security 如何實(shí)現(xiàn)

Web層中的Spring Security(用于UI和HTTP后端)基于Servlet Filters歧胁,下圖顯示了單個(gè)HTTP請求的處理程序的典型分層滋饲。

[圖片上傳失敗...(image-47e8d6-1528938759966)]

Spring Security通過FilterChainProxy作為單一的Filter注冊到web層厉碟,Proxy內(nèi)部的Filter。

[圖片上傳失敗...(image-3fba6-1528938759966)]

FilterChainProxy相當(dāng)于一個(gè)filter的容器屠缭,通過VirtualFilterChain來依次調(diào)用各個(gè)內(nèi)部filter



public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (clearContext) {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                doFilterInternal(request, response, chain);
            }
            finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        }
        else {
            doFilterInternal(request, response, chain);
        }
    }

    private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);

        List<Filter> filters = getFilters(fwRequest);

        if (filters == null || filters.size() == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                        + (filters == null ? " has no matching filters"
                                : " has an empty filter list"));
            }

            fwRequest.reset();

            chain.doFilter(fwRequest, fwResponse);

            return;
        }

        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }
    
    private static class VirtualFilterChain implements FilterChain {
        private final FilterChain originalChain;
        private final List<Filter> additionalFilters;
        private final FirewalledRequest firewalledRequest;
        private final int size;
        private int currentPosition = 0;

        private VirtualFilterChain(FirewalledRequest firewalledRequest,
                FilterChain chain, List<Filter> additionalFilters) {
            this.originalChain = chain;
            this.additionalFilters = additionalFilters;
            this.size = additionalFilters.size();
            this.firewalledRequest = firewalledRequest;
        }

        public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            if (currentPosition == size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " reached end of additional filter chain; proceeding with original chain");
                }

                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();

                originalChain.doFilter(request, response);
            }
            else {
                currentPosition++;

                Filter nextFilter = additionalFilters.get(currentPosition - 1);

                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + " at position " + currentPosition + " of " + size
                            + " in additional filter chain; firing Filter: '"
                            + nextFilter.getClass().getSimpleName() + "'");
                }

                nextFilter.doFilter(request, response, this);
            }
        }
    }
    

spring security動(dòng)態(tài)配置url權(quán)限

緣起

標(biāo)準(zhǔn)的RABC, 權(quán)限需要支持動(dòng)態(tài)配置箍鼓,spring security默認(rèn)是在代碼里約定好權(quán)限,真實(shí)的業(yè)務(wù)場景通常需要可以支持動(dòng)態(tài)配置角色訪問權(quán)限呵曹,即在運(yùn)行時(shí)去配置url對應(yīng)的訪問角色款咖。

基于spring security,如何實(shí)現(xiàn)這個(gè)需求呢奄喂?

最簡單的方法就是自定義一個(gè)Filter去完成權(quán)限判斷铐殃,但這脫離了spring security框架,如何基于spring security優(yōu)雅的實(shí)現(xiàn)呢跨新?

spring security 授權(quán)回顧

spring security 通過FilterChainProxy作為注冊到web的filter富腊,F(xiàn)ilterChainProxy里面一次包含了內(nèi)置的多個(gè)過濾器,我們首先需要了解spring security內(nèi)置的各種filter:

Alias Filter Class Namespace Element or Attribute
CHANNEL_FILTER ChannelProcessingFilter http/intercept-url@requires-channel
SECURITY_CONTEXT_FILTER SecurityContextPersistenceFilter http
CONCURRENT_SESSION_FILTER ConcurrentSessionFilter session-management/concurrency-control
HEADERS_FILTER HeaderWriterFilter http/headers
CSRF_FILTER CsrfFilter http/csrf
LOGOUT_FILTER LogoutFilter http/logout
X509_FILTER X509AuthenticationFilter http/x509
PRE_AUTH_FILTER AbstractPreAuthenticatedProcessingFilter Subclasses N/A
CAS_FILTER CasAuthenticationFilter N/A
FORM_LOGIN_FILTER UsernamePasswordAuthenticationFilter http/form-login
BASIC_AUTH_FILTER BasicAuthenticationFilter http/http-basic
SERVLET_API_SUPPORT_FILTER SecurityContextHolderAwareRequestFilter http/@servlet-api-provision
JAAS_API_SUPPORT_FILTER JaasApiIntegrationFilter http/@jaas-api-provision
REMEMBER_ME_FILTER RememberMeAuthenticationFilter http/remember-me
ANONYMOUS_FILTER AnonymousAuthenticationFilter http/anonymous
SESSION_MANAGEMENT_FILTER SessionManagementFilter session-management
EXCEPTION_TRANSLATION_FILTER ExceptionTranslationFilter http
FILTER_SECURITY_INTERCEPTOR FilterSecurityInterceptor http
SWITCH_USER_FILTER SwitchUserFilter N/A

最重要的是FilterSecurityInterceptor域帐,該過濾器實(shí)現(xiàn)了主要的鑒權(quán)邏輯赘被,最核心的代碼在這里:

protected InterceptorStatusToken beforeInvocation(Object object) {
    
        // 獲取訪問URL所需權(quán)限
        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

    
        Authentication authenticated = authenticateIfRequired();

        // 通過accessDecisionManager鑒權(quán)
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));

            throw accessDeniedException;
        }

        if (debug) {
            logger.debug("Authorization successful");
        }

        if (publishAuthorizationSuccess) {
            publishEvent(new AuthorizedEvent(object, attributes, authenticated));
        }

        // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
                attributes);

        if (runAs == null) {
            if (debug) {
                logger.debug("RunAsManager did not change Authentication object");
            }

            // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                    attributes, object);
        }
        else {
            if (debug) {
                logger.debug("Switching to RunAs Authentication: " + runAs);
            }

            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
            SecurityContextHolder.getContext().setAuthentication(runAs);

            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
    }

從上面可以看出,要實(shí)現(xiàn)動(dòng)態(tài)鑒權(quán)肖揣,可以從兩方面著手:

  • 自定義SecurityMetadataSource民假,實(shí)現(xiàn)從數(shù)據(jù)庫加載ConfigAttribute
  • 另外就是可以自定義accessDecisionManager,官方的UnanimousBased其實(shí)足夠使用龙优,并且他是基于AccessDecisionVoter來實(shí)現(xiàn)權(quán)限認(rèn)證的羊异,因此我們只需要自定義一個(gè)AccessDecisionVoter就可以了

下面來看分別如何實(shí)現(xiàn)。

自定義AccessDecisionManager

官方的三個(gè)AccessDecisionManager都是基于AccessDecisionVoter來實(shí)現(xiàn)權(quán)限認(rèn)證的彤断,因此我們只需要自定義一個(gè)AccessDecisionVoter就可以了野舶。

自定義主要是實(shí)現(xiàn)AccessDecisionVoter接口,我們可以仿照官方的RoleVoter實(shí)現(xiàn)一個(gè):


public class RoleBasedVoter implements AccessDecisionVoter<Object> {

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if(authentication == null) {
            return ACCESS_DENIED;
        }
        int result = ACCESS_ABSTAIN;
        Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);

        for (ConfigAttribute attribute : attributes) {
            if(attribute.getAttribute()==null){
                continue;
            }
            if (this.supports(attribute)) {
                result = ACCESS_DENIED;

                // Attempt to find a matching granted authority
                for (GrantedAuthority authority : authorities) {
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        return ACCESS_GRANTED;
                    }
                }
            }
        }

        return result;
    }

    Collection<? extends GrantedAuthority> extractAuthorities(
        Authentication authentication) {
        return authentication.getAuthorities();
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

如何加入動(dòng)態(tài)權(quán)限呢瓦糟?

vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)里的Object object的類型是FilterInvocation筒愚,可以通過getRequestUrl獲取當(dāng)前請求的URL:

  FilterInvocation fi = (FilterInvocation) object;
  String url = fi.getRequestUrl();

因此這里擴(kuò)展空間就大了赴蝇,可以從DB動(dòng)態(tài)加載菩浙,然后判斷URL的ConfigAttribute就可以了。

如何使用這個(gè)RoleBasedVoter呢句伶?在configure里使用accessDecisionManager方法自定義,我們還是使用官方的UnanimousBased,然后將自定義的RoleBasedVoter加入即可炭庙。

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
            // 自定義accessDecisionManager
            .accessDecisionManager(accessDecisionManager())
          
        .and()
            .apply(securityConfigurerAdapter());

    }


    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<? extends Object>> decisionVoters
            = Arrays.asList(
            new WebExpressionVoter(),
            // new RoleVoter(),
            new RoleBasedVoter(),
            new AuthenticatedVoter());
        return new UnanimousBased(decisionVoters);
    }

自定義SecurityMetadataSource

自定義FilterInvocationSecurityMetadataSource只要實(shí)現(xiàn)接口即可厢岂,在接口里從DB動(dòng)態(tài)加載規(guī)則。

為了復(fù)用代碼里的定義楚堤,我們可以將代碼里生成的SecurityMetadataSource帶上疫蔓,在構(gòu)造函數(shù)里傳入默認(rèn)的FilterInvocationSecurityMetadataSource含懊。

public class AppFilterInvocationSecurityMetadataSource implements org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource {

    private FilterInvocationSecurityMetadataSource  superMetadataSource;

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    public AppFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSource expressionBasedFilterInvocationSecurityMetadataSource){
         this.superMetadataSource = expressionBasedFilterInvocationSecurityMetadataSource;

         // TODO 從數(shù)據(jù)庫加載權(quán)限配置
    }

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    // 這里的需要從DB加載
    private final Map<String,String> urlRoleMap = new HashMap<String,String>(){{
        put("/open/**","ROLE_ANONYMOUS");
        put("/health","ROLE_ANONYMOUS");
        put("/restart","ROLE_ADMIN");
        put("/demo","ROLE_USER");
    }};

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();

        for(Map.Entry<String,String> entry:urlRoleMap.entrySet()){
            if(antPathMatcher.match(entry.getKey(),url)){
                return SecurityConfig.createList(entry.getValue());
            }
        }

        //  返回代碼定義的默認(rèn)配置
        return superMetadataSource.getAttributes(object);
    }



    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

怎么使用?和accessDecisionManager不一樣衅胀,ExpressionUrlAuthorizationConfigurer 并沒有提供set方法設(shè)置FilterSecurityInterceptorFilterInvocationSecurityMetadataSource岔乔,how to do?

發(fā)現(xiàn)一個(gè)擴(kuò)展方法withObjectPostProcessor,通過該方法自定義一個(gè)處理FilterSecurityInterceptor類型的ObjectPostProcessor就可以修改FilterSecurityInterceptor滚躯。

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
            // 自定義FilterInvocationSecurityMetadataSource
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(
                    O fsi) {
                    fsi.setSecurityMetadataSource(mySecurityMetadataSource(fsi.getSecurityMetadataSource()));
                    return fsi;
                }
            })
        .and()
            .apply(securityConfigurerAdapter());

    }


    @Bean
    public AppFilterInvocationSecurityMetadataSource mySecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
        AppFilterInvocationSecurityMetadataSource securityMetadataSource = new AppFilterInvocationSecurityMetadataSource(filterInvocationSecurityMetadataSource);
        return securityMetadataSource;
}

小結(jié)

本文介紹了兩種基于spring security實(shí)現(xiàn)動(dòng)態(tài)權(quán)限的方法雏门,一是自定義accessDecisionManager,二是自定義FilterInvocationSecurityMetadataSource掸掏。實(shí)際項(xiàng)目里可以根據(jù)需要靈活選擇茁影。

基于spring security 實(shí)現(xiàn)前后端分離項(xiàng)目權(quán)限控制

前后端分離的項(xiàng)目,前端有菜單(menu)丧凤,后端有API(backendApi)募闲,一個(gè)menu對應(yīng)的頁面有N個(gè)API接口來支持,本文介紹如何基于spring security實(shí)現(xiàn)前后端的同步權(quán)限控制息裸。

實(shí)現(xiàn)思路

還是基于Role來實(shí)現(xiàn)蝇更,具體的思路是,一個(gè)Role擁有多個(gè)Menu呼盆,一個(gè)menu有多個(gè)backendApi年扩,其中Role和menu,以及menu和backendApi都是ManyToMany關(guān)系访圃。

驗(yàn)證授權(quán)也很簡單厨幻,用戶登陸系統(tǒng)時(shí),獲取Role關(guān)聯(lián)的Menu腿时,頁面訪問后端API時(shí)况脆,再驗(yàn)證下用戶是否有訪問API的權(quán)限。

domain定義

我們用JPA來實(shí)現(xiàn)批糟,先來定義Role

public class Role implements Serializable {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 名稱
     */
    @NotNull
    @ApiModelProperty(value = "名稱", required = true)
    @Column(name = "name", nullable = false)
    private String name;

    /**
     * 備注
     */
    @ApiModelProperty(value = "備注")
    @Column(name = "remark")
    private String remark;

    @JsonIgnore
    @ManyToMany
    @JoinTable(
        name = "role_menus",
        joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
        inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    @BatchSize(size = 100)
    private Set<Menu> menus = new HashSet<>();
    
    }

以及Menu:

public class Menu implements Serializable {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "parent_id")
    private Integer parentId;

    /**
     * 文本
     */
    @ApiModelProperty(value = "文本")
    @Column(name = "text")
    private String text;
    
    @ApiModelProperty(value = "angular路由")
    @Column(name = "link")
    private String link;
    
    @ManyToMany
    @JsonIgnore
    @JoinTable(name = "backend_api_menus",
        joinColumns = @JoinColumn(name="menus_id", referencedColumnName="id"),
        inverseJoinColumns = @JoinColumn(name="backend_apis_id", referencedColumnName="id"))
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private Set<BackendApi> backendApis = new HashSet<>();

    @ManyToMany(mappedBy = "menus")
    @JsonIgnore
    private Set<Role> roles = new HashSet<>();
    }
    
    

最后是BackendApi格了,區(qū)分method(HTTP請求方法)、tag(哪一個(gè)Controller)和path(API請求路徑):

public class BackendApi implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "tag")
    private String tag;

    @Column(name = "path")
    private String path;

    @Column(name = "method")
    private String method;

    @Column(name = "summary")
    private String summary;

    @Column(name = "operation_id")
    private String operationId;

    @ManyToMany(mappedBy = "backendApis")
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private Set<Menu> menus = new HashSet<>();
    
    }

管理頁面實(shí)現(xiàn)

Menu菜單是業(yè)務(wù)需求確定的徽鼎,因此提供CRUD編輯即可盛末。
BackendAPI,可以通過swagger來獲取否淤。
前端選擇ng-algin悄但,參見Angular 中后臺前端解決方案 - Ng Alain 介紹

通過swagger獲取BackendAPI

獲取swagger api有多種方法,最簡單的就是訪問http接口獲取json石抡,然后解析檐嚣,這很簡單,這里不贅述啰扛,還有一種就是直接調(diào)用相關(guān)API獲取Swagger對象嚎京。

查看官方的web代碼嗡贺,可以看到獲取數(shù)據(jù)大概是這樣的:

        String groupName = Optional.fromNullable(swaggerGroup).or(Docket.DEFAULT_GROUP_NAME);
        Documentation documentation = documentationCache.documentationByGroup(groupName);
        if (documentation == null) {
            return new ResponseEntity<Json>(HttpStatus.NOT_FOUND);
        }
        Swagger swagger = mapper.mapDocumentation(documentation);
        UriComponents uriComponents = componentsFrom(servletRequest, swagger.getBasePath());
        swagger.basePath(Strings.isNullOrEmpty(uriComponents.getPath()) ? "/" : uriComponents.getPath());
        if (isNullOrEmpty(swagger.getHost())) {
            swagger.host(hostName(uriComponents));
        }
        return new ResponseEntity<Json>(jsonSerializer.toJson(swagger), HttpStatus.OK);

其中的documentationCache、environment鞍帝、mapper等可以直接Autowired獲得:

@Autowired
    public SwaggerResource(
        Environment environment,
        DocumentationCache documentationCache,
        ServiceModelToSwagger2Mapper mapper,
        BackendApiRepository backendApiRepository,
        JsonSerializer jsonSerializer) {

        this.hostNameOverride = environment.getProperty("springfox.documentation.swagger.v2.host", "DEFAULT");
        this.documentationCache = documentationCache;
        this.mapper = mapper;
        this.jsonSerializer = jsonSerializer;

        this.backendApiRepository = backendApiRepository;

    }

然后我們自動(dòng)加載就簡單了暑刃,寫一個(gè)updateApi接口,讀取swagger對象膜眠,然后解析成BackendAPI岩臣,存儲(chǔ)到數(shù)據(jù)庫:

@RequestMapping(
        value = "/api/updateApi",
        method = RequestMethod.GET,
        produces = { APPLICATION_JSON_VALUE, HAL_MEDIA_TYPE })
    @PropertySourcedMapping(
        value = "${springfox.documentation.swagger.v2.path}",
        propertyKey = "springfox.documentation.swagger.v2.path")
    @ResponseBody
    public ResponseEntity<Json> updateApi(
        @RequestParam(value = "group", required = false) String swaggerGroup) {

        // 加載已有的api
        Map<String,Boolean> apiMap = Maps.newHashMap();
        List<BackendApi> apis = backendApiRepository.findAll();
        apis.stream().forEach(api->apiMap.put(api.getPath()+api.getMethod(),true));

        // 獲取swagger
        String groupName = Optional.fromNullable(swaggerGroup).or(Docket.DEFAULT_GROUP_NAME);
        Documentation documentation = documentationCache.documentationByGroup(groupName);
        if (documentation == null) {
            return new ResponseEntity<Json>(HttpStatus.NOT_FOUND);
        }
        Swagger swagger = mapper.mapDocumentation(documentation);

        // 加載到數(shù)據(jù)庫
        for(Map.Entry<String, Path> item : swagger.getPaths().entrySet()){
            String path = item.getKey();
            Path pathInfo = item.getValue();
            createApiIfNeeded(apiMap, path,  pathInfo.getGet(), HttpMethod.GET.name());
            createApiIfNeeded(apiMap, path,  pathInfo.getPost(), HttpMethod.POST.name());
            createApiIfNeeded(apiMap, path,  pathInfo.getDelete(), HttpMethod.DELETE.name());
            createApiIfNeeded(apiMap, path,  pathInfo.getPut(), HttpMethod.PUT.name());
        }
        return new ResponseEntity<Json>(HttpStatus.OK);
    }

其中createApiIfNeeded,先判斷下是否存在宵膨,不存在的則新增:

 private void createApiIfNeeded(Map<String, Boolean> apiMap, String path, Operation operation, String method) {
        if(operation==null) {
            return;
        }
        if(!apiMap.containsKey(path+ method)){
            apiMap.put(path+ method,true);

            BackendApi api = new BackendApi();
            api.setMethod( method);
            api.setOperationId(operation.getOperationId());
            api.setPath(path);
            api.setTag(operation.getTags().get(0));
            api.setSummary(operation.getSummary());

            // 保存
            this.backendApiRepository.save(api);
        }
    }

最后架谎,做一個(gè)簡單頁面展示即可:

enter description here
enter description here

菜單管理

新增和修改頁面,可以選擇上級菜單辟躏,后臺API做成按tag分組谷扣,可多選即可:

enter description here
enter description here

列表頁面

enter description here
enter description here

角色管理

普通的CRUD,最主要的增加一個(gè)菜單授權(quán)頁面捎琐,菜單按層級顯示即可:

enter description here
enter description here

認(rèn)證實(shí)現(xiàn)

管理頁面可以做成千奇百樣会涎,最核心的還是如何實(shí)現(xiàn)認(rèn)證。

在上一篇文章spring security實(shí)現(xiàn)動(dòng)態(tài)配置url權(quán)限的兩種方法里我們說了瑞凑,可以自定義FilterInvocationSecurityMetadataSource來實(shí)現(xiàn)末秃。

實(shí)現(xiàn)FilterInvocationSecurityMetadataSource接口即可,核心是根據(jù)FilterInvocation的Request的method和path籽御,獲取對應(yīng)的Role练慕,然后交給RoleVoter去判斷是否有權(quán)限。

自定義FilterInvocationSecurityMetadataSource

我們新建一個(gè)DaoSecurityMetadataSource實(shí)現(xiàn)FilterInvocationSecurityMetadataSource接口技掏,主要看getAttributes方法:

     @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;

        List<Role> neededRoles = this.getRequestNeededRoles(fi.getRequest().getMethod(), fi.getRequestUrl());

        if (neededRoles != null) {
            return SecurityConfig.createList(neededRoles.stream().map(role -> role.getName()).collect(Collectors.toList()).toArray(new String[]{}));
        }

        //  返回默認(rèn)配置
        return superMetadataSource.getAttributes(object);
    }

核心是getRequestNeededRoles怎么實(shí)現(xiàn)铃将,獲取到干凈的RequestUrl(去掉參數(shù)),然后看是否有對應(yīng)的backendAPI,如果沒有哑梳,則有可能該API有path參數(shù)劲阎,我們可以去掉最后的path,去庫里模糊匹配鸠真,直到找到悯仙。

 public List<Role> getRequestNeededRoles(String method, String path) {
        String rawPath = path;
        //  remove parameters
        if(path.indexOf("?")>-1){
            path = path.substring(0,path.indexOf("?"));
        }
        // /menus/{id}
        BackendApi api = backendApiRepository.findByPathAndMethod(path, method);
        if (api == null){
            // try fetch by remove last path
            api = loadFromSimilarApi(method, path, rawPath);
        }

        if (api != null && api.getMenus().size() > 0) {
            return api.getMenus()
                .stream()
                .flatMap(menu -> menuRepository.findOneWithRolesById(menu.getId()).getRoles().stream())
                .collect(Collectors.toList());
        }
        return null;
    }

    private BackendApi loadFromSimilarApi(String method, String path, String rawPath) {
        if(path.lastIndexOf("/")>-1){
            path = path.substring(0,path.lastIndexOf("/"));
            List<BackendApi> apis = backendApiRepository.findByPathStartsWithAndMethod(path, method);

            // 如果為空,再去掉一層path
            while(apis==null){
                if(path.lastIndexOf("/")>-1) {
                    path = path.substring(0, path.lastIndexOf("/"));
                    apis = backendApiRepository.findByPathStartsWithAndMethod(path, method);
                }else{
                    break;
                }
            }

            if(apis!=null){
                for(BackendApi backendApi : apis){
                    if (antPathMatcher.match(backendApi.getPath(), rawPath)) {
                        return backendApi;
                    }
                }
            }
        }
        return null;
    }

其中弧哎,BackendApiRepository:

    @EntityGraph(attributePaths = "menus")
    BackendApi findByPathAndMethod(String path,String method);

    @EntityGraph(attributePaths = "menus")
    List<BackendApi> findByPathStartsWithAndMethod(String path,String method);
    

以及MenuRepository

    @EntityGraph(attributePaths = "roles")
    Menu findOneWithRolesById(long id);

使用DaoSecurityMetadataSource

需要注意的是雁比,在DaoSecurityMetadataSource里稚虎,不能直接注入Repository撤嫩,我們可以給DaoSecurityMetadataSource添加一個(gè)方法,方便傳入:

   public void init(MenuRepository menuRepository, BackendApiRepository backendApiRepository) {
        this.menuRepository = menuRepository;
        this.backendApiRepository = backendApiRepository;
    }

然后建立一個(gè)容器蠢终,存儲(chǔ)實(shí)例化的DaoSecurityMetadataSource序攘,我們可以建立如下的ApplicationContext來作為對象容器茴她,存取對象:

public class ApplicationContext {
    static Map<Class<?>,Object> beanMap = Maps.newConcurrentMap();

    public static <T> T getBean(Class<T> requireType){
        return (T) beanMap.get(requireType);
    }

    public static void registerBean(Object item){
        beanMap.put(item.getClass(),item);
    }
}

在SecurityConfiguration配置中使用DaoSecurityMetadataSource,并通過ApplicationContext.registerBeanDaoSecurityMetadataSource注冊:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
            ....
           // .withObjectPostProcessor()
            // 自定義accessDecisionManager
            .accessDecisionManager(accessDecisionManager())
            // 自定義FilterInvocationSecurityMetadataSource
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(
                    O fsi) {
                    fsi.setSecurityMetadataSource(daoSecurityMetadataSource(fsi.getSecurityMetadataSource()));
                    return fsi;
                }
            })
        .and()
            .apply(securityConfigurerAdapter());

    }

    @Bean
    public DaoSecurityMetadataSource daoSecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
        DaoSecurityMetadataSource securityMetadataSource = new DaoSecurityMetadataSource(filterInvocationSecurityMetadataSource);
        ApplicationContext.registerBean(securityMetadataSource);
        return securityMetadataSource;
    }

最后程奠,在程序啟動(dòng)后丈牢,通過ApplicationContext.getBean獲取到daoSecurityMetadataSource,然后調(diào)用init注入Repository

 public static void postInit(){
        ApplicationContext
            .getBean(DaoSecurityMetadataSource.class)
 .init(applicationContext.getBean(MenuRepository.class),applicationContext.getBean(BackendApiRepository.class));
    }

    static ConfigurableApplicationContext applicationContext;

    public static void main(String[] args) throws UnknownHostException {
        SpringApplication app = new SpringApplication(UserCenterApp.class);
        DefaultProfileUtil.addDefaultProfile(app);
        applicationContext = app.run(args);

        // 后初始化
        postInit();
}

大功告成瞄沙!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末己沛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子距境,更是在濱河造成了極大的恐慌申尼,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垫桂,死亡現(xiàn)場離奇詭異师幕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)诬滩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門霹粥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人疼鸟,你說我怎么就攤上這事后控。” “怎么了空镜?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵忆蚀,是天一觀的道長。 經(jīng)常有香客問我姑裂,道長馋袜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任舶斧,我火速辦了婚禮欣鳖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茴厉。我一直安慰自己泽台,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布矾缓。 她就那樣靜靜地躺著怀酷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗜闻。 梳的紋絲不亂的頭發(fā)上蜕依,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼样眠。 笑死友瘤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的檐束。 我是一名探鬼主播辫秧,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼被丧!你這毒婦竟也來了盟戏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤甥桂,失蹤者是張志新(化名)和其女友劉穎抓半,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體格嘁,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡笛求,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了糕簿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片探入。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖懂诗,靈堂內(nèi)的尸體忽然破棺而出蜂嗽,到底是詐尸還是另有隱情,我是刑警寧澤殃恒,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布植旧,位于F島的核電站,受9級特大地震影響离唐,放射性物質(zhì)發(fā)生泄漏病附。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一亥鬓、第九天 我趴在偏房一處隱蔽的房頂上張望完沪。 院中可真熱鬧,春花似錦嵌戈、人聲如沸覆积。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宽档。三九已至,卻和暖如春庵朝,著一層夾襖步出監(jiān)牢的瞬間吗冤,已是汗流浹背又厉。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留欣孤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓昔逗,卻偏偏與公主長得像降传,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子勾怒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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