Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Security 提供的方式做授权,也可以自定义授权逻辑。一句话,你想怎么玩都可以!
在配置类中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();//所有请求都要验证
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) | 拥有指定任一角色的用户可访问 |
方法 | 说明 |
---|---|
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是否被访问
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定义的方法只是在我们的权限控制中不能访问,脱离了权限控制还是可以访问的。
@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"; } }
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认证异常
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授权核心在于权限投票器
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方法中
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(); } }