Authorization

SpringSecurity5.6.2版本的授权改动的还是很大的,所以将以前的文档重新写了一边。

主要包含一下部分内容:

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

1. 授权框架

1.1 权限

AuthenticationManager(认证管理)GrantedAuthority(分配好的权限)添加到Authentication对象中。AuthorizationManager 会读取,并作出授权判断。GrantedAuthority 是一个接口,只有一个方法String getAuthority();,这个方法只能返回一个字符串,如果你的权限表达式很复杂,那么需要自己定义一个方法,来支持getAuthority()GrantedAuthority 以集合的方式保存在Authentication对象中。

GrantedAuthority有很多具体的实现类:

  • SimpleGrantedAuthority: 默认的实现类
  • JaasGrantedAuthority
  • SwitchUserGrantedAuthority :将原始的用户Authentication保存在类里面了。

① 认证:填充 Authority

Remember-me为例子:

(1)doFilter 实际上是父类AbstractAuthenticationProcessingFilter 中的方法。这种设计抽象了具体的方法,让子类按照自己的业务来进行设计。

  • AbstractAuthenticationProcessingFilter 的子类
    • UsernamePasswordAuthenticationFilter:基于用户名与密码的
    • OAuth2LoginAuthenticationFilterOAuth2登陆的,会产生出一个OAuth2LoginAuthenticationToken
    • Saml2WebSsoAuthenticationFilter: 不常见
    • MockAuthenticationFilter : 这是一个测试代码,直接给了一个名叫 test 的用户名。正常包中没有这个类。
  • 今后自己定义的权限认证 Filter 都建议继承这个抽象类。

(2)attemptAuthentication是类UsernamePasswordAuthenticationFilter 中的方法。具体实现步骤如下

  • 新建了一个UsernamePasswordAuthenticationToken类。
  • 调用getAuthenticationManager().authenticate对这个Token进行验证。
  • 如果验证成功就返回一个认证通过的Authentication

(3)new是类UsernamePasswordAuthenticationToken 中的方法。继承了AbstractAuthenticationToken

(4)authenticate是类ProviderManager 中的方法。实现了AuthenticationManager接口。具体实现步骤如下

  • 第一步:getProviders()得到具体的Provider集合
  • 第二步:遍历Provider,判断当前provider是否支持这个authentication,如果支持,就调用result = provider.authenticate(authentication);
  • 第三步:如果result为空,就调用父类的authenticate得到result
  • 第四步:最后将result中的密码给清除了,并且通过eventPublisher发布成功的消息。

(5)supports是类AnonymousAuthenticationProvider 中的方法。实现了AuthenticationProvider接口。supports方法是AuthenticationProvider接口中定义的,必须要实现的函数。判断是否支持当前Token

(6)authenticate是类AnonymousAuthenticationProvider 中的方法。实现了AuthenticationProvider接口。 为了安全,这里设置的一个随机的 key。

(7)authenticate是类DaoAuthenticationProvider 中的方法。继承了AbstractUserDetailsAuthenticationProvider类,这个类实现了AuthenticationProvider接口。

  • 阅读原文的注释,UsernamePasswordAuthenticationToken会被创建、填充并返回。这里的principal表示当前登陆的主体,可以是String类型,也可以是UserDetails类型,但是系统中不推荐使用UserDetails类型,如果要强制使用String类型,可以setForcePrincipalAsString这个方法。
    • 我设计的系统中,就是保存了UserDetails,主要是为了获取数据方便,如果不推荐这个方法,那么每次就保存用户名,用到的时候,再从缓存中取。
    • 不管是用油箱登陆或者短信登陆,都转换成用户名,然后根据用户名来判断登陆是否正确。
    • 今后使用的过程中,也通过用户名来找到用户 ID 等附加信息。
  • 程序的主要逻辑:
    • AbstractUserDetailsAuthenticationProvider:authenticate 逻辑
      • 得到user,先从缓存中获取,如果得不到,就从抽象方法retrieveUser获取,这个抽象方法是由子类DaoAuthenticationProvider 实现的。
      • 使用DefaultPreAuthenticationChecks校验用户信息,主要是校验账户是否被锁定、是否过期、是否可用等信息。
      • 调用抽象方法additionalAuthenticationChecks,进行验证,这个抽象方法是由子类DaoAuthenticationProvider 实现的。
      • 使用DefaultPostAuthenticationChecks校验用户过期信息
      • 判断是否缓存用户
      • 如果forcePrincipalAsString=true,那么强制将principal=Username
      • 新建并返回UsernamePasswordAuthenticationToken
        • 添加用户名
        • 凭证:应该已经被情况
        • 详细信息
        • 权限列表
        • authenticated=true 用这个表示已经登陆了。

(8)eraseCredentials是类ProviderManager 中调用了CredentialsContainer#eraseCredentials()去清空里面关于密码相关的内容。

  • CredentialsContainer是一个接口,被AbstractAuthenticationTokenUsernamePasswordAuthenticationToken实现与继承,所以只用调用 token 的类就行。

(9)publishAuthenticationSuccess是类AuthenticationEventPublisher 中的方法。

  • 为了避免重复发送消息,在这里做了限制:parentResult == null
  • 系统默认的使用了NullEventPublisher类,这个类是不发送消息的。需要定义自己的类来发送消息。

(10)onAuthentication是类SessionAuthenticationStrategy 中的方法。主要负责处理认证成功的时候 session 需要执行的逻辑。

包含如下子类:

  • SessionFixationProtectionStrategy:创建一个新的 session,将老 session 的属性迁移到新的 session 中
  • ChangeSessionIdAuthenticationStrategy:仍使用原来的 session,仅修改下 session id
  • ConcurrentSessionControlAuthenticationStrategy:限制用户同时登陆的次数
  • CompositeSessionAuthenticationStrategy:组合多个 SessionAuthenticationStrategy
  • CsrfAuthenticationStrategy:登陆成功之后,更换原来的 csrf token
  • NullAuthenticatedSessionStrategy:空实现,什么都不操作
  • RegisterSessionAuthenticationStrategy:注册新 session 信息到 SessionRegistry

参考文档中描述了各个实现类的代码

(11)successfulAuthentication是类AbstractAuthenticationProcessingFilter 中的方法。

  • 1:初始化了SecurityContext
  • 2:rememberMeServices 做了处理
  • 3:ApplicationEventPublisher 进行了发布
  • 4:AuthenticationSuccessHandler进行了处理

② 授权:读取 Authority

假设权限配置如下:

http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/user").hasRole("USER")
.antMatchers("/info").hasRole("INFO")
.anyRequest().authenticated()
)

系统会根据配置内容,初始化一个RequestMatcherDelegatingAuthorizationManager代理类,这个类针对每个 URL 都会初始化一个对应的,按照上面代码,会把初始化数据放到一个Map<RequestMatcher, AuthorizationManager<RequestAuthorizationContext>> MAP 中。具体如下:

  • /user -》 AuthorityAuthorizationManager
  • /info -》 AuthorityAuthorizationManager
  • anyRequest() -》 AuthenticatedAuthorizationManager

如果碰到/info,就调用AuthorityAuthorizationManager中的verify方法。

(1)doFilter是类OncePerRequestFilter 中的方法。这个 Filter 不会被重复执行。

(2)doFilterInternal是类AuthorizationFilter 中的方法。

  • 首先会调用AuthorizationManager来进行授权
    • this.authorizationManager.verify(this::getAuthentication, request)
  • 继续后续的filterChain
    • filterChain.doFilter(request, response);

(3)verify是类AuthorizationManager 中的方法,AuthorizationManager是一个接口,这个接口中verify会调用具体实现的类中实现的check函数,这时候调用的是RequestMatcherDelegatingAuthorizationManager实现类.

(4)check是类RequestMatcherDelegatingAuthorizationManager URL匹配代理授权类 中的方法。

  • RequestMatcherDelegatingAuthorizationManager 是一个代理类,里面有一个 MAP
    • Map<RequestMatcher, AuthorizationManager<RequestAuthorizationContext>>
    • 这个 MAP 中 KEY 代表要匹配的 URL,VALUE 代表的是对应的AuthorizationManager
  • 按照配置文件,如果URL=/user,要调用对应的AuthorityAuthorizationManager中的check函数。
  • 按照配置文件,如果URL=any,要调用对应的AuthenticatedAuthorizationManager中的check函数。

(5):如果URL=/user,check是类AuthenticatedAuthorizationManager[按照权限授权类] 中的方法。

什么情况下授权通过:

  • authentication != null
  • authentication.isAuthenticated()
  • isAuthorized
    • authentication取出Authorities 与 自身的Set<GrantedAuthority>做对比
    • 如果有字符串匹配上的,就返回true ,否则返回false

(6):如果URL=any,check是类AuthenticatedAuthorizationManager[是否登陆授权类] 中的方法。

什么情况下授权通过:

  • authentication != null
  • authentication.isAuthenticated()
  • isNotAnonymous:不能是匿名类
    • 通过AuthenticationTrustResolver接口中的isAnonymous判断是否是匿名类。
    • AuthenticationTrustResolverImpl是系统默认的AuthenticationTrustResolver实现类。
    • 这里在mfa例子中,就说明白为啥要自定义一个AuthenticationTrustResolver

1.2 前置处理逻辑

1.2.1 AuthorizationManager

AuthorizationManager 会取代 AccessDecisionManagerAccessDecisionVoter.

@Nullable
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
default void verify(Supplier<Authentication> authentication, T object) {
AuthorizationDecision decision = check(authentication, object);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}

AuthorizationManager 有两个方法,check方法需要由具体的类来实现。T object一般用不到,但是在特殊情况可以用到,例如下面的例子,可以将HttpServletRequest类型的object传入,然后获得HttpServletRequest中的参数进行额外的判断。如果投票不通过,,那么就抛出AccessDeniedException

// 例如AuthorizationFilter中,就定义了一个HttpServletRequest类型的object
public class AuthorizationFilter extends OncePerRequestFilter {
private final AuthorizationManager<HttpServletRequest> authorizationManager;

1.2.2 AuthorizationManager 的具体实现

用户可以定义自己的AuthorizationManager来控制认证的各个方面,Spring Security装了一个代理类,这个代理类会把每个单个的AuthorizationManager组合起来。

针对匹配 URL,可以使用RequestMatcherDelegatingAuthorizationManager来做代理。针对函数的安全,可以使用 AuthorizationManagerBeforeMethodInterceptorAuthorizationManagerAfterMethodInterceptor

  • AuthorizationManager 具体实现类
    • AuthenticatedAuthorizationManager
    • AuthorityAuthorizationManager
    • Jsr250AuthorizationManager
    • PostAuthorizeAuthorizationManager
    • PreAuthorizeAuthorizationManager
    • RequestMatcherDelegatingAuthorizationManager
    • SecuredAuthorizationManager
① AuthorityAuthorizationManager

基于权限的授权管理,如果当前的Authentication包含了所需的权限,那么就通过。

② AuthenticatedAuthorizationManager

为了区分匿名用户,已认证用户和remember-me用户。 有些系统允许remember-me用户访问,但是在一些特殊访问时,需要用户输入密码进行登陆。

1.3 自定义 AuthorizationManagers

可以自定义AuthorizationManagers,通过查询外部接口或自己的数据库来实现授权的逻辑。

特别说明

老版本中的AccessDecisionVoter 已经作废了,现在通过AuthorizationManagers来替代了.

1.4 兼容 AccessDecisionManager 与 AccessDecisionVoters

为了继承使用老的系统中AccessDecisionManagerAccessDecisionVoters的逻辑,不用大的修改,可以参考官方的文档

1.5 可分层的角色

一个很常见的需求,在一个系统中,可能要求一个角色包含另外一个角色的权限. 例如一个系统中有两个角色"admin" 和 "user",可以指定"admin"可以做任何"user"的操作,如果不是这样,可能要做很复杂的指定。

role-hierarchy 允许你指定一个角色(或权限)包含其他的。

@Bean
AccessDecisionVoter hierarchyVoter() {
RoleHierarchy hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER\n" +
"ROLE_USER > ROLE_GUEST");
return new RoleHierarcyVoter(hierarchy);
}

AccessDecisionVoter可能会被遗弃。

在实际的过程中,可以通过ant表达式来实现的,例如“\user\*” 包含了下面的权限。

1.6 要作废的认证组件

  • AccessDecisionManager
  • Voting-Based AccessDecisionManager Implementations
    • RoleVoter
    • AuthenticatedVoter
    • Custom Voters

2. 针对 HTTP 请求的授权

通过AuthorizationFilter来给 HTTP 请求授权。如果要更深入的了解针对Servlet应用程序的授权,可以参考这个章节: Servlet 架构与实现

2.1 兼容性提示

FilterSecurityInterceptor未来会被AuthorizationFilter,为了兼容老的系统,FilterSecurityInterceptor仍被作为默认,这里讨论AuthorizationFilter如何工作,以及怎么去覆盖缺省的配置。

AuthorizationFilter 可以为HttpServletRequest提供授权。它会作为一个Security Filters被添加到 FilterChainProxy

通过定义一个SecurityFilterChain,并使用authorizeHttpRequests来替换原先的authorizeRequests,这样就可以覆盖原先的做法,不使用FilterSecurityInterceptor

Example 1 Use authorizeHttpRequests

// 定义了一个SecurityFilterChain 并使用了authorizeHttpRequests
@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated();
)
// ...
return http.build();
}

相对于老的版本,有几个改进的方法:

1:使用AuthorizationManager API 来替换原先复杂的metadata sources, config attributes, decision managers, and voters. ,这样更便于重用与定制化。

2:使用延迟授权,避免以前每个请求都去判断授权,而是在需要进行授权的时候,进行授权处理。

3:基于Bean的配置。

2.2 基本处理流程

authorizeHttpRequests 被用于替换 authorizeRequests时, AuthorizationFilter 被用于替换 FilterSecurityInterceptor.

  • 首先,AuthorizationFilter通过SecurityContextHolder得到Authentication,这里通过一个Supplier来实现延迟获取。
this.authorizationManager.verify(this::getAuthentication, request);
  • 其次,AuthorizationFilter通过HttpServletRequest, HttpServletResponseFilterChain创立了一个FilterInvocation。 【文档的这部分有问题,实际上没有创将FilterInvocation,而是将HttpServletRequest作为参数传递进去了。】

  • 下一步,将Supplier<Authentication>FilterInvocation【或者是HttpServletRequest】 传递给 AuthorizationManager

    • 如果授权失败,后抛出AccessDeniedException,这个异常会被ExceptionTranslationFilter 捕获并处理。
    • 如果授权成功,AuthorizationFilter会继续FilterChain的正常请求流程。

2.3 案例说明

① 常见配置

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize ①
.mvcMatchers("/resources/**", "/signup", "/about").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().denyAll()
);
return http.build();
}
  • ① 这里指定了多个授权的规则,其中的顺序是经过周密的考虑的。
  • ② 我们指定了多个 URL 匹配模式,让所有的用户都可以访问。具体来说,所有人都可以访问用"/resources/"开头, 等于"/signup", 或 等于 "/about"的 URL。
  • ③ 任何以"/admin/"开头的 URL,都被限定在具有"ROLEADMIN"角色的用户才可以访问。你会发现,如果使用了hasRole方法,就不用指定 "ROLE"前缀
  • ④ 任何以"/db/"开头的 URL,都被限定在同时拥有"ROLE_ADMIN" 与 "ROLE_DBA"角色的用户才可以访问。
  • ⑤ 任何无法匹配到的 URL 都会被拒绝。这是一个好的策略,如果你不想意外忘记去更新你的认证规则。

② 组织 RequestMatcherDelegatingAuthorizationManager

你可以做一个基于 bean 的方法去组织你自己的 RequestMatcherDelegatingAuthorizationManager

@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(access)
)
// ...
return http.build();
}
@Bean
AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
RequestMatcher permitAll =
new AndRequestMatcher(
new MvcRequestMatcher(introspector, "/resources/**"),
new MvcRequestMatcher(introspector, "/signup"),
new MvcRequestMatcher(introspector, "/about"));
RequestMatcher admin = new MvcRequestMatcher(introspector, "/admin/**");
RequestMatcher db = new MvcRequestMatcher(introspector, "/db/**");
RequestMatcher any = AnyRequestMatcher.INSTANCE;
AuthorizationManager<HttpRequestServlet> manager = RequestMatcherDelegatingAuthorizationManager.builder()
.add(permitAll, (context) -> new AuthorizationDecision(true))
.add(admin, AuthorityAuthorizationManager.hasRole("ADMIN"))
.add(db, AuthorityAuthorizationManager.hasRole("DBA"))
.add(any, new AuthenticatedAuthorizationManager())
.build();
return (context) -> manager.check(context.getRequest());
}

③ 自定义 AuthorizationManager

针对某个 URL 的授权。

Example . Custom Authorization Manager

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager());
)
// ...
return http.build();
}

也可以针对所有 URL 授权

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest.access(new CustomAuthorizationManager());
)
// ...
return http.build();
}

3. 基于表达式的访问控制

3.1 常见的内置表达式

这些表达式的【 root objects】是SecurityExpressionRoot

表达描述
hasRole(String role)返回true当前主体是否具有指定的角色。例如, hasRole('admin')默认情况下,如果提供的角色不以“ROLE_”开头,则会添加它。这可以通过修改defaultRolePrefixon 来定制DefaultWebSecurityExpressionHandler
hasAnyRole(String… roles)返回true当前主体是否具有任何提供的角色(以逗号分隔的字符串列表形式给出)。例如, hasAnyRole('admin', 'user')默认情况下,如果提供的角色不以“ROLE_”开头,则会添加它。这可以通过修改defaultRolePrefixon 来定制DefaultWebSecurityExpressionHandler
hasAuthority(String authority)返回true当前主体是否具有指定的权限。例如, hasAuthority('read')
hasAnyAuthority(String… authorities)返回true当前主体是否具有任何提供的权限(以逗号分隔的字符串列表形式给出)例如, hasAnyAuthority('read', 'write')
principal允许直接访问代表当前用户的主体对象
authentication允许直接访问AuthenticationSecurityContext
permitAll总是评估为 true
denyAll总是评估为 false
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')
hasIpAddresshasIpAddress('192.168.1.0/24')

3.2 Web 安全表达式

① 自定义校验 Bean

下面的方法与自定义 AuthorizationManager 功能优点重复,这样做的好处是可以传递一些参数

项目中最常用的就是基于 URL 的安全表达式了,这里重点说一下如何自定义校验 Bean。

例如,假设您有一个名称为 的 Bean,webSecurity其中包含以下方法签名:

public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
}

您可以使用以下方法参考该方法:

http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
)

② 获取 RUL 中的参数

例如,考虑一个 RESTful 应用程序,它通过 id 格式从 URL 路径中查找用户/user/{userId}

public class WebSecurity {
public boolean checkUserId(Authentication authentication, int id) {
...
}
}

您可以使用以下方法参考该方法:

http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);

3.3 函数安全表达式

函数的安全会稍微复杂点,Spring Security 3.0 提供一些新的注解来解决这些复杂的问题。

什么时候用基于方法的表达式呢? 如果想对用户的权限进行更加详细的控制,例如某个人,只能修改自己的权限内容,那么就可以用到这个功能。但是这个方法,也可以在函数内部实现。我见有些 Sql 语句,会自动添加一个 Where 条件,从当前登录的用户获取 ID。

有四个注释支持表达式属性,以允许调用前和调用后授权检查,并支持过滤提交的集合参数或返回值。它们是@PreAuthorize@PreFilter@PostAuthorize@PostFilter。 这个需要单独启动。

① @PreAuthorize 和@PostAuthorize

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

这意味着只有具有“ROLE_USER”角色的用户才能访问。显然,使用传统配置和所需角色的简单配置属性可以轻松实现相同的事情。但是关于:

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

我们实际上使用方法参数作为表达式的一部分来确定当前用户是否具有给定联系人的“管理员”权限。内置hasPermission()表达式通过应用程序上下文链接到 Spring Security ACL 模块。

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

② @PreFilter 和@PostFilter

通常在方法的返回值上执行,例如:

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

该名称filterObject指的是集合中的当前对象。

4. 安全对象的实现

4.1 MethodSecurityInterceptor

您当然可以MethodSecurityInterceptor直接在您的应用程序上下文中配置一个用于 Spring AOP 的代理机制之一:

<bean id="bankManagerSecurity" class=
"org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
<sec:method-security-metadata-source>
<sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
<sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
</sec:method-security-metadata-source>
</property>
</bean>

4.2 AspectJ

AspectJ 安全拦截器与上一节讨论的 AOP 联盟安全拦截器非常相似。事实上,我们将只讨论本节中的差异。

让我们首先考虑AspectJSecurityInterceptor在 Spring 应用程序上下文中是如何配置的:

<bean id="bankManagerSecurity" class=
"org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
<sec:method-security-metadata-source>
<sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
<sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
</sec:method-security-metadata-source>
</property>
</bean>

5. 方法的安全访问

可以针对 Service 层定义安全,不常用。所以不深入的理解了。

5.1 EnableMethodSecurity

5.6 版本后在任何一个@Configuration实例中使用@EnableMethodSecurity来启用。

相对与@EnableGlobalMethodSecurity@EnableMethodSecurity带来了新的好处:

    1. 使用了AuthorizationManager中非常简单的 API 来替代了以前的metadata sources,config atrributes,dicision managersvotes,这样简化了重用与定制。
    2. 基于 bean 的配置,而不是继承 GlobalMethodSecurityConfiguration 去进行定制化配置。
    3. 使用 Spring 本身的 AOP 进行构建
    4. 会检查那些有冲突的注解配置,保证配置的正确性与明确性。
    5. 符合 JSR-250 标准
    6. 默认启用了 @PreAuthorize, @PostAuthorize, @PreFilter, 和 @PostFilter

例如,以下将启用 Spring Security 的@PreAuthorize 注释。

@EnableMethodSecurity
public class MethodSecurityConfig {
// ...
}

在一个类或接口的方法上添加注解后,就会限制对这个方法的访问。Spring Security 的原生注解支持为方法定义了一组属性, 这些会被传递给 DefaultAuthorizationMethodInterceptorChain 做出判断。

// 下面的代码把权限写死到注解中,让未来很难进行维护
public interface BankService {
@PreAuthorize("hasRole('USER')")
Account readAccount(Long id);
@PreAuthorize("hasRole('USER')")
List<Account> findAccounts();
@PreAuthorize("hasRole('TELLER')")
Account post(Account account, Double amount);
}

要启动 @Secured 注解,可以这么来配置。

@EnableMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}

启用@Secured的具体用法

public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

或者使用 JSR-250

@EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}

① 定制授权

如果您需要自定义处理表达式的方式,您可以公开一个自定义 MethodSecurityExpressionHandler

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setTrustResolver(myCustomTrustResolver);
return handler;
}

提示

  • 上面的代码使用了 static,主要目标是确保 Spring 在初始化 Spring Security 的方法 security @Configuration 类之前发布它

可以配置角色的前缀

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}

提示

  • 上面的代码使用了 static

② 定制授权 Managers

可以在方法执行前后进行授权,如果在执行前被拒绝,那么抛出AccessDeniedException异常,方法不会被执行。 如果是方法后授权,那么方法会被执行,如果被拒绝了,不会返回值,同样也会抛出AccessDeniedException异常。

假设要重载系统默认的定义, 那么需要做下面的配置

@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preFilterAuthorizationMethodInterceptor() {
return new PreFilterAuthorizationMethodInterceptor();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorizeAuthorizationMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorizeAuthorizationMethodInterceptor() {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilterAuthorizationMethodInterceptor() {
return new PostFilterAuthorizationMethodInterceptor();
}
}

请注意,Spring Security 的方法安全性是使用 Spring AOP 构建的。因此,拦截器是根据指定的顺序调用的。这可以通过在拦截器实例上调用 setOrder 来定制,如下所示:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilterAuthorizationMethodInterceptor() {
PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationMethodInterceptor();
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE.getOrder() - 1);
return interceptor;
}

您可能只想在应用程序中支持 @PreAuthorize,在这种情况下,您可以执行以下操作:

@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}
}

或者,您可能为AuthorizationManager添加一个before校验的方法。在这种情况下,您需要告诉Spring Security AuthorizationManager以及您的AuthorizationManager应用于哪些方法和类。因此,您可以将Spring Security配置为在@PreAuthorize@PostAuthorize之间调用AuthorizationManager,如下所示:

//下面的代码不保证可以执行,因为在Spring security中不存在代码中的有些类,可能是笔误
@EnableMethodSecurity
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor customAuthorize() {
JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
pattern.setPattern("org.mycompany.myapp.service.*");
AuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pattern, rule);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}

After method授权通常涉及分析返回值以验证访问。例如,您可能有一种方法可以确认请求的帐户实际上属于登录用户,如下所示:

public interface BankService {
@PreAuthorize("hasRole('USER')")
@PostAuthorize("returnObject.owner == authentication.name")
Account readAccount(Long id);
}

您可以提供自己的 AuthorizationMethodInterceptor 来自定义如何评估对返回值的访问。 例如,如果你有自己的自定义注解,你可以像这样配置它:

@EnableMethodSecurity
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public Advisor customAuthorize(AuthorizationManager<MethodInvocationResult> rules) {
AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules);
interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
return interceptor;
}
}

它将在 @PostAuthorize 拦截器之后调用。

5.2 EnableGlobalMethodSecurity

按照上一章节的文档,EnableGlobalMethodSecurity好像要被替换了,所以这里就不深入讲解了。

所以关于下面的都可以不看了

  • GlobalMethodSecurityConfiguration
  • The Element
  • Adding Security Pointcuts using protect-pointcut

使用保护切入点添加安全切入点

的使用protect-pointcut特别强大,因为它允许您只用一个简单的声明就可以对许多 bean 应用安全性。考虑以下示例:

<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
access="ROLE_USER"/>
</global-method-security>

6. 对象的安全访问

Spring Security ACL

ACL 的核心就是 Acl 类,它将 Domain Object 和 对应的 Security Object 以及权限关联了起来。
其中,将 Security Object 和权限关联起来的类是 AccessControlEntry

ACL 的原理是这样:

对于系统中的每一个资源,都会配置一个访问列表,这个列表中记录了用户/角色对于资源的 CURD 权限,当系统需要访问这些资源时,会首先检查列表中是否存在当前用户的访问权限,进而确定当前用户是否可以执行相应的操作。

ACL 的使用非常简单,搞明白它的原理自己分分钟就能实现。但是 ACL 有一个明显的缺点,就是需要维护大量的访问权限列表。大量的访问控制列表带来的问题就是性能下降以及维护复杂。

参考文档:

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

① 概述

为了实现权限控制,系统维护了这么一个表(实际上是由 4 个表组成)

实际由下面 4 个表组成

acl_class

acl_sid

acl_object_identity

acl_entry

② 创建 Service

@Service
public class NoticeMessageService {
@Autowired
NoticeMessageMapper noticeMessageMapper;
@PostFilter("hasPermission(filterObject, 'READ')")
public List<NoticeMessage> findAll() {
List<NoticeMessage> all = noticeMessageMapper.findAll();
return all;
}
@PostAuthorize("hasPermission(returnObject, 'READ')")
public NoticeMessage findById(Integer id) {
return noticeMessageMapper.findById(id);
}
@PreAuthorize("hasPermission(#noticeMessage, 'CREATE')")
public NoticeMessage save(NoticeMessage noticeMessage) {
noticeMessageMapper.save(noticeMessage);
return noticeMessage;
}
@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
public void update(NoticeMessage noticeMessage) {
noticeMessageMapper.update(noticeMessage);
}
}

② 创建 acl

让 hr 这个用户可以读取 system_message 表中 id 为 1 的记录,方式如下:

@Autowired
JdbcMutableAclService jdbcMutableAclService;
// 这里假设javaboy用户登录了,所以这个 acl 创建好之后,它的 owner 是 javaboy
public void test02() {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 1);
Permission p = BasePermission.READ;
//添加了一个Object Identity
MutableAcl acl = jdbcMutableAclService.createAcl(objectIdentity);
acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("hr"), true);
jdbcMutableAclService.updateAcl(acl);
}

在这个过程中,会分别向 acl_entry、acl_object_identity 以及 acl_sid 三张表中添加记录