springboot+springsecurity如何實現動態url細粒度權限認證

謹記:Url表隻儲存受保護的資源,不在表裡的資源說明不受保護,任何人都可以訪問

1、MyFilterInvocationSecurityMetadataSource 類判斷該訪問路徑是否被保護

@Component
//用於設置受保護資源的權限信息的數據源
public class MyFilterInvocationSecurityMetadataSource implements
        FilterInvocationSecurityMetadataSource {
    @Bean
    public AntPathMatcher getAntPathMatcher(){
        return new AntPathMatcher();
    }    
    @Autowired  
	//獲取數據庫中的保存的url  Url表隻儲存受保護的資源,不在表裡的資源說明不受保護,任何人都可以訪問
    private RightsMapper rightsMapper; 
    
    @Autowired
    private AntPathMatcher antPathMatcher;
    @Override
    /*
     * @param 被調用的保護資源
     * @return 返回能夠訪問該保護資源的角色集合,如果沒有,則應返回空集合。
     */
    public Collection<ConfigAttribute> getAttributes(Object object)
            throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        //獲取用戶請求的Url
        String url = fi.getRequestUrl();
        //先到數據庫獲取受權限控制的Url
        List<Rights> us = rightsMapper.queryAll();
        //用於儲存用戶請求的Url能夠訪問的角色
        Collection<ConfigAttribute> rs=new ArrayList<ConfigAttribute>();
        for(Rights u:us){
            if (u.getUrl() != null) {
                //逐一判斷用戶請求的Url是否和數據庫中受權限控制的Url有匹配的
                if (antPathMatcher.match(u.getUrl(), url)) {
                    //如果有則將可以訪問該Url的角色儲存到Collection<ConfigAttribute>
                    rs.add(rightsMapper.queryById(u.getId()));
                }
            }
        }
        if(rs.size()>0) {
            return rs;
        }
        //沒有匹配到,就說明此資源沒有被控制,所有人都可以訪問,返回null即可,返回null則不會進入之後的decide方法
        return null;
    }
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        // TODO 自動生成的方法存根
        return null;
    }
    @Override
    public boolean supports(Class<?> clazz) {
        // TODO 自動生成的方法存根
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

rights表中的部分內容:

表結構

在這裡插入圖片描述

內容:

在這裡插入圖片描述

2、MyAccessDecisionManager 類判斷該用戶是否有權限訪問

@Component
//用於設置判斷當前用戶是否可以訪問被保護資源的邏輯
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    /*
     * @param 請求該保護資源的用戶對象
     * @param 被調用的保護資源
     * @param 有權限調用該資源的集合
     */
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> ite = configAttributes.iterator();
        //遍歷configAttributes,查看當前用戶是否有對應的權限訪問該保護資源
        while (ite.hasNext()) {
            ConfigAttribute ca = ite.next();
            String needRole = ca.getAttribute();
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (ga.getAuthority().equals(needRole)) {
                    // 匹配到有對應角色,則允許通過
                    return;
                }
            }
        }
        // 該url有配置權限,但是當前登錄用戶沒有匹配到對應權限,則禁止訪問
        throw new AccessDeniedException("not allow");
    }
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

3、在SecurityConfig 類中配置說明

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyUserDetailsService myUserDetailsService;
    @Autowired
    private SendSmsSecurityConfig sendSmsSecurityConfig;
    @Autowired
    private MyAccessDecisionManager myAccessDecisionManager;
    @Autowired
    private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
    //加密機制
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance(); // 不加密
    }
    //認證
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()//對請求授權
                .antMatchers("/**").permitAll()
                .anyRequest()//任何請求
                .authenticated()//登錄後訪問
                .withObjectPostProcessor(
                        new ObjectPostProcessor<FilterSecurityInterceptor>() {
                            @Override
                            public <O extends FilterSecurityInterceptor> O postProcess(
                                    O fsi) {
                                fsi.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
                                fsi.setAccessDecisionManager(myAccessDecisionManager);
                                return fsi;
                            }
                        })
                .and().csrf().disable();
    }
}

配置如下代碼:

在這裡插入圖片描述

至此完成所有配置!!!

SpringSecurity解決公共接口自定義權限驗證失效問題,和源碼分析

背景:

自定義權限認證,一部分接口必須要有相應的角色權限,一部分接口面向所有訪問者,一部分接口任何人都不能訪問。但是在使用 SpringSecurity的過程中發現,框架會將沒有指定角色列表的URL資源直接放行,不做攔截。

用戶登錄認證成功後,攜帶Token訪問URL資源,spring security 根據Token(請求頭Authorization中)來分辨不同用戶。

用戶權限數據源是一個Map:以 URL資源為Key,以有權訪問的Key的角色列表為Value。

使用時發現當一個接口有Key,但是Value為空或null時,spring security 框架自動放行,導致瞭權限失效問題。

解決方法有兩種:

第一種方法:

默認rejectPublicInvocations為false。

對需要控制權限的URL資源添加標志,以防止roleList為空,跳過瞭權限驗證.

公共權限設置為null,不進行權限驗證

第二種方法:

配置rejectPublicInvocations為true

此後roleList為空,或者沒有找到URL資源時,都為拒絕訪問

需要控制權限的URL資源,即使對應角色為空,也會進行權限驗證

公共權限設置為所有角色和匿名角色,不進行權限驗證

package org.springframework.security.access.intercept;
/**
 * 對安全對象(訪問請求+用戶主體)攔截的抽象類源碼
 */
public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
	// ... 其他方法省略
	
	protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		final boolean debug = logger.isDebugEnabled();
		if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
			throw new IllegalArgumentException(
					"Security invocation attempted for object "
							+ object.getClass().getName()
							+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
							+ getSecureObjectClass());
		}
		// 從權限數據源獲取瞭當前 <URL資源> 對應的 <角色列表>
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
				
		// 框架在此處判斷URL資源對應的角色列表是否為空
		if (attributes == null || attributes.isEmpty()) {
			// rejectPublicInvocations默認為false 
			// 可以配置為true,即角色列表為空的時候不進行放行
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException(
						"Secure object invocation "
								+ object
								+ " was denied as public invocations are not allowed via this interceptor. "
								+ "This indicates a configuration error because the "
								+ "rejectPublicInvocations property is set to 'true'");
			}
			if (debug) {
				logger.debug("Public object - authentication not attempted");
			}
			publishEvent(new PublicInvocationEvent(object));
			return null; // no further work post-invocation
		}
		if (debug) {
			logger.debug("Secure object: " + object + "; Attributes: " + attributes);
		}
		
		// 如果當前用戶權限對象為null
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(
					"AbstractSecurityInterceptor.authenticationNotFound",
					"An Authentication object was not found in the SecurityContext"),
					object, attributes);
		}
		Authentication authenticated = authenticateIfRequired();
		// Attempt authorization,此處調用accessDecisionManager 進行鑒權
		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);
		}
	}
	// ... 其他方法略
}

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: