Java教程

Spring Security核心组件之授权

本文主要是介绍Spring Security核心组件之授权,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Security 提供的方式做授权,也可以自定义授权逻辑。一句话,你想怎么玩都可以!

一、 URL层面的授权

1.1 访问控制url的匹配

在配置类中http.authorizeRequests()主要是对url进行控制。配置顺序会影响之后授权的效果,越是具体的应该放在前面,越是笼统的应该放到后面。

anyRequest():表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证,会放在最后。

auth.authorizeRequests()
    .antMatchers("/*/test1").hasAuthority("admin")
    .anyRequest().authenticated();//所有接口都要进行验证

antMatchers:参数是不定向参数,每个参数是一个ant表达式,用于匹配URL规则。

ANT 通配符 说明
?匹配任何单字符
*匹配0或者任意数量的字符
**匹配0或者更多的目录

案例:/demo/test可以直接免认证

auth.authorizeRequests()
    .antMatchers("/demo/test").permitAll()
    .anyRequest().authenticated();//所有请求都要验证

regexMatchers:通过正则表达式 查询路径

auth.authorizeRequests()
    .regexMatchers("spitters/.*").permitAll()
    .anyRequest().authenticated();//所有请求都要验证
1.2 内置授权

Spring Security 匹配了 URL 后调用了 permitAll()表示不需要认证, 随意访问。在 Spring Security 中提供了多种内置控制。

a.直接授权

方法说明
permitAll()所匹配的 URL ,任何人都允许访问
denyAll()所匹配的 URL, 任何人都不允许被访问
authenticated()所匹配的 URL ,任何人 都需要被认证才能访问
anonymous()表示可以匿名访问匹配的 URL。和 permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter 链中
rememberMe()被“remember me”的用户允许访问
fullyAuthenticated()如果用户不是被 remember me ,才可以访问。

anonymous和permitAll的区别
anonymous() :匿名访问,仅允许匿名用户访问,如果登录认证后,带有token信息再去请求,这个anonymous()关联的资源就不能被访问,
permitAll() 登录能访问,不登录也能访问,一般用于静态资源js等

b.权限授权

方法说明
hasAuthority(String authorities)拥有指定权限的用户可以访问
hasAuthority(String…authorities)拥有指定任一权限的用户可访问

c.角色授权

方法说明
hasIpAddress(String ipaddressExpression)指定的ip用户才可以访问
hasRole (String roles)拥有 指定角色的用户可以访问,角色将被增加“ROLE_”前缀
hasAnyRole(String…roles)拥有指定任一角色的用户可访问
1.3 自定义控制
方法说明
access(String attribute)当Spring EL表达式的执行结果为true时,可以访问

我们可以使用SecurityExpressionOperations进行自定义控制

案例:/demo/test接口,只有用户名包含1才有权限访问

@Component
public class MySecurityExpressionOperations  {

    //HttpServletRequest的参数名必须是request
    public boolean hasPermission(HttpServletRequest request, Authentication authentication){
        Object obj = authentication.getPrincipal();
        if(obj instanceof UserDetails){
            UserDetails userDetails = (UserDetails)obj;

            String name = userDetails.getUsername();

            return name.contains("1");
        }
        return false;
    }
}

配置

@Override
    protected void configure(HttpSecurity auth) throws Exception {
       auth.formLogin();                              
       auth.authorizeRequests()
           .antMatchers("/demo/test")
           .access("@mySecurityExpressionOperations.hasPermission(request,authentication)")
           .anyRequest().authenticated();

       auth.csrf().disable();
    }

二、方法层面的授权

springSecurity在方法的权限控制上支持三种类型的注解,JSR-250注解,@secured注解和表达式的注解。这三种注解默认都没有启用的,需要通过@EnableGlobalMethodSecurity来进行启用

这些注解可以写在Service接口或者方法上,也可以写到Controller或者Controller的方法上。通常情况下都是写在控制器方法上,控制接口url是否被访问

2.1 JSR-250注解

JSR-250注解开启方式如下

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
}

@RolesAllowed()

表示访问对应方法时所应具有的角色,其可以标注在类上,也可以标注在方法上,当标注在类上时表示其中所有方法的执行都需要对应的角色,当标注在方法上表示执行该方法时所需要的角色,当方法和类上都使用了@RolesAllowed进行标注,则方法上的@RolesAllowed将覆盖类上的@RolesAllowed,即方法上@RolesAllowed优先级大于类上的。@RolesAllowed的值是由角色名称组成的数组

//访问此接口必须要有admin权限
@GetMapping("test")
@RolesAllowed({"admin"})
public String test1(){
        return "test1";
}

@PermitAll()

表示允许所有的角色进行访问,也就是说不进行权限控制。@PermitAlli可以标注在方法上也可以标注在类上,当标注在方法上时则只对对应方法不进行权限控制,而标注在类上时表示对类里面所有的方法都不进行权限控制。
当@PermitAll标注在类上,而@RolesAllowed标注在方法上时则按照@RolesAllowed将覆盖@PermitAll,即需要@RolesAllowed对应的角色才能访问。
当@RolesAllowed标注在类上,而@PermitAll标注在方法上时则对应的方法也是不进行权限控制的。
当在类和方法上同时使用了@PermitAll和@RolesAllowed时先定义的将发生作用(这个没多大的实际意义,实际应用中不会有这样的定义)。

问题:为什么jsr250规范的@PermitAlli注解在存在java configi配置授权模式的情况下需要认证后访问的问题?
因为会先经过FilterSecurityInterceptori过滤器,利用匿名的认证用户进行投票决策,此时vote返回-1(因为没有匹配到当前url,只能匹配authenticated0),默认AffirmativeBased决策下就会直接抛出AccessDeniedException,跳转到认证界面。此时就不会进入到
MethodSecurityInterceptor的判断逻辑,必须认证之后才行。
所以基于方法注解的@PermitAll配置正常使用需要在不被FilterSecurityInterceptor拦截的情况下使用,也就不能在WebSecurityConfig中配置http.authorizeRequests(.anyRequest).authenticated0

@DenyAll()

是和PermitAll相反的,表示无论什么角色都不能访问。@DenyAll只能定义在方法上。你可能会有疑问使用@DenyAllt标注的方法无论拥有什么权限都不能访问,那还定义它干啥呢?使用@DenyAll定义的方法只是在我们的权限控制中不能访问,脱离了权限控制还是可以访问的。

2.2 @Secured注解

@Secured是由Spring Security定义的用来支持方法权限控制的注解。它的使用也是需要启用对应的支持才会生效的。@Secured是专门用于判断是否具有角色的,能写在方法或类上。参数要以ROLE开头。

@Secured注解开启方式如下

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
}

案例: 访问/demo/test/需要用户有admin权限

@RestController
@RequestMapping("demo")
public class DemoController {

    //访问此接口必须要有admin权限
    @GetMapping("test")
    @Secured("ROLE_admin")
    public String test1(){
        return "test1";
    }
}
2.2 支持表达式的注解

Spring Security中定义了四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。

支持表达式的注解开启方式如下

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
方法说明
@PreAuthorize控制一个方法是否能够被调用,执行方法之前先判断权限
@PostAuthorize控制一个方法是否能够被调用,执行方法之后先判断权限
@Service
public class HelloService {

    //方法执行前,只有当前登录用户名为 javaboy 的用户才可以访问该方法
    @PreAuthorize("principal.username.equals('javaboy')")
    public String hello() {
        return "hello";
    }
    
   //表示访问该方法的 age 参数必须大于 98,
    @PostAuthorize("#age>98")
    public String getAge(Integer age) {
        return String.valueOf(age);
    }
}
方法说明
@PreFilter对入参进行过滤
@PostFilter对返回值进行过滤
//对集合进行过滤,只返回后缀为 2 的元素,filterObject 表示要过滤的元素对象。
@PostFilter("filterObject.lastIndexOf('2')!=-1")
public List<String> getAllUser() {
    List<String> users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        users.add("javaboy:" + i);
    }
    return users;
}
//由于有两个集合,因此使用 filterTarget 指定过滤对象
@PreFilter(filterTarget = "ages",value = "filterObject%2==0")
public void getAllAge(List<Integer> ages,List<String> users) {
    System.out.println("ages = " + ages);
    System.out.println("users = " + users);
}

ExceptionTranslationFilter异常转换器位于整个springSecurityFilterChain的后方
,用来转换整个链路中出现的异常。此过滤器本身不处理异常。而是将认证过程中出现的异常交给
内部维护的一些类去处理,一般处理两大异常:AccessDeniedException访问异常和
AuthenticationException认证异常

三、pringSecurity授权原理

​FilterSecurityInterceptor 作为 Spring Security Filter Chain 的最后一个 Filter,承担着非常重要的作用。如获取当前 request 对应的权限配置,调用访问控制器进行鉴权操作等,都是核心功能。

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    
    //忽略代码......
    
	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 {
		if ((fi.getRequest() != null)&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// 过滤器已应用于此请求,用户希望我们观察
			// 每个请求处理一次,因此不要重新进行安全检查
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// 第一次调用此请求时,请执行安全检查
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
			//核心逻辑,调用的是父类AbstractSecurityInterceptor的方法
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}

			super.afterInvocation(token, null);
		}
	}

	 //忽略代码......
}

AbstractSecurityInterceptor#beforeInvocation

public abstract class AbstractSecurityInterceptor implements InitializingBean,
		ApplicationEventPublisherAware, MessageSourceAware {

	protected InterceptorStatusToken beforeInvocation(Object object) {
        //获取当前请求的权限限制
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
        //如果没有权限限制
		if (attributes == null || attributes.isEmpty()) {
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException("");
			}
			if (debug) {
				logger.debug("Public object - authentication not attempted");
			}
			publishEvent(new PublicInvocationEvent(object));
			return null; // 调用后没有进一步的工作
		}

		//如果有权限限制,执行下面代码

		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(""),object, attributes);
		}
        
        //获取当前登陆用户的认证信息
		Authentication authenticated = authenticateIfRequired();

		
		try {
		   //开始尝试授权,调用的是权限管理器的方法
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

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

		// 尝试以其他用户身份运行
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);

		if (runAs == null) {
			// 调用后没有进一步的工作
			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);
		}
		else {
			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);
			// 需要恢复到令牌。认证后调用
			return new InterceptorStatusToken(origCtx, true, attributes, object);
		}
	}
}

从上面 我们可以知道springSecurity授权核心在于权限投票器

3.1 权限投票器

AccessDecisionVoter 投票器顶级接口,负责对授权决策进行表决。然后,最终由唱票者AccessDecisionManager 统计所有的投票器表决后,来做最终的授权决策。

public interface AccessDecisionVoter<S> {
	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;
	boolean supports(ConfigAttribute attribute);
	boolean supports(Class<?> clazz);
	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);
}

在这里插入图片描述

WebExpressionVoter,最常用的,也是 Spring Security 框架默认 FilterSecurityInterceptor 实例中 AccessDecisionManager 默认的投票器 WebExpressionVoter。其实,就是对使用 http.authorizeRequests() 基于 Spring-EL进行控制权限的的授权决策类

AuthenticatedVoter,针对 ConfigAttribute#getAttribute() 中配置为 IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY 权限标识时的授权决策。因此,其投票策略比较简单:

PreInvocationAuthorizationAdviceVoter,用于处理基于注解 @PreFilter 和 @PreAuthorize 生成的 PreInvocationAuthorizationAdvice,来处理授权决策的实现。还记得我们最早使用 @PreAuthorize 来进行权限控制的介绍吗?

RoleVoter,角色投票器。用于 ConfigAttribute#getAttribute() 中配置为角色的授权决策。其默认前缀为 ROLE_,可以自定义,也可以设置为空,直接使用角色标识进行判断。这就意味着,任何属性都可以使用该投票器投票,也就偏离了该投票器的本意,是不可取的。

RoleHierarchyVoter基于 RoleVoter,唯一的不同就是该投票器中的角色是附带上下级关系的。也就是说,角色A包含角色B,角色B包含 角色C,此时,如果用户拥有角色A,那么理论上可以同时拥有角色B、角色C的全部资源访问权限。

Jsr250Voter,JSR-250配置属性上的投票者。

springSecurity授权核心原理在AccessDecisionManager的decide方法中

3.2 决策管理器

AccessDecisionManager 顾名思义,访问决策管理器。

public interface AccessDecisionManager {
	void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
			InsufficientAuthenticationException;
	boolean supports(ConfigAttribute attribute);
	boolean supports(Class<?> clazz);
}

在这里插入图片描述

Spring security默认使用的是AffirmativeBased

AffirmativeBased的逻辑是:
(1)只要有AccessDecisionVoterE的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException.。

public class AffirmativeBased extends AbstractAccessDecisionManager {

	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);
			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED://1
				return;
			case AccessDecisionVoter.ACCESS_DENIED://-1
				deny++;
				break;
			default:
				break;
			}
		}
		if (deny > 0) {throw new AccessDeniedException(messages.getMessage(""));}
		// 为了走到这一步,每一位参与决策的选民都投了弃权票
		checkAllowIfAllAbstainDecisions();
	}
}

ConsensusBased的逻辑是:
(1)如果赞成票多于反对票则表示通过。
(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。
(3)如果赞成票与反对票相同且不等于0,并且属性allowlfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。

public class ConsensusBased extends AbstractAccessDecisionManager {
	
	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int grant = 0;//赞成票数
		int deny = 0;//反对票数
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);
			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED://1
				grant++;
				break;

			case AccessDecisionVoter.ACCESS_DENIED://-1
				deny++;
				break;

			default:
				break;
			}
		}

		if (grant > deny) {return;}//赞成票多于反对票,通过
        //反对票多于赞成票,抛出AccessDeniedException
		if (deny > grant) {throw new AccessDeniedException(messages.getMessage(""));}

		if ((grant == deny) && (grant != 0)) {
			if (this.allowIfEqualGrantedDeniedDecisions) {
				return;
			}
			else {
				throw new AccessDeniedException(messages.getMessage(""));
			}
		}
		// 弃权票
		checkAllowIfAllAbstainDecisions();
	}
}

UnanimousBased的逻辑是:
(1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException.
(2)如果没有反对票,但是有赞成票,则表示通过。
(3)如果全部弃权了,则将视参数allowlfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException.

public class UnanimousBased extends AbstractAccessDecisionManager {

	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> attributes) throws AccessDeniedException {
		
		int grant = 0;//赞成票数
		List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
		singleAttributeList.add(null);

		for (ConfigAttribute attribute : attributes) {
			singleAttributeList.set(0, attribute);

			for (AccessDecisionVoter voter : getDecisionVoters()) {
				int result = voter.vote(authentication, object, singleAttributeList);
				switch (result) {
				case AccessDecisionVoter.ACCESS_GRANTED:
					grant++;

					break;

				case AccessDecisionVoter.ACCESS_DENIED:
					throw new AccessDeniedException(messages.getMessage(""));

				default:
					break;
				}
			}
		}
		// To get this far, there were no deny votes
		if (grant > 0) {
			return;
		}
		// 弃权
		checkAllowIfAllAbstainDecisions();
	}
}
这篇关于Spring Security核心组件之授权的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!