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);
}
三、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();
}
}