springboot+springsecurity如何實(shí)現(xiàn)動(dòng)態(tài)url細(xì)粒度權(quán)限認(rèn)證
謹(jǐn)記:Url表只儲(chǔ)存受保護(hù)的資源,不在表里的資源說(shuō)明不受保護(hù),任何人都可以訪問(wèn)
1、MyFilterInvocationSecurityMetadataSource 類判斷該訪問(wèn)路徑是否被保護(hù)
@Component
//用于設(shè)置受保護(hù)資源的權(quán)限信息的數(shù)據(jù)源
public class MyFilterInvocationSecurityMetadataSource implements
FilterInvocationSecurityMetadataSource {
@Bean
public AntPathMatcher getAntPathMatcher(){
return new AntPathMatcher();
}
@Autowired
//獲取數(shù)據(jù)庫(kù)中的保存的url Url表只儲(chǔ)存受保護(hù)的資源,不在表里的資源說(shuō)明不受保護(hù),任何人都可以訪問(wèn)
private RightsMapper rightsMapper;
@Autowired
private AntPathMatcher antPathMatcher;
@Override
/*
* @param 被調(diào)用的保護(hù)資源
* @return 返回能夠訪問(wèn)該保護(hù)資源的角色集合,如果沒(méi)有,則應(yīng)返回空集合。
*/
public Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
//獲取用戶請(qǐng)求的Url
String url = fi.getRequestUrl();
//先到數(shù)據(jù)庫(kù)獲取受權(quán)限控制的Url
List<Rights> us = rightsMapper.queryAll();
//用于儲(chǔ)存用戶請(qǐng)求的Url能夠訪問(wèn)的角色
Collection<ConfigAttribute> rs=new ArrayList<ConfigAttribute>();
for(Rights u:us){
if (u.getUrl() != null) {
//逐一判斷用戶請(qǐng)求的Url是否和數(shù)據(jù)庫(kù)中受權(quán)限控制的Url有匹配的
if (antPathMatcher.match(u.getUrl(), url)) {
//如果有則將可以訪問(wèn)該Url的角色儲(chǔ)存到Collection<ConfigAttribute>
rs.add(rightsMapper.queryById(u.getId()));
}
}
}
if(rs.size()>0) {
return rs;
}
//沒(méi)有匹配到,就說(shuō)明此資源沒(méi)有被控制,所有人都可以訪問(wèn),返回null即可,返回null則不會(huì)進(jìn)入之后的decide方法
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
// TODO 自動(dòng)生成的方法存根
return null;
}
@Override
public boolean supports(Class<?> clazz) {
// TODO 自動(dòng)生成的方法存根
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
rights表中的部分內(nèi)容:
表結(jié)構(gòu)

內(nèi)容:

2、MyAccessDecisionManager 類判斷該用戶是否有權(quán)限訪問(wèn)
@Component
//用于設(shè)置判斷當(dāng)前用戶是否可以訪問(wèn)被保護(hù)資源的邏輯
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
/*
* @param 請(qǐng)求該保護(hù)資源的用戶對(duì)象
* @param 被調(diào)用的保護(hù)資源
* @param 有權(quán)限調(diào)用該資源的集合
*/
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
Iterator<ConfigAttribute> ite = configAttributes.iterator();
//遍歷configAttributes,查看當(dāng)前用戶是否有對(duì)應(yīng)的權(quán)限訪問(wèn)該保護(hù)資源
while (ite.hasNext()) {
ConfigAttribute ca = ite.next();
String needRole = ca.getAttribute();
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (ga.getAuthority().equals(needRole)) {
// 匹配到有對(duì)應(yīng)角色,則允許通過(guò)
return;
}
}
}
// 該url有配置權(quán)限,但是當(dāng)前登錄用戶沒(méi)有匹配到對(duì)應(yīng)權(quán)限,則禁止訪問(wèn)
throw new AccessDeniedException("not allow");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
3、在SecurityConfig 類中配置說(shuō)明
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService myUserDetailsService;
@Autowired
private SendSmsSecurityConfig sendSmsSecurityConfig;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
//加密機(jī)制
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 不加密
}
//認(rèn)證
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()//對(duì)請(qǐng)求授權(quán)
.antMatchers("/**").permitAll()
.anyRequest()//任何請(qǐng)求
.authenticated()//登錄后訪問(wèn)
.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解決公共接口自定義權(quán)限驗(yàn)證失效問(wèn)題,和源碼分析
背景:
自定義權(quán)限認(rèn)證,一部分接口必須要有相應(yīng)的角色權(quán)限,一部分接口面向所有訪問(wèn)者,一部分接口任何人都不能訪問(wèn)。但是在使用 SpringSecurity的過(guò)程中發(fā)現(xiàn),框架會(huì)將沒(méi)有指定角色列表的URL資源直接放行,不做攔截。
用戶登錄認(rèn)證成功后,攜帶Token訪問(wèn)URL資源,spring security 根據(jù)Token(請(qǐng)求頭Authorization中)來(lái)分辨不同用戶。
用戶權(quán)限數(shù)據(jù)源是一個(gè)Map:以 URL資源為Key,以有權(quán)訪問(wèn)的Key的角色列表為Value。
使用時(shí)發(fā)現(xiàn)當(dāng)一個(gè)接口有Key,但是Value為空或null時(shí),spring security 框架自動(dòng)放行,導(dǎo)致了權(quán)限失效問(wèn)題。
解決方法有兩種:
第一種方法:
默認(rèn)rejectPublicInvocations為false。
對(duì)需要控制權(quán)限的URL資源添加標(biāo)志,以防止roleList為空,跳過(guò)了權(quán)限驗(yàn)證.
公共權(quán)限設(shè)置為null,不進(jìn)行權(quán)限驗(yàn)證
第二種方法:
配置rejectPublicInvocations為true
此后roleList為空,或者沒(méi)有找到URL資源時(shí),都為拒絕訪問(wèn)
需要控制權(quán)限的URL資源,即使對(duì)應(yīng)角色為空,也會(huì)進(jìn)行權(quán)限驗(yàn)證
公共權(quán)限設(shè)置為所有角色和匿名角色,不進(jìn)行權(quán)限驗(yàn)證
package org.springframework.security.access.intercept;
/**
* 對(duì)安全對(duì)象(訪問(wèn)請(qǐng)求+用戶主體)攔截的抽象類源碼
*/
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());
}
// 從權(quán)限數(shù)據(jù)源獲取了當(dāng)前 <URL資源> 對(duì)應(yīng)的 <角色列表>
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 框架在此處判斷URL資源對(duì)應(yīng)的角色列表是否為空
if (attributes == null || attributes.isEmpty()) {
// rejectPublicInvocations默認(rèn)為false
// 可以配置為true,即角色列表為空的時(shí)候不進(jìn)行放行
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);
}
// 如果當(dāng)前用戶權(quán)限對(duì)象為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,此處調(diào)用accessDecisionManager 進(jìn)行鑒權(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,這里可以另外配置或修改用戶的權(quán)限對(duì)象,特殊場(chǎng)景使用
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);
}
}
// ... 其他方法略
}
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring中使用自定義ThreadLocal存儲(chǔ)導(dǎo)致的坑及解決
這篇文章主要介紹了Spring中使用自定義ThreadLocal存儲(chǔ)導(dǎo)致的坑及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
SpringBoot Starter機(jī)制及整合tomcat的實(shí)現(xiàn)詳解
這篇文章主要介紹了SpringBoot Starter機(jī)制及整合tomcat的實(shí)現(xiàn),我們知道SpringBoot自己在“后臺(tái)”幫我們配置了很多原本需要我們手動(dòng)去的東西,至于這個(gè)“后臺(tái)”是啥,就是Starter機(jī)制2022-09-09
MyBatis查詢時(shí)屬性名和字段名不一致問(wèn)題的解決方法
這篇文章主要給大家介紹了關(guān)于MyBatis查詢時(shí)屬性名和字段名不一致問(wèn)題的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
如何使用會(huì)話Cookie和Java實(shí)現(xiàn)JWT身份驗(yàn)證
這篇文章主要介紹了如何使用會(huì)話Cookie和Java實(shí)現(xiàn)JWT身份驗(yàn)證,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2021-03-03
Java Socket設(shè)置timeout的幾種常用方式說(shuō)明
這篇文章主要介紹了Java Socket設(shè)置timeout的幾種常用方式說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
Spring-AOP-ProceedingJoinPoint的使用詳解
這篇文章主要介紹了Spring-AOP-ProceedingJoinPoint的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
Spring-基于Spring使用自定義注解及Aspect實(shí)現(xiàn)數(shù)據(jù)庫(kù)切換操作
這篇文章主要介紹了Spring-基于Spring使用自定義注解及Aspect實(shí)現(xiàn)數(shù)據(jù)庫(kù)切換操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
Springboot RestTemplate設(shè)置超時(shí)時(shí)間的方法(Spring boot
這篇文章主要介紹了Springboot RestTemplate設(shè)置超時(shí)時(shí)間的方法,包括Spring boot 版本<=1.3和Spring boot 版本>=1.4,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08
SpringBoot整合ELK實(shí)現(xiàn)日志監(jiān)控
這篇文章主要為大家詳細(xì)介紹了SpringBoot整合ELK實(shí)現(xiàn)日志監(jiān)控的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-11-11

