Authorization Architecture(授权架构)

Authorities(权限)

Authentication,讨论了所有Authentication实现是如何存储GrantedAuthority对象集合。这些对象代表已经授予登录用户的权限。GrantedAuthority对象由AuthenticationManager插入到Authentication对象中,随后由AccessDecisionManagers在做出授权决定时读取。

GrantedAuthority是一个只有一个方法的接口:

  1. String getAuthority();

这个方法允许AccessDecisionManagers获得GrantedAuthority的一个精确的字符串表示。通过返回一个字符串的表示,GrantedAuthority可以被大多数访问决策管理程序轻松 “读取”。如果一个GrantedAuthority不能精确地表示为一个字符串,那么这个GrantedAuthority被认为是 “复杂的”,getAuthority()必须返回null。
一个 “复杂的 “GrantedAuthority的例子是一个实现,它存储了一个适用于不同客户帐号的操作和权限阈值的列表。将这种复杂的GrantedAuthority表示为一个字符串将是相当困难的,因此getAuthority()方法应该返回null。这将向任何AccessDecisionManager表明,它将需要特别支持GrantedAuthority的实现,以便理解其内容。
Spring Security包括一个具体的GrantedAuthority实现,即SimpleGrantedAuthority。这允许任何用户指定的字符串被转换为GrantedAuthority。安全架构中的所有AuthenticationProviders都使用SimpleGrantedAuthority来填充Authentication对象。

Pre-Invocation Handling

Spring Security提供了拦截器,控制对安全对象的访问,如方法调用或Web请求。访问决策管理器(AccessDecisionManager)对是否允许调用进行了预分配决定。

The AccessDecisionManager

AccessDecisionManager由AbstractSecurityInterceptor调用,负责做出最终的访问控制决定。AccessDecisionManager接口包含三个方法:

  1. // 根据相关的信息作出授权决策(允许访问、禁止),如果权限不被允许的时候,抛出AccessDeniedException
  2. void decide(Authentication authentication, Object secureObject,
  3. Collection<ConfigAttribute> attrs) throws AccessDeniedException;
  4. // 在decide被调用前调用,确保AccessDecisionManager可以处理Collection<ConfigAttribute> attrs
  5. boolean supports(ConfigAttribute attribute);
  6. // 在decide前被调用,确保secureObject被支持
  7. boolean supports(Class clazz);

Voting-Based AccessDecisionManager Implementations(基于投票的访问决策管理器的实现)

Authorization - 图1
使用这种方法,一系列的AccessDecisionVoter实现会对授权决定进行投票。然后,AccessDecisionManager根据其对投票的评估决定是否抛出一个AccessDeniedException。
AccessDecisionVoter接口有三个方法:

  1. int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);
  2. boolean supports(ConfigAttribute attribute);
  3. boolean supports(Class clazz);

具体的实现会返回一个int,值会反映在AccessDecisionVoter静态字段ACCESS_ABSTAIN、ACCESS_DENIED和ACCESS_GRANTED其中一个上。如果一个投票实现对授权决定没有意见,它将返回 ACCESS_ABSTAIN。如果它有意见,则必须返回 ACCESS_DENIED 或 ACCESS_GRANTED。
AccessDecisionManager主要有三个具体实现类:ConsensusBased(基于一致的)认为,超过半数投了赞成票即视为通过;AffirmativeBased(基于乐观的)认为,只要有赞成票即视为通过;UnanimousBased(基于悲观的)认为,只要有不赞成票即视为不通过。
可以实现一个自定义的AccessDecisionManager,以不同的方式计算投票。例如,来自特定访问决策投票人的投票可能会得到额外的权重,而来自特定投票人的拒绝投票可能会有否决作用。

RoleVoter

Spring Security提供的最常用的AccessDecisionVoter是简单的RoleVoter,它将配置属性视为简单的角色名称,如果用户被分配了该角色,则投票授予访问权。
如果任何ConfigAttribute以前缀ROLE开头,它就会投票。如果有一个GrantedAuthority返回的字符串表示(通过getAuthority()方法)与一个或多个以ROLE开头的ConfigAttribute完全相同,它将投票批准访问。如果没有与任何以ROLE开头的ConfigAttribute完全匹配,RoleVoter将投票拒绝访问。如果没有以ROLE开头的ConfigAttribute,投票者将投弃权票。

AuthenticatedVoter

另一个投票者是AuthenticatedVoter,它可以用来区分匿名、完全认证和remember-me认证的用户。许多网站在Remember-me认证下允许某些有限的访问,但要求用户通过登录来确认他们的身份以获得完整的访问。
当我们使用属性IS_AUTHENTICATED_ANONYMOUSLY来授予匿名访问权时,这个属性就会被AuthenticatedVoter处理。

Custom Voters

很明显,你也可以实现一个自定义的AccessDecisionVoter,你可以把你想要的任何访问控制逻辑放在里面。它可能是针对你的应用程序的(与业务逻辑相关),也可能实现一些安全管理逻辑。例如,你可以在Spring网站上找到一篇博客文章,其中描述了如何使用一个投票器来实时拒绝账户被暂停的用户的访问。

After Invocation Handling

虽然AccessDecisionManager是由AbstractSecurityInterceptor在进行安全对象调用前调用的,但有些应用程序需要一种方法来修改安全对象调用实际返回的对象。虽然你可以很容易地用AOP来实现这一点,但Spring Security提供了一个方便的钩子,它有几个具体的实现,与它的ACL功能相结合。
Authorization - 图2
像 Spring Security 的许多其他部分一样,AfterInvocationManager 有一个单一的具体实现,即 AfterInvocationProviderManager,它轮询 AfterInvocationProviders 的列表。每个 AfterInvocationProvider 都被允许修改返回对象或抛出一个 AccessDeniedException。事实上,多个提供者可以修改该对象,因为前一个提供者的结果会传递给列表中的下一个。
请注意,如果你使用 AfterInvocationManager,你仍然需要允许 MethodSecurityInterceptor 的 AccessDecisionManager 允许操作的配置属性。如果你使用典型的Spring Security包含的AccessDecisionManager实现,没有为某个安全方法调用定义配置属性将导致每个AccessDecisionVoter投弃权票。反过来,如果AccessDecisionManager属性 “allowIfAllAbstainDecisions “为假,就会抛出一个AccessDeniedException。你可以通过以下方式避免这个潜在的问题:(i)将 “allowIfAllAbstainDecisions “设置为 “true”(尽管通常不推荐这样做),或者(ii)简单地确保至少有一个配置属性是AccessDecisionVoter会投票授予访问权的。这后一种(推荐)方法通常是通过ROLE_USER或ROLE_AUTHENTICATED配置属性来实现的。

Hierarchical Roles(分层角色)

在一个应用程序中,一个特定的角色应该自动 “包括 “其他角色,这是一个常见的要求。例如,在一个有 “管理员 “和 “用户 “角色概念的应用程序中,你可能希望管理员能够做所有普通用户能做的事情。为了达到这个目的,你可以确保所有的管理员用户也被分配为 “用户 “角色。或者,你可以修改每个需要 “用户 “角色的访问限制,使其也包括 “管理员 “角色。如果你的应用程序中有很多不同的角色,这可能会变得相当复杂。
使用角色层次结构允许你配置哪些角色(或权限)应该包括其他角色(或权限)。Spring Security的RoleVoter的一个扩展版本,RoleHierarchyVoter,被配置了一个RoleHierarchy,它从其中获得了用户被分配的所有 “授权”。一个典型的配置可能看起来像这样。

  1. <bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
  2. <constructor-arg ref="roleHierarchy" />
  3. </bean>
  4. <bean id="roleHierarchy"
  5. class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
  6. <property name="hierarchy">
  7. <value>
  8. ROLE_ADMIN > ROLE_STAFF
  9. ROLE_STAFF > ROLE_USER
  10. ROLE_USER > ROLE_GUEST
  11. </value>
  12. </property>
  13. </bean>

这里我们在一个层次结构中有四个角色 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST。一个被认证为ROLE_ADMIN的用户,在对配置有上述RoleHierarchyVoter的AccessDecisionManager进行安全约束评估时,将表现得好像他们拥有所有四个角色。>符号可以被认为是 “包括 “的意思。
角色分层提供了一种方便的方法,可以简化你的应用程序的访问控制配置数据,并减少你需要分配给用户的权限数量。对于更复杂的要求,你可能希望在你的应用程序需要的特定访问权限和分配给用户的角色之间定义一个逻辑映射,在加载用户信息时在两者之间进行转换。

Authorize HttpServletRequest with FilterSecurityInterceptor

FilterSecurityInterceptor为HttpServletRequests提供授权。它被插入到FilterChainProxy中,作为安全过滤器之一。

Authorization - 图3

  1. 首先,FilterSecurityInterceptor从SecurityContextHolder获得一个Authentication
  2. 其次,FilterSecurityInterceptor从传入其中的HttpServletRequest、HttpServletResponse和FilterChain中创建一个FilterInvocation
  3. 接下来,它将FilterInvocation传递给SecurityMetadataSource以获得ConfigAttributes
  4. 最后将 Authentication, FilterInvocation, and ConfigAttributes 传给 AccessDecisionManager
    1. 如果授权被拒绝,就会抛出一个AccessDeniedException。在这种情况下,ExceptionTranslationFilter会处理AccessDeniedException
    2. 如果被允许访问,FilterSecurityInterceptor继续进行FilterChain,允许应用程序正常处理

默认情况下,Spring Security的授权要求所有的请求都要经过认证。

  1. protected void configure(HttpSecurity http) throws Exception {
  2. http
  3. // ...
  4. .authorizeHttpRequests(authorize -> authorize
  5. .anyRequest().authenticated()
  6. );
  7. }

我们可以通过按优先顺序添加更多的规则来配置Spring Security的不同规则。

  1. protected void configure(HttpSecurity http) throws Exception {
  2. http
  3. // ...
  4. .authorizeHttpRequests(authorize -> authorize
  5. .mvcMatchers("/resources/**", "/signup", "/about").permitAll()
  6. .mvcMatchers("/admin/**").hasRole("ADMIN")
  7. .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
  8. .anyRequest().denyAll()
  9. );
  10. }
  1. 有多个授权规则被指定。每条规则都是按照它们的申报顺序来考虑的
  2. 我们指定了多个URL,任何用户都可以访问。具体来说,如果URL以”/resources/“,”/signup”,或”/about”开头,任何用户都可以访问
  3. 任何以”/admin/“开头的URL将被限制给拥有 “ROLEADMIN “角色的用户。你会注意到,由于我们调用的是hasRole方法,我们不需要指定 “ROLLE“前缀
  4. 任何以”/db/“开头的URL都要求用户同时拥有 “ROLEADMIN “和 “ROLE_DBA”。你会注意到,由于我们使用的是hasRole表达式,我们不需要指定 “ROLE“前缀
  5. 任何还没有被匹配的URL都会被拒绝访问

    Expression-Based Access Control(基于表达式的访问控制)

    Common Built-In Expressions(常见的内置表达式)

    表达式根对象的基类是SecurityExpressionRoot。这提供了一些常见的表达式,在web和method security中都可以使用。
Expression Description
hasRole(String role) 如果当前用户具有指定的角色,则返回true。
例如,hasRole(‘admin’)
默认情况下,如果提供的角色不是以’ROLLE_’开头,它将被添加。可以通过修改DefaultWebSecurityExpressionHandler的defaultRolePrefix来定制。
hasAnyRole(String… roles) 如果当前用户拥有所提供的任何角色(以逗号分隔的字符串列表的形式给出),则返回true。
例如,hasAnyRole(‘admin’, ‘user’)
默认情况下,如果提供的角色不是以’ROLLE_’开头,它将被添加。可以通过修改DefaultWebSecurityExpressionHandler的defaultRolePrefix来定制。
hasAuthority(String authority) 如果当前用户有指定的权限,则返回true。
例如,hasAuthority(‘read’)
hasAnyAuthority(String… authorities) 如果当前用户具有所提供的任何授权(以逗号分隔的字符串列表形式给出),则返回true。
例如,hasAnyAuthority(‘read’, ‘write’)
principal 允许直接访问代表当前用户的主对象。
authentication 允许直接访问从SecurityContext获得的当前Authentication对象。
permitAll 允许访问所有资源
denyAll 不允许访问任何资源
isAnonymous() 如果当前用户是匿名用户,则返回true。
isRememberMe() 如果当前用户是一个记住我登录的,则返回true。
isAuthenticated() 如果用户不是匿名用户,返回true
isFullyAuthenticated() 如果用户不是匿名或记住我的用户,则返回true。
hasPermission(Object target, Object permission) 如果用户对所提供的目标有访问权限,返回true。例如,hasPermission(domainObject, ‘read’)
hasPermission(Object targetId, String targetType, Object permission) 如果用户对所提供的目标有访问权限,返回true。例如,hasPermission(1, ‘com.example.domain.Message’, ‘read’)

Web Security Expressions

要使用表达式来保护单个URL,你首先需要将元素中的use-expressions属性设置为true。然后,Spring Security将期望元素的访问属性包含Spring EL表达式。这些表达式应该评估为一个布尔值,定义是否允许访问。比如说:

  1. <http>
  2. <intercept-url pattern="/admin*"
  3. access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
  4. ...
  5. </http>

这里我们定义了一个应用程序的 “管理 “区域(由URL模式定义)应该只对拥有 “admin “授权的用户开放,并且其IP地址与本地子网匹配。我们已经在上一节中看到了内置的hasRole表达式。hasIpAddress表达式是一个额外的内置表达式,专门用于网络安全。它由WebSecurityExpressionRoot类定义,该类的一个实例在评估Web访问表达式时被用作表达式根对象。这个对象也直接暴露了名称为request的HttpServletRequest对象,所以你可以在表达式中直接调用请求。如果正在使用表达式,一个WebExpressionVoter将被添加到AccessDecisionManager中,它被命名空间所使用。因此,如果你不使用命名空间而想使用表达式,你将不得不在你的配置中添加其中一个。

Referring to Beans in Web Security Expressions

如果你想扩展可用的表达式,你可以很容易地引用你所暴露的任何Spring Bean。例如,假设你有一个名字为webSecurity的Bean,它包含以下方法签名:

  1. public class WebSecurity {
  2. public boolean check(Authentication authentication, HttpServletRequest request) {
  3. ...
  4. }
  5. }

如:

  1. http
  2. .authorizeHttpRequests(authorize -> authorize
  3. .antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
  4. ...
  5. )

Path Variables in Web Security Expressions

有时,能够在URL中引用路径变量是件好事。例如,考虑一个RESTful应用程序,通过URL路径的格式/user/{userId}来查找用户的ID。
你可以通过把路径变量放在模式中来轻松地引用它。例如,如果你有一个名字为webSecurity的Bean,它包含以下方法签名:

  1. public class WebSecurity {
  2. public boolean checkUserId(Authentication authentication, int id) {
  3. ...
  4. }
  5. }

如:

  1. http
  2. .authorizeHttpRequests(authorize -> authorize
  3. .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
  4. ...
  5. );

在这个配置中,匹配的URL将传入路径变量(并将其转换)到checkUserId方法。例如,如果URL是/user/123/resource,那么传入的id将是123。

Method Security Expressions

方法安全比简单的允许或拒绝规则要复杂一些。Spring Security 3.0引入了一些新的注解,以允许全面支持表达式的使用。

@Pre and @Post Annotations

有四个注解支持表达式属性,以允许调用前和调用后的授权检查,也支持对提交的集合参数或返回值进行过滤。它们是@PreAuthorize、@PreFilter、@PostAuthorize和@PostFilter。它们的使用是通过global-method-security命名空间元素启用的。

  1. <global-method-security pre-post-annotations="enabled"/>

Access Control using @PreAuthorize and @PostAuthorize

最明显有用的注解是@PreAuthorize,它决定了一个方法是否真的可以被调用。例如(来自联系人示例应用程序)

  1. @PreAuthorize("hasRole('USER')")
  2. public void create(Contact contact);

这意味着只允许具有 “ROLE_USER “角色的用户访问。显然,使用传统的配置和所需角色的简单配置属性,可以很容易地实现同样的事情。但是,怎么办呢?

  1. @PreAuthorize("hasPermission(#contact, 'admin')")
  2. public void deletePermission(Contact contact, Sid recipient, Permission permission);

在这里,我们实际上是将一个方法参数作为表达式的一部分,来决定当前用户是否有给定联系人的 “管理 “权限。内置的hasPermission()表达式通过应用程序上下文链接到Spring Security ACL模块,我们将在下面看到。你可以以表达式变量的名义访问任何方法参数。
Spring Security有多种方法来解析方法参数。Spring Security使用DefaultSecurityParameterNameDiscoverer来发现参数名称。默认情况下,对于一个方法的整体,会尝试以下选项:

  • 如果Spring Security的@P注解出现在方法的单个参数上,那么将使用该值。这对于用JDK 8之前的JDK编译的接口很有用,因为这些接口不包含任何关于参数名称的信息。比如说。 ```java import org.springframework.security.access.method.P;

@PreAuthorize(“#c.name == authentication.name”) public void doSomething(@P(“c”) Contact contact);

  1. 在幕后,这是用AnnotationParameterNameDiscoverer实现的,它可以被定制以支持任何指定注释的值属性。
  2. - 如果Spring Data@Param注解至少存在于该方法的一个参数上,那么该值将被使用。这对于用JDK 8之前的JDK编译的接口很有用,因为这些接口不包含任何关于参数名称的信息。比如说。
  3. ```java
  4. import org.springframework.data.repository.query.Param;
  5. ...
  6. @PreAuthorize("#n == authentication.name")
  7. Contact findContactByName(@Param("n") String name);

在幕后,这是用AnnotationParameterNameDiscoverer实现的,它可以被定制以支持任何指定注释的值属性。
任何Spring-EL的功能都可以在表达式中使用,所以你也可以访问参数的属性。例如,如果你想让一个特定的方法只允许与联系人的用户名相匹配的用户访问,你可以写道:

  1. @PreAuthorize("#contact.name == authentication.name")
  2. public void doSomething(Contact contact);

这里我们要访问另一个内置表达式,authentication,它是存储在安全上下文中的Authentication。你也可以使用表达式 principal,直接访问它的 “principal “属性。这个值通常是一个 UserDetails 实例,所以你可以使用 principal.username 或 principal.enabled 这样的表达式。
不太常见的是,你可能希望在该方法被调用后进行访问控制检查。这可以通过@PostAuthorize注解来实现。要访问一个方法的返回值,在表达式中使用内置名称returnObject。

Filtering using @PreFilter and @PostFilter

Spring Security支持使用表达式对集合、数组、地图和流进行过滤。这最常见的是在方法的返回值上执行。比如说。

  1. @PreAuthorize("hasRole('USER')")
  2. @PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
  3. public List<Contact> getAll();

当使用@PostFilter注解时,Spring Security会遍历返回的集合或地图,并删除所提供表达式为false的任何元素。对于数组来说,一个新的数组实例将被返回,其中包含过滤的元素。filterObject这个名字指的是集合中的当前对象。如果使用Map,它将指的是当前的Map.Entry对象,这允许人们在表达式中使用 filterObject.key 或 filterObject.value。你也可以在方法调用之前使用@PreFilter进行过滤,尽管这是不太常见的要求。语法是一样的,但是如果有一个以上的参数是集合类型,那么你必须使用这个注解的filterTarget属性按名称选择一个。
请注意,过滤显然不能替代对数据检索查询的调整。如果你正在过滤大的集合并删除许多条目,那么这很可能是低效的。

Built-In Expressions

有一些内置的表达式是专门针对方法安全的,我们已经在上面看到了这些表达式的使用。filterTarget和returnValue值很简单,但是hasPermission()表达式的使用值得我们仔细研究。

The PermissionEvaluator interface

hasPermission()表达式被委托给PermissionEvaluator的一个实例。它的目的是在表达式系统和Spring Security的ACL系统之间架起一座桥梁,允许你根据抽象权限来指定领域对象的授权约束。它对ACL模块没有明确的依赖性,所以如果需要,你可以把它换成其他的实现。该接口有两个方法:

  1. boolean hasPermission(Authentication authentication, Object targetDomainObject,
  2. Object permission);
  3. boolean hasPermission(Authentication authentication, Serializable targetId,
  4. String targetType, Object permission);

直接映射到表达式的可用版本,但第一个参数(认证对象)没有提供。第一个参数用于被控制访问的域对象已经被加载的情况。如果当前用户拥有该对象的给定权限,那么表达式将返回真。第二个版本用于对象没有被加载,但是它的标识符是已知的情况。域对象的一个抽象的 “类型 “指定器也是需要的,允许加载正确的ACL权限。传统上,这是该对象的Java类,但不一定是,只要它与权限的加载方式一致。
为了使用hasPermission()表达式,你必须在你的应用程序上下文中明确配置一个PermissionEvaluator。这看起来就像这样。

  1. <security:global-method-security pre-post-annotations="enabled">
  2. <security:expression-handler ref="expressionHandler"/>
  3. </security:global-method-security>
  4. <bean id="expressionHandler" class=
  5. "org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
  6. <property name="permissionEvaluator" ref="myPermissionEvaluator"/>
  7. </bean>

其中myPermissionEvaluator是实现PermissionEvaluator的bean。通常这将是ACL模块的实现,它被称为AclPermissionEvaluator。

Method Security Meta Annotations

你可以利用方法安全的元注解来使你的代码更加可读。如果你发现你在整个代码库中重复相同的复杂表达式,这就特别方便。例如,考虑下面的情况。

  1. @PreAuthorize("#contact.name == authentication.name")

我们可以创建一个可以替代的元注解,而不是到处重复这个。

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @PreAuthorize("#contact.name == authentication.name")
  3. public @interface ContactPermission {}

元注解可用于任何Spring Security方法的安全注释。为了与规范保持一致,JSR-250注解不支持元注解。