Spring security权限管理

权限管理

1 什么是权限管理

Spring security支持多种不同的认证方式,但是无论开发者使用哪种认证方式,都不会影响授权功能的使用,spring security很好地实现了认证和授权两大功能的解耦。

2 Spring security权限管理策略

从技术上来说,spring security中提供的权限管理功能主要有两种类型:

FilterSecurityInterceptor
MethodSecurityInterceptor

基于过滤器的权限管理主要用来拦截HTTP请求,拦截下来之后,根据HTTP请求地址进行权限校验。

基于AOP的权限管理则主要用来处理方法级别的权限问题。当需要调用某一个方法时,通过AOP将操作拦截下来,然后判断用户是否具备相关的权限,如果具备,则允许方法调用;否则禁止方法调用。

3 核心概念

3.1 角色与权限

在spring security中,当用户登录成功后,当前登录用户信息将保存在 Authentication 对象中,该对象中有一个 getAuthorities 方法,用来返回当前对象所具备的权限信息,也就是已经授予当前登录用户的权限, getAuthorities 方法返回值是 Collection<? extends GrantedAuthority> ,即集合中存放的是 GrantedAuthority 的子类,当需要进行权限判断的时候,就会调用该方法获取用户的权限,进而做出判断。

无论用户的认证方式是用户名/密码形式、remember-me形式,还是其他如CAS、OAuth2等认证方式,最终用户的权限信息都可以通过 getAuthorities 方法获取。

那么对于 Authentication#getAuthorities 方法的返回值,应该如何理解:

  • 从设计层面来讲,角色和权限是两个完全不同的东西:权限就是一些具体的操作,例如针对员工数据的读权限( READ_EMPLOYEE )和针对员工数据的写权限( WRITE_EMPLOYEE );角色则是某些权限的集合,例如管理员角色 ROLE_ADMIN 、普通用户角色 ROLE_USER
  • 从代码层面来讲,角色和权限并没有太大的不同,特别是在spring security中,角色和权限的处理的方式基本上是一样的,唯一的区别在于spring security在多个地方会自动给角色添加一个 ROLE_ 前缀,而权限则不会自动添加任何前缀。

至于 Authentication#getAuthorities 方法的返回值,则要分情况来对待:

  • 如果权限系统设计比较简单,就是 用户<=>权限<=>资源 三者之间的关系,那么 getAuthorities 方法的含义就很明确,就是返回用户的权限。
  • 如果权限系统设计比较复杂,同时存在角色和权限的概念,如 用户<=>角色<=>权限<=>资源 (用户关联角色、角色关联权限、权限关联资源),此时可以将 getAuthorities 方法的返回值当做权限来理解。由于spring security并未提供相关的角色类,因此这个时候需要自定义角色类。

对于第一种情况,相对来说比较好理解,这里简单介绍一下第二种情况。

如果系统同时存在角色和权限,可以使用 GrantedAuthority 的实现类 SimpleGrantedAuthority 来表示一个权限,在 SimpleGrantedAuthority 类中,可以将权限描述为一个字符串,如 READ_EMPLOYEEWRITE_EMPLOYEE 。据此,定义角色类如下:

public class Role implements GrantedAuthority {
	private String name;
	private List allowedOperations = new ArrayList<>();

	@Override
	public String getAuthority() {
		return name;
	}
	
	// 省略getter/setter
}

角色继承自 GrantedAuthority ,一个角色对应多个权限。然后在定义用户类的时候,将角色转换为权限即可:

public class User implements UserDetails {
	private List roles = new ArrayList<>();

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List authorities= new ArrayList<>();
	
		for (Role role : roles) {
			authorities.addAll(role.getAllowedOperations());
		}
		
		return authorities.stream().distinct().collect(Collectors.toList());
	}

	// 省略getter/setter
}

整体上来说,设计层面上,角色和权限是两个东西;代码层面上,角色和权限其实差别不大,注意区分即可。

3.2 角色继承

角色继承就是指角色存在一个上下级的关系,例如 ROLE_ADMIN 继承自 ROLE_USER ,那么 ROLE_ADMIN 就自动具备 ROLE_USER 的所有权限。

Spring security中通过 RoleHierarchy 类对角色继承提供支持:

public interface RoleHierarchy {
	/**
	 * 该方法返回用户真正可触达的权限。例如,假设用户定义了ROLE_ADMIN继承自ROLE_USER,ROLE_USER继承自ROLE_GUEST,
	 * 现在当前用户角色是ROLE_ADMIN,但是它实际可访问的资源也包含ROLE_USER和ROLE_GUEST能访问的资源。该方法就是根据
	 * 当前用户所具有的角色,从角色层级映射中解析出用户真正可触达的权限。
	 */
	Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
			Collection<? extends GrantedAuthority> authorities);

}

RoleHierarchy 只有一个实现类 RoleHierarchyImpl ,开发者一般通过 RoleHierarchyImpl 类来定义角色的层级关系:

@Bean
RoleHierarchy roleHierarchy() {
	RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
	// ROLE_A继承自ROLE_B,ROLE_B继承自ROLE_C,ROLE_C继承自ROLE_D
	hierarchy.setHierarchy("ROLE_A > ROLE_B > ROLE_C > ROLE_D");
	return hierarchy;
}

这样的角色层级,在 RoleHierarchyImpl 类中首先通过 buildRolesReachableInOneStepMap 方法解析成 Map 集合:

ROLE_A -> ROLE_B
ROLE_B -> ROLE_C
ROLE_C -> ROLE_D

然后再通过 buildRolesReachableInOneOrMoreStepsMap 方法对上面的集合再次解析,最终解析结果如下:

ROLE_A -> [ROLE_B, ROLE_C, ROLE_D]
ROLE_B -> [ROLE_C, ROLE_D]
ROLE_C -> ROLE_D

最后通过 getReachableGrantedAuthorities 方法从该 Map 集合中获取用户真正可触达的权限。

3.3 两种处理器

基于过滤器的权限管理( FilterSecurityInterceptor )和基于AOP的权限管理( MethodSecurityInterceptor ),无论是哪种,都涉及一个前置处理器和后置处理器。

在基于过滤器的权限管理中,请求首先到达过滤器 FilterSecurityInterceptor ,在其执行过程中,首先会由前置处理器去判断发起当前请求的用户是否具备相应的权限,如果具备,则请求继续走下去,到达目标方法并执行完毕。在响应时,又会经过 FilterSecurityInterceptor 过滤器,此时由后置处理器再去完成其他收尾工作。在基于过滤器的权限管理中,后置处理器一般是不工作的。这也很好理解,因为基于过滤器的权限管理,实际上就是拦截请求URL地址,这种权限管理方式粒度较粗,而且过滤器中拿到的是响应的 HttpServletResponse 对象,对其所返回的数据做二次处理并不方便。

在基于方法的权限管理中,目标方法的调用会被 MethodSecurityInterceptor 拦截下来,实现原理当然就是大家所熟知的AOP机制。当目标方法的调用被 MethodSecurityInterceptor 拦截下之后,在其 invoke 方法中首先会由前置处理器去判断当前用户是否具备调用目标方法所需要的权限,如果具备,则继续执行目标方法。当目标方法执行完毕并给出返回结果后,在 MethodSecurityInterceptor#invoke 方法中,由后置处理器再去对目标方法的返回结果进行过滤或者鉴权,然后在 invoke 方法中将处理后的结果返回。

3.4 前置处理器

要理解前置处理器,需要先了解投票器。

投票器

投票器是spring security权限管理功能中的一个组件,顾名思义,投票器的作用就是针对是否允许某一个操作进行投票。当请求的URL地址被拦截下来之后,或者当调用的方法被AOP拦截下来之后,都会调用投票器对当前操作进行投票,以便决定是否允许当前操作。

在spring security中,投票器由 AccessDecisionVoter 来定义:

public interface AccessDecisionVoter {
	// 表示投票通过
	int ACCESS_GRANTED = 1;
	// 表示弃权
	int ACCESS_ABSTAIN = 0;
	// 表示拒绝
	int ACCESS_DENIED = -1;
	// 用来判断是否支持处理ConfigAttribute对象
	boolean supports(ConfigAttribute attribute);
	// 用来判断是否支持处理受保护的安全对象
	boolean supports(Class<?> clazz);
	/**
	 * 具体的投票方法,根据用户所具有的权限以及当前请求需要的权限进行投票。
	 * @param authentication 进行调用的调用者,可以提取出来当前用户所具备的权限
	 * @param object 受保护的安全对象,如果受保护的是URL地址,则object就是一个FilterInvocation对象;如果受保护的是一个
	 * 方法,则object就是一个MethodInvocation对象
	 * @param attributes 访问受保护对象所需要的权限
	 * @return 定义的三个常量之一
	 */
	int vote(Authentication authentication, S object, Collection attributes);
}

Spring security中为 AccessDecisionVoter 提供了诸多不同的实现类:

  • RoleVoterRoleVoter 是根据登录主体的角色进行投票,即判断当前用户是否具备受保护对象所需要的角色。需要注意的是,默认情况下角色需以 ROLE_ 开始,否则 supports 方法直接返回 false ,不进行后续的投票操作。
  • RoleHierarchyVoterRoleHierarchyVoter 继承自 RoleVoter ,投票逻辑和 RoleVoter 一致,不同的是, RoleHierarchyVoter 支持角色的继承,它通过 RoleHierarchyImpl 对象对用户所具有的角色进行解析,获取用户真正可触达的角色;而 RoleVoter 则直接调用 authentication.getAuthorities() 方法获取用户的角色。
  • WebExpressionVoter :基于URL地址进行权限控制时的投票器(支持SpEL)。
  • Jsr250Voter :处理JSR-250权限注解的投票器,如 @PermitAll@DenyAll 等。
  • AuthenticatedVoterAuthenticatedVoter 用于判断当前用户的认证形式,它有三种取值: IS_AUTHENTICATED_FULLYIS_AUTHENTICATED_REMEMBEREDIS_AUTHENTICATED_ANONYMOUSLY 。其中, IS_AUTHENTICATED_FULLY 要求当前用户既不是匿名用户也不是通过remember-me进行认证; IS_AUTHENTICATED_REMEMBERED 则在前者的基础上,允许用户通过remember-me进行认证; IS_AUTHENTICATED_ANONYMOUSLY 则允许当前用户通过remember-me进行认证,也允许当前用户是匿名用户。
  • AbstractAclVoter :基于ACL进行权限控制时的投票器。这是一个抽象类,没有绑定到具体的ACL系统。
  • PreInvocationAuthorizationAdviceVoter :处理 @PreFilter@PreAuthorize 注解的投票器。

这些投票器在具体使用中,可以单独使用一个,也可以多个一起使用。如果上面这些投票器都无法满足需求,开发者也可以自定义投票器。需要注意的是,投票结果并非最终结果(通过或拒绝),最终结果还是要看决策器( AccessDecisionManager )。

决策器

决策器由 AccessDecisionManager 负责, AccessDecisionManager 会同时管理多个投票器,由 AccessDecisionManager 调用投票器进行投票,然后根据投票结果做出相应的决策,所以将 AccessDecisionManager 也称作是一个决策管理器:

public interface AccessDecisionManager {
	// 核心的决策方法,在这个方法中判断是否允许当前URL或者方法的调用,如果不允许,则抛出AccessDeniedException异常
	void decide(Authentication authentication, Object object, Collection configAttributes)
			throws AccessDeniedException, InsufficientAuthenticationException;
	// 用来判断是否支持处理ConfigAttribute对象
	boolean supports(ConfigAttribute attribute);
	// 用来判断是否支持当前安全对象
	boolean supports(Class<?> clazz);
}

可以看出, AccessDecisionManager 有一个实现类 AbstractAccessDecisionManager ,一个 AbstractAccessDecisionManager 对应多个投票器。多个投票器针对同一个请求可能会给出不同的结果,那么听谁的呢,这就要看决策器了。

  • AffirmativeBased :一票通过机制,即只要有一个投票器通过就可以访问(默认即此)。
  • UnanimousBased :一票否决机制,即只要有一个投票器反对就不可以访问。
  • ConsensusBased :少数服从多数机制。如果是平局并且至少有一张赞同票,则根据 allowIfEqualGrantedDeniedDecisions 参数的取值来决定,如果该参数的取值为 true ,则可以访问,否则不可以访问。

如果这三个决策器无法满足需求,开发者也可以自定义类继承自 AbstractAccessDecisionManager 实现自己的决策器。

这就是前置处理器中的大致逻辑,无论是基于URL地址的权限管理,还是基于方法的权限管理,都是在前置处理器中通过 AccessDecisionManager 调用 AccessDecisionVoter 进行投票,进而做出相应的决策。

3.5 后置处理器

后置处理器一般只在基于方法的权限控制中会用到,当目标方法执行完毕后,通过后置处理器可以对目标方法的返回值进行权限校验或者过滤。

后期处理器由 AfterInvocationManager 负责:

// 和AccessDecisionManager高度相似
public interface AfterInvocationManager {
	/**
	 * 主要的区别在于decide方法的参数和返回值。当后置处理器执行时,被权限保护的方法以及执行完毕,后置处理器主要是对执行的结果
	 * 进行过滤,所以decide方法中有一个returnedObject参数,这就是目标方法的执行结果,decide方法的返回值就是对returnedObject
	 * 对象进行过滤/鉴权后的结果
	 */
	Object decide(Authentication authentication, Object object, Collection attributes,
			Object returnedObject) throws AccessDeniedException;

	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);
}

AuthenticationManagerProviderManager 以及 AuthenticationProvider 相似。

AfterInvocationManager 只有一个实现类 AfterInvocationProviderManager ,一个 AfterInvocationProviderManager 关联多个 AfterInvocationProvider 。在 AfterInvocationManagerdecide 以及 supports 方法执行时,都是遍历 AfterInvocationProvider 并执行它里边对应的方法。

AfterInvocationProvider 有多个不同的实现类,常见到的是 PostInvocationAdviceProvider ,该类主要用来处理 @PostAuthorize@PostFilter 注解配置的过滤器。

3.6 权限元数据

ConfigAttribute

在投票器具体的投票方法 vote 中,受保护对象所需要的权限保存在一个 Collection 集合中,集合中的对象是 ConfigAttribute ,而不是所熟知的 GrantedAuthority

ConfigAttribute 用来存储与安全系统相关的配置属性,也就是系统关于权限的配置,通过 ConfigAttribute 来存储:

public interface ConfigAttribute extends Serializable {
	String getAttribute();
}

该接口只有一个 getAttribute 方法返回具体的权限字符串,而 GrantedAuthority 中则是通过 getAuthority 方法返回用户所具有的权限,两者返回值都是字符串。所以虽然是 ConfigAttributeGrantedAuthority 两个不同的对象,但是最终是可以比较的。

  • WebExpressionConfigAttribute :如果用户是基于URL地址来控制权限并且支持SpEL,那么默认配置的权限控制表达式最终会被封装为 WebExpressionConfigAttribute 对象。
  • SecurityConfig :如果用户使用了 @Secured 注解来控制权限,那么配置的权限就会被封装为 SecurityConfig 对象。
  • Jsr250SecurityConfig :如果用户使用了JSR-250相关的注解来控制权限(如 @PermitAll@DenyAll ),那么配置的权限就会被封装为 Jsr250SecurityConfig 对象。
  • PreInvocationExpressionAttribute :如果用户使用了 @PreAuthorize@PreFilter 注解来控制权限,那么相关的配置就会被封装为 PreInvocationExpressionAttribute 对象。
  • PostInvocationExpressionAttribute :如果用户使用了 @PostAuthorize@PostFilter 注解来控制权限,那么相关的配置就会被封装为 PostInvocationExpressionAttribute 对象。

SecurityMetadataSource

当投票器在投票时,需要两方面的权限:其一是当前用户具备哪些权限;其二是当前访问的URL或者方法需要哪些权限才能访问。投票器所做的事情就是对这两种权限进行比较。

用户具备的权限保存在 authentication 中,而当前访问的URL或者方法所需要的权限和 SecurityMetadataSource 有关。

SecurityMetadataSource 所做的事情,就是提供受保护对象所需要的权限。例如,用户访问了一个URL地址,访问该URL地址所需要的权限就由 SecurityMetadataSource 来提供。

public interface SecurityMetadataSource extends AopInfrastructureBean {
	/**
	 * 根据传入的安全对象参数返回其所需要的权限。如果受保护的对象是一个URL地址,那么传入的参数object就是一个FilterInvocation
	 * 对象;如果受保护的是一个方法,那么object就是一个MethodInvocation对象。
	 */
	Collection getAttributes(Object object) throws IllegalArgumentException;
	// 返回所有的角色/权限,以便验证是否支持。不过这个方法不是必须的,也可以直接返回null
	Collection getAllConfigAttributes();
	// 返回当前的SecurityMetadataSource是否支持受保护的对象如FilterInvocation或者MethodInvocation
	boolean supports(Class<?> clazz);
}

可以看到,直接继承自 SecurityMetadataSource 的接口主要有两个: FilterInvocationSecurityMetadataSourceMethodSecurityMetadataSource

FilterInvocationSecurityMetadataSource
MethodSecurityMetadataSource

FilterInvocationSecurityMetadataSource 有一个子类 DefaultFilterInvocationSecurityMetadataSource ,该类中定义了一个如下格式的 Map 集合:

private final Map> requestMap;

可以看到,在这个 Map 集合中, key 是一个请求匹配器, value 则是一个权限集合,也就是说, requestMap 中保存了请求URL和其所需权限之间的映射关系。在spring security中,如果直接在 configure(HttpSecurity) 方法中配置URL请求地址拦截:

http.autorizeRequests()
	// 访问/admin/**格式的URL地址需要admin角色
	.antMatchers("/admin/**").hasRole("admin")
	// 访问/user/**格式的URL地址需要user角色
	.antMatchers("/user/**").access("hasRole('user')")
	// 其余地址认证后即可访问
	.anyRequest().access("isAuthenticated()")

这段请求和权限之间的映射关系,会经过 DefaultFilterInvocationSecurityMetadataSource 的子类 ExpressionBasedFilterInvocationSecurityMetadataSource 进行处理,并最终将映射关系保存到 requestMap 变量中,以备后续使用。

在实际开发中,URL地址以及访问它所需要的权限可能保存在数据库中,此时可以自定义类实现 FilterInvocationSecurityMetadataSource 接口,然后重写 getAttributes 方法,在该方法中,根据当前请求的URL地址去数据库中查询其所需要的权限,然后将查询结果封装为相应的 ConfigAttribute 集合返回即可。

如果是基于方法的权限管理,那么对应的 MethodSecurityMetadataSource 实现类就比较多了:

  • PrePostAnnotationSecurityMetadataSource@PreAuthorize@PreFilter@PostAuthorize@PostFilter 四个注解所标记的权限规则,将由该类负责提供。
  • SecuredAnnotationSecurityMetadataSource@Secured 注解所标记的权限规则,将由该类负责提供。
  • MapBasedMethodSecurityMetadataSource :基于XML文件配置的方法权限拦截规则(基于 sec:protect 节点),将由该类负责提供。
  • Jsr250MethodSecurityMetadataSource :JSR-250相关的注解(如 @PermitAll@DenyAll )所标记的权限规则,将由该类负责提供。

3.7 权限表达式

可以在请求的URL或者访问的方法上,通过SpEL来配置需要的权限。内置的权限表达式:

配置类名称

作用

hasRole(String role)

当前用户是否具备指定角色

hasAnyRole(String... roles)

当前用户是否具备指定角色中的任意一个

hasAuthority(String authority)

当前用户是否具备指定的权限

hasAnyAuthority(String... authorities)

当前用户是否具备指定权限中的任意一个

principal

代表当前登录主体 Principal

authentication

这个是从 SecurityContext 中获取到的 Authentication 对象

permitAll

允许所有的请求/调用

denyAll

拒绝所有的请求/调用

isAnonymouse()

当前用户是否是一个匿名用户

isRememberMe()

当前用户是否是通过remember-me自动登录

isAuthenticated()

当前用户是否已经认证成功

isFullyAuthenticated()

当前用户是否既不是匿名用户又不是通过remember-me自动登录的

hasPermission(Object target, Object permission)

当前用户是否具备指定目标的指定权限

hasPermission(Object targetId, String targetType, Object permission)

当前用户是否具备指定目标的指定权限

hasIpAddress(String ipAddress)

当前请求IP地址是否为指定IP

Spring security中通过 SecurityExpressionOperations 接口定义了基本的权限表达式:

public interface SecurityExpressionOperations {
	Authentication getAuthentication();

	boolean hasAuthority(String authority);

	boolean hasAnyAuthority(String... authorities);

	boolean hasRole(String role);

	boolean hasAnyRole(String... roles);

	boolean permitAll();

	boolean denyAll();

	boolean isAnonymous();

	boolean isAuthenticated();

	boolean isRememberMe();

	boolean isFullyAuthenticated();

	boolean hasPermission(Object target, Object permission);

	boolean hasPermission(Object targetId, String targetType, Object permission);
}

返回值为 boolean 类型的就是权限表达式,如果返回 true ,则表示权限校验通过,否则表示权限校验失败。

SecurityExpressionRoot

SecurityExpressionRootSecurityExpressionOperations 接口做了基本的实现,并在此基础上增加了 principal

接口的实现原理都很简单,所以说一下实现思路。

hasAuthorityhasAnyAuthorityhasRole 以及 hasAnyRole 四个方法主要是将传入的参数和 authentication 对象中保存的用户权限进行比对,如果用户具备相应的权限就返回 true ,否则返回 falsepermitAll 方法总是返回 true ,而 denyAll 方法总是返回 falseisAnonymousisAuthenticatedisRememberMe 以及 isFullyAuthenticated 四个方法则是根据对 authentication 对象的分析,然后返回 true 或者 false 。最后的 hasPermission 则需要调用 PermissionEvaluator 中对应的方法进行计算,然后返回 true 或者 false

SecurityExpressionRoot 中定义的表达式既可以在基于URL地址的权限管理中使用,也可以在基于方法的权限管理中使用。

WebSecurityExpressionRoot

继承自 SecurityExpressionRoot ,并增加了 hasIpAddress 方法,用来判断请求的IP地址是否满足要求。

在spring security中,如果权限管理是基于URL地址的,那么使用的是 WebSecurityExpressionRoot ,换句话说,这时可以使用 hasIpAddress 表达式。

MethodSecurityExpressionOperations

定义了基于方法的权限管理时一些必须实现的接口,主要是参数对象的 get/set 、返回对象的 get/set 以及返回受保护的对象。

MethodSecurityExpressionRoot

实现了 MethodSecurityExpressionOperations 接口,并对其定义的方法进行了实现。 MethodSecurityExpressionRoot 虽然也继承自 SecurityExpressionRoot ,但是并未扩展新的表达式,换句话说, SecurityExpressionRoot 中定义的权限表达式在方法上也可以使用,但是 hasIpAddress 不可以在方法上使用。

4 基于URL地址的权限管理

基于URL地址的权限管理主要是通过过滤器 FilterSecurityInterceptor 来实现的。如果开发者配置了基于URL地址的权限管理,那么 FilterSecurityInterceptor 就会被自动添加到spring security过滤器链中,在过滤器链中拦截下请求,然后分析当前用户是否具备请求所需要的权限,如果不具备,则抛出异常。

FilterSecurityInterceptor 将请求拦截下来之后,会交给 AccessDecisionManager 进行处理, AccessDecisionManager 则会调用投票器进行投票,然后对投票结果进行决策,最终决定请求是否通过。

4.1 基本用法

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 定义用户以及相应的角色和权限。
     * 对于复杂的权限管理系统,用户和角色关联,角色和权限关联,权限和资源关联;对于简单的权限管理系统,
     * 用户和权限关联,权限和资源关联。无论是哪种,用户都不会和角色以及权限同时直接关联。反应到代码上
     * 就是roles方法和authorities方法不能同时调用,如果同时调用,后者会覆盖前者(可以自行查看源码,
     * 最终都会调用authorities(Collection<? extends GrantedAuthority> authorities)方法)。
     * 需要注意的是,spring security会自动给用户的角色加上ROLE_前缀。
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                // javaboy具有ADMIN角色
                .withUser("javaboy").password("{noop}123").roles("ADMIN")
                .and()
                // zhangsan具有USER角色
                .withUser("zhangsan").password("{noop}123").roles("USER")
                .and()
                // itboyhub具有READ_INFO权限
                .withUser("itboyhub").password("{noop}123").authorities("READ_INFO");
    }

    /**
     * 配置拦截规则
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 用户必须具备ADMIN角色才可以访问/admin/**格式的地址
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 用户必须具备USER和ADMIN任意一个角色,才可以访问/user/**格式的地址
                .antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")
                // 用户必须具备READ_INFO权限,才可以访问/getInfo接口
                .antMatchers("/getInfo").hasAuthority("READ_INFO")
                // 剩余的请求只要是认证后的用户就可以访问
                .anyRequest().access("isAuthenticated()")
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

配置其实很好理解,但是有一些需要注意的地方:

  1. 大部分的表达式都有对应的方法可以直接调用,例如 hasRole 方法对应的就是 hasRole 表达式。开发者为了方便可以直接调用 hasRole 方法,但是最终还是会被转为表达式,当表达式执行结果为 true ,请求可以通过;否则请求不通过。
  2. Spring security会为 hasRole 表达式自动添加 ROLE_ 前缀,例如 hasRole("ADMIN") 方法转为表达式之后,就是 hasRole('ROLE_ADMIN') ,所以用户的角色也必须有 ROLE_ 前缀,而基于内存创建的用户会自动加上该前缀; hasAuthority 方法并不会添加任何前缀,而在用户定义时设置的用户权限也不会添加任何前缀。一言以蔽之,基于内存定义的用户,会自动给角色添加 ROLE_ 前缀,而 hasRole 也会自动添加 ROLE_ 前缀;基于内存定义的用户,不会给权限添加任何前缀,而 hasAuthority 也不会添加任何前缀。如果用户信息是从数据库中读取的,则需要注意 ROLE_ 前缀的问题。
  3. 可以通过 access 方法来使用权限表达式, access 方法的参数就是权限表达式。
  4. 代码的顺序很关键,当请求到达后,按照从上往下的顺序依次进行匹配。

配置完成后,再提供相应的测试接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }

    @GetMapping("/admin/hello")
    public String admin() {
        return "Hello admin";
    }

    @GetMapping("/user/hello")
    public String user() {
        return "Hello user";
    }

    @GetMapping("/getInfo")
    public String getInfo() {
        return "GetInfo";
    }
}

4.2 角色继承

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 如果需要配置角色继承,则只需要提供一个RoleHierarchy的实例即可
     */
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        return hierarchy;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 省略
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                // ROLE_ADMIN继承自ROLE_USER,因此可以直接访问/user/**格式的地址
                .antMatchers("/user/**").access("hasRole('USER')")
                .antMatchers("/getInfo").hasAuthority("READ_INFO")
                .anyRequest().access("isAuthenticated()")
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

4.3 自定义表达式

如果内置的表达式无法满足需求,开发者也可以自定义表达式。

假设现在有两个接口:

@RestController
public class HelloController {
    /**
     * 第一个接口:参数userId必须是偶数方可请求成功。
     */
    @GetMapping("/hello/{userId}")
    public String hello(@PathVariable Integer userId) {
        return "Hello " + userId;
    }

    /**
     * 第二个接口:参数username必须是javaboy方可请求成功,同时这两个接口必须认证后才能访问。
     */
    @GetMapping("/hi")
    public String hello2User(String username) {
        return "Hello " + username;
    }
}

/**
 * 自定义PermissionExpression类并注册到spring容器中,然后定义相应的方法。
 */
@Component
public class PermissionExpression {
    public boolean checkId(Authentication authentication, Integer userId) {
        if (authentication.isAuthenticated()) {
            return userId % 2 == 0;
        }

        return false;
    }

    public boolean check(HttpServletRequest request) {
        return "javaboy".equals(request.getParameter("username"));
    }
}

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 省略
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 省略其他
                // 在access方法中,可以通过@符号引用一个bean并调用其中的方法。在checkId方法调用时,
                // #userId表示前面的userId参数
                .antMatchers("/hello/{userId}").access("@permissionExpression.checkId(authentication, #userId)")
                // 需要同时满足isAuthenticated和check方法都为true,该请求才会通过
                .antMatchers("/hi").access("isAuthenticated() and @permissionExpression.check(request)")
                // 省略其他
    }
}

4.4 原理剖析

接下来简单梳理一下spring security中基于URL地址进行权限管理的一个大致原理。

AbstractSecurityInterceptor

该类统筹着关于权限处理的一切。方法很多,不过只需要关注其中的三个方法: beforeInvocationafterInvocation 以及 finallyInvocation

在这三个方法中, beforeInvocation 中会调用前置处理器完成权限校验, afterInvocation 中调用后置处理器完成权限校验, finallyInvocation 则主要做一些校验后的清理工作。

先来看下 beforeInvocation

protected InterceptorStatusToken beforeInvocation(Object object) {
	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());
	}

	// 首先调用obtainSecurityMetadataSource方法获取SecurityMetadataSource对象,然后调用其getAttributes方法获取
	// 受保护对象所需要的权限
	Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);
	 
	// 如果获取到的权限值为空
	if (CollectionUtils.isEmpty(attributes)) {
		// 则发布PublicInvocationEvent事件,该事件在调用公共安全对象(没有定义ConfigAttributes的对象)时生成
		publishEvent(new PublicInvocationEvent(object));
		// 此时直接返回null即可
		return null; // no further work post-invocation
	}
	
	// 查看当前用户的认证信息是否存在
	if (SecurityContextHolder.getContext().getAuthentication() == null) {
		credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
				"An Authentication object was not found in the SecurityContext"), object, attributes);
	}
	
	// 检查当前用户是否已经登录
	Authentication authenticated = authenticateIfRequired();
	// 尝试授权
	attemptAuthorization(object, attributes, authenticated);

	if (this.publishAuthorizationSuccess) {
		publishEvent(new AuthorizedEvent(object, attributes, authenticated));
	}

	// 临时替换用户身份,不过默认情况下,runAsManager的实例是NullRunAsManager,即不做任何替换,所以runAs为null
	Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
	
	if (runAs != null) {
		// 如果runAs不为空,则将SecurityContext中保存的用户信息修改为替换的用户对象,然后返回一个InterceptorStatusToken。
		// InterceptorStatusToken对象中保存了当前用户的SecurityContext对象,假如进行了临时用户替换,在替换完成后,最终
		// 还是要恢复成当前用户身份的
		SecurityContext origCtx = SecurityContextHolder.getContext();
		SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
		newCtx.setAuthentication(runAs);
		SecurityContextHolder.setContext(newCtx);
		// need to revert to token.Authenticated post-invocation
		return new InterceptorStatusToken(origCtx, true, attributes, object);
	}

	// 如果runAs为空,则直接创建一个InterceptorStatusToken对象返回即可
	return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}

private void attemptAuthorization(Object object, Collection attributes,
		Authentication authenticated) {
	try {
		// 核心功能:进行决策,该方法中会调用投票器进行投票,如果该方法执行抛出异常,则说明权限不足
		this.accessDecisionManager.decide(authenticated, object, attributes);
	}
	catch (AccessDeniedException ex) {
		publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
		throw ex;
	}
}

再来看下 finallyInvocation 方法:

/**
 * 如果临时替换了用户身份,那么最终要将用户身份恢复,finallyInvocation方法所做的事情就是恢复用户身份。这里的参数token就是
 * beforeInvocation方法的返回值,用户原始的身份信息都保存在token中,从token中取出用户身份信息,并设置到SecurityContextHolder
 * 中去即可。
 */
protected void finallyInvocation(InterceptorStatusToken token) {
	if (token != null && token.isContextHolderRefreshRequired()) {
		SecurityContextHolder.setContext(token.getSecurityContext());
	}
}

最后再来看看 afterInvocation 方法:

/**
 * 该方法接收两个参数,第一个参数token就是beforeInvocation方法的返回值,第二个参数returnedObject则是受保护对象的返回值,
 * afterInvocation方法的核心工作就是调用afterInvocationManager.decide方法对returnedObject进行过滤,然后将过滤后的
 * 结果返回。
 */
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
	if (token == null) {
		// public object
		return returnedObject;
	}
	finallyInvocation(token); // continue to clean in this method for passivity
	if (this.afterInvocationManager != null) {
		// Attempt after invocation handling
		try {
			returnedObject = this.afterInvocationManager.decide(token.getSecurityContext().getAuthentication(),
					token.getSecureObject(), token.getAttributes(), returnedObject);
		}
		catch (AccessDeniedException ex) {
			publishEvent(new AuthorizationFailureEvent(token.getSecureObject(), token.getAttributes(),
					token.getSecurityContext().getAuthentication(), ex));
			throw ex;
		}
	}
	return returnedObject;
}

FilterSecurityInterceptor

基于URL地址的权限管理,此时最终使用的是 AbstractSecurityInterceptor 的子类 FilterSecurityInterceptor ,这是一个过滤器。当在 configure(HttpSecurity) 方法中调用 http.authorizeRequests() 开启URL路径拦截规则配置时,就会通过 AbstractInterceptUrlConfigurer#configure 方法将 FilterSecurityInterceptor 添加到spring security过滤器链中。

对过滤器而且,最重要的就是 doFilter方法

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	// 构建受保护对象FilterInvocation,然后调用invoke方法
	invoke(new FilterInvocation(request, response, chain));
}

public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
	// 判断当前过滤器是否已经执行过,如果是,则继续执行剩下的过滤器
	if (isApplied(filterInvocation) && this.observeOncePerRequest) {
		// filter already applied to this request and user wants us to observe
		// once-per-request handling, so don't re-do security checking
		filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
		return;
	}
	
	// first time this request being called, so perform security checking
	if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
		filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
	}
	
	// 调用父类的beforeInvocation方法进行权限校验
	InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
	
	try {
		// 校验通过后,继续执行剩余的过滤器
		filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
	}
	finally {
		// 调用父类的finallyInvocation方法
		super.finallyInvocation(token);
	}
	
	// 最后调用父类的afterInvocation方法,可以看到,前置处理器和后置处理器都是在invoke方法中触发的
	super.afterInvocation(token, null);
}

AbstractInterceptUrlConfigurer

该类主要负责创建 FilterSecurityInterceptor 对象, AbstractInterceptUrlConfigurer 有两个不同的子类,两个子类创建出来的 FilterSecurityInterceptor 对象略有差异:

ExpressionUrlAuthorizationConfigurer
UrlAuthorizationConfigurer

通过 ExpressionUrlAuthorizationConfigurer 构建出来的 FilterSecurityInterceptor ,使用的投票器是 WebExpressionVoter ,使用的权限元数据对象是 ExpressionBasedFilterInvocationSecurityMetadataSource ,所以它支持权限表达式。

通过 UrlAuthorizationConfigurer 构建出来的 FilterSecurityInterceptor ,使用的投票器是 RoleVoterAuthenticatedVoter ,使用的权限元数据对象是 DefaultFilterInvocationSecurityMetadataSource ,所以它不支持权限表达式。

这是两者最主要的区别。

当在 configure(HttpSecurity) 方法中开启权限配置时,一般是通过如下方式:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").access("hasAnyRole('USER', 'ADMIN')")
			// ...
}

http.authorizeRequests() 方法实际上就是通过 ExpressionUrlAuthorizationConfigurer 来配置基于URL的权限管理,所以在配置时可以使用权限表达式。使用 ExpressionUrlAuthorizationConfigurer 进行配置,有一个硬性要求,就是至少配置一对URL地址和权限之间的映射关系。如果写成下面这种,就会出错:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }

如果使用 UrlAuthorizationConfigurer 去配置 FilterSecurityInterceptor ,则不存在此要求,即代码中可以不配置任何的映射关系,只需要URL路径和权限之间的映射关系完整即可,这在动态权限配置中非常有用。

不过在spring security中,使用 UrlAuthorizationConfigurer 去配置 FilterSecurityInterceptor 并不像使用 ExpressionUrlAuthorizationConfigurer 去配置那么容易,没有现成的方法,需要手动创建:

@Override
protected void configure(HttpSecurity http) throws Exception {
    ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
    
    // 开发者手动创建一个UrlAuthorizationConfigurer对象出来,并调用其getRegistry方法去开启URL路径和权限之间映射关系的配置。
    // 由于UrlAuthorizationConfigurer中使用的投票器是RoleVoter和AuthenticatedVoter,所以这里的角色需要自带ROLE_前缀
    // (因为RoleVoter的supports方法中会判断角色是否带有ROLE_前缀)
    http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
            .getRegistry()
            .mvcMatchers("/admin/**").access("ROLE_ADMIN")
            .mvcMatchers("/user/**").access("ROLE_USER");
    http.formLogin()
            .and()
            .csrf().disable();
}

使用 UrlAuthorizationConfigurer 去配置 FilterSecurityInterceptor 时,需要确保映射关系完整,即必须成对出现。

另外需要注意的是,无论是 ExpressionUrlAuthorizationConfigurer 还是 UrlAuthorizationConfigurer ,对于 FilterSecurityInterceptor 的配置来说都是在其父类 AbstractInterceptUrlConfigurer#configure 方法中,该方法中并未配置后置处理器 afterInvocationManager ,所以在基于URL地址的权限管理中,主要是前置处理器在工作。

4.5 动态管理权限规则

在前面的案例中,配置的URL拦截规则和请求URL所需要的权限都是通过代码来配置的,这样就比较死板,如果想要调整访问某一个URL所需要的权限,就需要修改代码。

动态管理权限规则就是将URL拦截规则和访问URL所需要的权限都保存在数据库中,这样,在不改变源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。

4.5.1 数据库设计

数据库脚本:

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `menu` (`id`, `pattern`)
VALUES
	(1,'/admin/**'),
	(2,'/user/**'),
	(3,'/guest/**');

CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `mid` (`mid`),
  KEY `rid` (`rid`),
  CONSTRAINT `menu_role_ibfk_1` FOREIGN KEY (`mid`) REFERENCES `menu` (`id`),
  CONSTRAINT `menu_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `menu_role` (`id`, `mid`, `rid`)
VALUES
	(1,1,1),
	(2,2,2),
	(3,3,3),
	(4,3,2);

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
	(1,'ROLE_ADMIN','系统管理员'),
	(2,'ROLE_USER','普通用户'),
	(3,'ROLE_GUEST','游客');

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` (`id`, `username`, `password`, `enabled`, `locked`)
VALUES
	(1,'admin','{noop}123',1,0),
	(2,'user','{noop}123',1,0),
	(3,'javaboy','{noop}123',1,0);

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `uid` (`uid`),
  KEY `rid` (`rid`),
  CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`),
  CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
	(1,1,1),
	(2,1,2),
	(3,2,2),
	(4,3,3);

4.5.2 实战

项目创建

创建项目,在 pom.xml 文件中引入web、spring security、mysql以及mybatis依赖。接下来在 application.properties 中配置数据库连接信息。

创建实体类

public class Role {
    private Integer id;
    private String name;
    private String nameZh;
    // 省略getter/setter
}

public class Menu {
    private Integer id;
    private String pattern;
    private List roles;
	// 省略getter/setter
}

public class User implements UserDetails {
    private Integer id;
    private String password;
    private String username;
    private boolean enabled;
    private boolean locked;
    private List roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
	// 省略其他getter/setter
}

创建service

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        user.setRoles(userMapper.getUserRoleByUid(user.getId()));
        return user;
    }
}

@Mapper
public interface UserMapper {
    List getUserRoleByUid(Integer uid);

    User loadUserByUsername(String username);
}





    

    



@Service
public class MenuService {
    @Autowired
    MenuMapper menuMapper;

    public List getAllMenu() {
        return menuMapper.getAllMenu();
    }
}

@Mapper
public interface MenuMapper {
    List getAllMenu();
}





    
        
        
        
            
            
            
        
    

    

配置spring security

/**
 * SecurityMetadataSource接口负责提供受保护对象所需要的权限。由于该案例中,受保护对象所需要的权限保存在数据库中,所以可以通过自定义类继承自
 * FilterInvocationSecurityMetadataSource,并重写getAttributes方法来提供受保护对象所需要的权限。
 */
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 在基于URL地址的权限控制中,受保护对象就是FilterInvocation。
     * @param object 受保护对象
     * @return 受保护对象所需要的权限
     */
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        // 从受保护对象FilterInvocation中提取出当前请求的URI地址,例如/admin/hello
        String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
        // 查询所有菜单数据(每条数据中都包含访问该条记录所需要的权限)
        List allMenu = menuService.getAllMenu();

        // 遍历菜单数据,如果当前请求的URL地址和菜单中某一条记录的pattern属性匹配上了(例如/admin/hello匹配上/admin/**)
        // 那么就可以获取当前请求所需要的权限;如果没有匹配上,则返回null。需要注意的是,如果AbstractSecurityInterceptor
        // 中的rejectPublicInvocations属性为false(默认值)时,则表示当getAttributes返回null时,允许访问受保护对象
        for (Menu menu : allMenu) {
            if (antPathMatcher.match(menu.getPattern(), requestURI)) {
                String[] roles = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
                return SecurityConfig.createList(roles);
            }
        }

        return null;
    }

    /**
     * 方便在项目启动阶段做校验,如果不需要校验,则直接返回null即可。
     * @return 所有的权限属性
     */
    @Override
    public Collection getAllConfigAttributes() {
        return null;
    }

    /**
     * 表示当前对象支持处理的受保护对象是FilterInvocation。
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    CustomSecurityMetadataSource customSecurityMetadataSource;
    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
        http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
                .withObjectPostProcessor(new ObjectPostProcessor() {
                    @Override
                    public  O postProcess(O object) {
                        // 使用配置好的CustomSecurityMetadataSource来代替默认的SecurityMetadataSource对象
                        object.setSecurityMetadataSource(customSecurityMetadataSource);
                        // 将rejectPublicInvocations设置为true,表示当getAttributes返回null时,不允许访问受保护对象
                        object.setRejectPublicInvocations(true);
                        return object;
                    }
                });
        http.formLogin()
                .and()
                .csrf().disable();
    }
}

测试

@RestController
public class HelloController {
    @GetMapping("/admin/hello")
    public String admin() {
        return "Hello admin";
    }

    @GetMapping("/user/hello")
    public String user() {
        return "Hello user";
    }

    @GetMapping("/guest/hello")
    public String guest() {
        return "Hello guest";
    }

	/**
     * 由于rejectPublicInvocations设置为true,因此,只有具备该接口权限的用户才能访问
     */
    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }
}

5 基于方法的权限管理

基于方法的权限管理主要是通过AOP来实现的,spring security中通过 MethodSecurityInterceptor 来提供相关的实现。不同在于, FilterSecurityInterceptor 只是在请求之前进行前置处理, MethodSecurityInterceptor 在此基础上还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理则是对方法的执行结果进行二次过滤。

5.1 注解介绍

目前在spring boot中基于方法的权限管理主要是通过注解来实现,需要通过 @EnableGlobalMethodSecurity 注解开启权限注解的使用:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

这个注解中,设置了三个属性:

  • prePostEnabled :开启spring security提供的四个权限注解, @PostAuthorize@PostFilter@PreAuthorize 以及 @PreFilter ,这四个注解支持权限表达式,功能比较丰富。
  • securedEnabled :开启spring security提供的 @Secured 注解,该注解不支持权限表达式。
  • jsr250Enabled :开始JSR-250提供的注解,主要包括 @DenyAll@PermitAll 以及 @RolesAllowed 三个注解,这些注解也不支持权限表达式。

这些注解的含义分别如下:

@PostAuthorize
@PostFilter
@PreAuthorize
@PreFilter
@Secured
@DenyAll
@PermitAll
@RolesAllowed

一般来说,只要设置 prePostEnabled = true 就够用了。

5.2 基本用法

创建spring boot项目,引入spring security和web依赖,项目创建完成后,添加配置文件:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
}

创建 User 类:

public class User {
    private Integer id;
    private String username;
    // 省略构造器、toString和getter/setter
}

@PreAuthorize

@Service
public class HelloService {
	// 执行该方法必须具备ADMIN角色才可以访问
    @PreAuthorize("hasRole('ADMIN')")
    public String preAuthorizeTest01() {
        return "Hello";
    }

    // 访问者名称必须是javaboy,而且还需要同事具备ADMIN角色才可以访问
    @PreAuthorize("hasRole('ADMIN') and authentication.name == 'javaboy'")
    public String preAuthorizeTest02() {
        return "Hello";
    }

    // 通过#引用方法参数,并对其进行校验,表示请求者的用户名必须等于方法参数name的值,方法才可以被执行
    @PreAuthorize("authentication.name == #name")
    public String preAuthorizeTest03(String name) {
        return "Hello: " + name;
    }
}

@SpringBootTest
class BasedOnMethodApplicationTests {
	@Autowired
    HelloService helloService;

    @Test
    // 通过该注解设定当前执行的用户角色是ADMIN
    @WithMockUser(roles = "ADMIN")
    void preAuthorizeTest01() {
        String hello = helloService.preAuthorizeTest01();
        Assertions.assertNotNull(hello);
        Assertions.assertEquals("Hello", hello);
    }

    @Test
    @WithMockUser(roles = "ADMIN", username = "javaboy")
    void preAuthorizeTest02() {
        String hello = helloService.preAuthorizeTest02();
        Assertions.assertNotNull(hello);
        Assertions.assertEquals("Hello", hello);
    }

    @Test
    @WithMockUser(username = "javaboy")
    void preAuthorizeTest03() {
        String hello = helloService.preAuthorizeTest03("javaboy");
        Assertions.assertNotNull(hello);
        Assertions.assertEquals("Hello: javaboy", hello);
    }
}

@PreFilter

@Service
public class HelloService {
	/**
     * PreFilter主要是对方法的请求参数进行过滤,它里边包含了一个内置对象filterObject,表示具体元素。
     * 如果方法只有一个参数,则内置的filterObject对象就代表该参数;如果有多个参数,则需要通过filterTarget
     * 来指定filterObject到底代表哪个对象。
     * 表示只保留id为奇数的user对象。
     */
    @PreFilter(filterTarget = "users", value = "filterObject.id % 2 != 0")
    public void preFilterTest(List users) {
        System.out.println("users = " + users);
    }
}


@Test
@WithMockUser(username = "javaboy")
void preFilterTest() {
    List users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        users.add(new User(i, "javaboy: " + i));
    }
    helloService.preFilterTest(users);
}

@PostAuthorize

@Service
public class HelloService {
	/**
     * PostAuthorize是在目标方法执行之后进行权限校验,其实这个主要是在ACL权限模型中会用到,目标方法执行完毕后,
     * 通过该注解去校验目标方法的返回值是否满足相应的权限要求。从技术角度来讲,该注解中也可以使用权限表达式,但是
     * 在实际开发中权限表达式一般都是结合PreAuthorize注解一起使用的。PostAuthorize包含一个内置对象returnObject,
     * 表示方法的返回值,开发中可以对返回值进行校验。
     */
    @PostAuthorize("returnObject.id == 1")
    public User postAuthorizeTest(Integer id) {
        return new User(id, "javaboy");
    }
}


@Test
@WithMockUser(username = "javaboy")
void postAuthorizeTest() {
    User user = helloService.postAuthorizeTest(1);
    Assertions.assertNotNull(user);
    Assertions.assertEquals(1, user.getId());
    Assertions.assertEquals("javaboy", user.getUsername());
}

@PostFilter

@Service
public class HelloService {
	/**
     * PostFilter注解在目标方法执行之后,对目标方法的返回结果进行过滤,该注解中包含了一个内置对象filterObject,
     * 表示目标方法返回的集合/数组中的具体元素。
     */
    @PostFilter("filterObject.id % 2 == 0")
    public List postFilterTest() {
        List users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add(new User(i, "javaboy: " + i));
        }
        return users;
    }
}


@Test
@WithMockUser(roles = "ADMIN")
void postFilterTest() {
    List all = helloService.postFilterTest();
    Assertions.assertNotNull(all);
    Assertions.assertEquals(5, all.size());
    Assertions.assertEquals(2, all.get(1).getId());
}

@Secured

@Service
public class HelloService {
	// 该注解不支持权限表达式,只能做一些简单的权限描述
    @Secured({"ROLE_ADMIN", "ROLE_USER"})
    public User securedTest(String username) {
        return new User(99, username);
    }
}


@Test
@WithMockUser(roles = "ADMIN")
void securedTest() {
    User user = helloService.securedTest("javaboy");
    Assertions.assertNotNull(user);
    Assertions.assertEquals(99, user.getId());
    Assertions.assertEquals("javaboy", user.getUsername());
}

@DenyAll

@Service
public class HelloService {
	// JSR-250:拒绝所有访问
    @DenyAll
    public void denyAllTest() {
    }
}


@Test
@WithMockUser(username = "javaboy")
void denyAllTest() {
    helloService.denyAllTest();
}

@PermitAll

@Service
public class HelloService {
	// JSR-250:允许所有访问
    @PermitAll
    public void permitAllTest() {
    }
}


@Test
@WithMockUser(username = "javaboy")
void permitAllTest() {
    helloService.permitAllTest();
}

@RolesAllowed

@Service
public class HelloService {
	/**
     * JSR-250:可以添加在方法上或类上,当添加在类上时,表示该注解对类中的所有方法生效;如果类上和方法上都有该注解,
     * 并且起冲突,则以方法上的注解为准。
     */
    @RolesAllowed({"ADMIN", "USER"})
    public String rolesAllowedTest() {
        return "RolesAllowed";
    }
}


@Test
@WithMockUser(roles = "ADMIN")
void rolesAllowedTest() {
    String s = helloService.rolesAllowedTest();
    Assertions.assertNotNull(s);
    Assertions.assertEquals("RolesAllowed", s);
}

13.5.3原理剖析

MethodSecurityInterceptor

当基于URL请求地址进行权限控制时,使用的 AbstractSecurityInterceptor 实现类是 FilterSecurityInterceptor ,而当基于方法进行权限控制时,使用的实现类则是 MethodSecurityInterceptor

MethodSecurityInterceptor 提供了基于AOP Alliance的方法拦截,该拦截器中所使用的 SecurityMetadataSource 类型为 MethodSecurityMetadataSourceMethodSecurityInterceptor 中最重要的就是 invoke 方法:

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    // 调用父类的beforeInvocation方法进行权限校验
    InterceptorStatusToken token = super.beforeInvocation(mi);
    Object result;

    try {
        // 校验通过后,调用mi.proceed()方法继续执行目标方法
        result = mi.proceed();
    }
    finally {
        // 在finally块中调用finallyInvocation方法完成一些清理工作
        super.finallyInvocation(token);
    }

    // 最后调用父类的afterInvocation方法进行请求结果的过滤
    return super.afterInvocation(token, result);
}

EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity 用来开启方法的权限注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 引入了一个配置GlobalMethodSecuritySelector,该类的作用主要是用来导入外部配置类
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
    // 省略其他
}


final class GlobalMethodSecuritySelector implements ImportSelector {
    // importingClassMetadata中保存了@EnableGlobalMethodSecurity注解的元数据,包括各个属性的值、注解是加在哪个配置类上等
	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		Class annoType = EnableGlobalMethodSecurity.class;
		Map annotationAttributes = importingClassMetadata.getAnnotationAttributes(annoType.getName(),
				false);
		AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationAttributes);
		// TODO would be nice if could use BeanClassLoaderAware (does not work)
		Class<?> importingClass = ClassUtils.resolveClassName(importingClassMetadata.getClassName(),
				ClassUtils.getDefaultClassLoader());
		boolean skipMethodSecurityConfiguration = GlobalMethodSecurityConfiguration.class
				.isAssignableFrom(importingClass);
		AdviceMode mode = attributes.getEnum("mode");
		boolean isProxy = AdviceMode.PROXY == mode;
		String autoProxyClassName = isProxy ? AutoProxyRegistrar.class.getName()
				: GlobalMethodSecurityAspectJAutoProxyRegistrar.class.getName();
		boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled");
		List classNames = new ArrayList<>(4);
		
		if (isProxy) {
			classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName());
		}
		
		classNames.add(autoProxyClassName);
		
		if (!skipMethodSecurityConfiguration) {
			classNames.add(GlobalMethodSecurityConfiguration.class.getName());
		}
		
		if (jsr250Enabled) {
			classNames.add(Jsr250MetadataSourceConfiguration.class.getName());
		}
		
		return classNames.toArray(new String[0]);
	}

}

selectImports 方法的逻辑比较简单,要导入的外部配置类有以下几种:

  • MethodSecurityMetadataSourceAdvisorRegistrar :如果使用的是spring自带的AOP,则该配置类会被导入。该类主要用来向spring容器中注册一个 MethodSecurityMetadataSourceAdvisor 对象,这个对象中定义了AOP中的pointcut和advice。
  • autoProxyClassName :注册自动代理创建者,根据不同的代理模式而定。
  • GlobalMethodSecurityConfiguration :这个配置类用来提供 MethodSecurityMetadataSourceMethodInterceptor 两个关键对象。如果开发者自定义配置类继承自 GlobalMethodSecurityConfiguration ,则这里不会导入这个外部配置类。
  • Jsr250MetadataSourceConfiguration :如果开启了JSR-250注解,这会导入该配置类。该配置类主要用来提供JSR-250注解所需的 Jsr250MethodSecurityMetadataSource 对象。

先来看 MethodSecurityMetadataSourceAdvisorRegistrar

class MethodSecurityMetadataSourceAdvisorRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 首先定义BeanDefinitionBuilder
		BeanDefinitionBuilder advisor = BeanDefinitionBuilder
				.rootBeanDefinition(MethodSecurityMetadataSourceAdvisor.class);
        // 然后给目标对象MethodSecurityMetadataSourceAdvisor的构造方法设置参数
		advisor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        // 要引用的MethodInterceptor对象名
		advisor.addConstructorArgValue("methodSecurityInterceptor");
        // 要引用的MethodSecurityMetadataSource对象名
		advisor.addConstructorArgReference("methodSecurityMetadataSource");
        // 和第二个一样,只不过一个是引用,一个是字符串
		advisor.addConstructorArgValue("methodSecurityMetadataSource");
		MultiValueMap attributes = importingClassMetadata
				.getAllAnnotationAttributes(EnableGlobalMethodSecurity.class.getName());
		Integer order = (Integer) attributes.getFirst("order");
		if (order != null) {
			advisor.addPropertyValue("order", order);
		}
        // 所有属性都配置好之后,将其注册到spring容器中
		registry.registerBeanDefinition("metaDataSourceAdvisor", advisor.getBeanDefinition());
	}

}

再来看 MethodSecurityMetadataSourceAdvisor

/**
 * 继承自AbstractPointcutAdvisor,主要定义了AOP的pointcut和advice,poincut也就是切点,可以简单理解为方法的拦截规则,
 * 即哪些方法需要拦截,哪些方法不需要拦截;advice也就是增强/通知,就是将方法拦截下来之后要增强的功能
 */
public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

	private transient MethodSecurityMetadataSource attributeSource;

	private transient MethodInterceptor interceptor;

	// 这里的pointcut对象就是内部类MethodSecurityMetadataSourcePointcut,在它的matches方法中,定义了具体的拦截规则
	private final Pointcut pointcut = new MethodSecurityMetadataSourcePointcut();

	private BeanFactory beanFactory;

	private final String adviceBeanName;

	private final String metadataSourceBeanName;

	private transient volatile Object adviceMonitor = new Object();

    // 构造方法所需的三个参数就是MethodSecurityMetadataSourceAdvisorRegistrar类中提供的三个参数
	public MethodSecurityMetadataSourceAdvisor(String adviceBeanName, MethodSecurityMetadataSource attributeSource,
			String attributeSourceBeanName) {
		this.adviceBeanName = adviceBeanName;
		this.attributeSource = attributeSource;
		this.metadataSourceBeanName = attributeSourceBeanName;
	}

	@Override
	public Pointcut getPointcut() {
		return this.pointcut;
	}

	/**
	 * Advice由getAdvice方法返回,在该方法内部,就是去spring容器中查找一个名为
	 * methodSecurityInterceptor的MethodInterceptor对象。
	 */
	@Override
	public Advice getAdvice() {
		synchronized (this.adviceMonitor) {
			if (this.interceptor == null) {
				this.interceptor = this.beanFactory.getBean(this.adviceBeanName, MethodInterceptor.class);
			}
			return this.interceptor;
		}
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		this.beanFactory = beanFactory;
	}

	private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
		ois.defaultReadObject();
		this.adviceMonitor = new Object();
		this.attributeSource = this.beanFactory.getBean(this.metadataSourceBeanName,
				MethodSecurityMetadataSource.class);
	}

	class MethodSecurityMetadataSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
		@Override
		public boolean matches(Method m, Class<?> targetClass) {
			MethodSecurityMetadataSource source = MethodSecurityMetadataSourceAdvisor.this.attributeSource;
			// 通过attributeSource.getAttributes方法去查看目标方法上有没有相应的权限注解,
			// 如果有,则返回true,目标方法就被拦截下来;如果没有,则返回false,目标方法就
			// 不会被拦截,这里的attributeSource实际上就是MethodSecurityMetadataSource对象,
			// 也就是提供权限元数据的类
			return !CollectionUtils.isEmpty(source.getAttributes(m, targetClass));
		}

	}
}

此时,应该已经明白AOP的切点和增强/通知是如何定义的了,这里涉及两个关键的对象:一个名为 methodSecurityInterceptorMethodInterceptor 对象和一个名为 methodSecurityMetadataSourceMethodSecurityMetadataSource 对象。

这两个关键的对象在 GlobalMethodSecurityConfiguration 类中定义,相关的方法比较长,先来看 MethodSecurityMetadataSource 对象的定义:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
	// 创建List集合,用来保存所有的MethodSecurityMetadataSource对象
	List sources = new ArrayList<>();
	ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
			getExpressionHandler());
	// 然后调用customMethodSecurityMetadataSource方法去获取自定义的MethodSecurityMetadataSource,
	// 默认情况下该方法返回null,如果想买有需要,开发者可以重写该方法来提供自定义的MethodSecurityMetadataSource对象
	MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();

	if (customMethodSecurityMetadataSource != null) {
		sources.add(customMethodSecurityMetadataSource);
	}

	// 接下来就是根据注解中配置的属性值,来向sources集合中添加相应的MethodSecurityMetadataSource对象
	boolean hasCustom = customMethodSecurityMetadataSource != null;
	boolean isPrePostEnabled = prePostEnabled();
	boolean isSecuredEnabled = securedEnabled();
	boolean isJsr250Enabled = jsr250Enabled();

	// 如果@EnableGlobalMethodSecurity注解配置了prePostEnabled=true,
	// 则加入PrePostAnnotationSecurityMetadataSource对象来解析相应的注解
	if (isPrePostEnabled) {
		sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
	}

	// 如果@EnableGlobalMethodSecurity注解配置了securedEnabled=true,
	// 则加入SecuredAnnotationSecurityMetadataSource对象来解析相应的注解
	if (isSecuredEnabled) {
		sources.add(new SecuredAnnotationSecurityMetadataSource());
	}

	// 如果@EnableGlobalMethodSecurity注解配置了jsr250Enabled=true,
	// 则加入Jsr250MethodSecurityMetadataSource对象来解析相应的注解
	if (isJsr250Enabled) {
		GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
		Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context
				.getBean(Jsr250MethodSecurityMetadataSource.class);
		if (grantedAuthorityDefaults != null) {
			jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
		}
		sources.add(jsr250MethodSecurityMetadataSource);
	}

	// 最后构建一个代理对象DelegatingMethodSecurityMetadataSource返回即可
	return new DelegatingMethodSecurityMetadataSource(sources);
}

可以看到,默认提供的 MethodSecurityMetadataSource 对象实际上是一个代理对象,它包含多个不同的 MethodSecurityMetadataSource 实例。在判断一个方法是否需要被拦截下来时,由这些被代理的对象逐个去解析目标方法是否含有相应的注解(例如 PrePostAnnotationSecurityMetadataSource 可以检查出目标方法是否含有 @PostAuthorize@PostFilter@PreAuthorize 以及 @PreFilter ),如果有,则请求就会被拦截下来。

再来看 MethodInterceptor 的定义:

@Bean
public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource) {
	// 查看代理的方式,默认使用spring自带的AOP,所以使用MethodSecurityInterceptor来创建对应的MethodInterceptor实例
	this.methodSecurityInterceptor = isAspectJ() ? new AspectJMethodSecurityInterceptor()
			: new MethodSecurityInterceptor();
	// 然后给methodSecurityInterceptor设置AccessDecisionManager决策管理器
	this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
	// 接下来给methodSecurityInterceptor配置后置处理器
	this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
	// 最后再把前面创建好的MethodSecurityMetadataSource对象配置给methodSecurityInterceptor
	this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
	RunAsManager runAsManager = runAsManager();
	if (runAsManager != null) {
		this.methodSecurityInterceptor.setRunAsManager(runAsManager);
	}
	return this.methodSecurityInterceptor;
}

protected AccessDecisionManager accessDecisionManager() {
	// 默认的决策管理器是AffirmativeBased
	List> decisionVoters = new ArrayList<>();
	// 根据@EnableGlobalMethodSecurity注解的配置,配置不同的投票器
	if (prePostEnabled()) {
		ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
		expressionAdvice.setExpressionHandler(getExpressionHandler());
		decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
	}
	if (jsr250Enabled()) {
		decisionVoters.add(new Jsr250Voter());
	}
	RoleVoter roleVoter = new RoleVoter();
	GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
	if (grantedAuthorityDefaults != null) {
		roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
	}
	decisionVoters.add(roleVoter);
	decisionVoters.add(new AuthenticatedVoter());
	return new AffirmativeBased(decisionVoters);
}

protected AfterInvocationManager afterInvocationManager() {
	// 如果@EnableGlobalMethodSecurity注解配置了prePostEnabled=true,则添加一个后置处理器
	// PostInvocationAdviceProvider,该类用来处理@PostAuthorize和@PostFilter两个注解
	if (prePostEnabled()) {
		AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
		ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
				getExpressionHandler());
		PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice);
		List afterInvocationProviders = new ArrayList<>();
		afterInvocationProviders.add(postInvocationAdviceProvider);
		invocationProviderManager.setProviders(afterInvocationProviders);
		return invocationProviderManager;
	}
	return null;
}

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

原文链接:https://blog.csdn.net/jiong9412/article/details/126801586

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章