匿名认证
概述
通常认为,采用“默认拒绝”的安全策略是一种良好的实践,在这种策略中,你明确指定允许的内容,并禁止其他所有内容。定义未认证用户可以访问的内容与此类似,特别是对于Web应用程序。许多站点要求用户必须进行认证才能访问除少数URL(例如主页和登录页面)之外的所有内容。在这种情况下,为这些特定的URL定义访问配置属性比为每个受保护资源定义更为简单。换句话说,有时最好说默认需要ROLE_SOMETHING
,并只允许某些例外情况,比如登录、注销和应用程序的主页。你也可以完全将这些页面从过滤器链中排除,从而绕过访问控制检查,但这可能出于其他原因而不被希望,特别是如果这些页面对已认证用户的行为有所不同。
这就是我们所说的匿名认证。请注意,“匿名认证”的用户和未认证的用户之间没有真正的概念差异。Spring Security 的匿名认证只是为你提供了一种更方便的方式来配置访问控制属性。即使 SecurityContextHolder
中实际上存在一个匿名认证对象,像 getCallerPrincipal
这样的 Servlet API 调用仍然返回 null。
在其他一些情况下,匿名认证也很有用,例如当审计拦截器查询 SecurityContextHolder
以确定哪个主体负责给定操作时。如果类能够确保 SecurityContextHolder
始终包含一个 Authentication
对象而永远不会是 null
,那么它们可以编写得更健壮。
配置
当你使用 HTTP 配置(在 Spring Security 3.0 中引入)时,会自动提供匿名认证支持。你可以使用 <anonymous>
元素来自定义(或禁用)它。除非你使用传统的 bean 配置,否则不必配置此处描述的 bean。
三个类协同工作以提供匿名认证功能。AnonymousAuthenticationToken
是 Authentication
的一个实现,并存储适用于匿名主体的 GrantedAuthority
实例。有一个对应的 AnonymousAuthenticationProvider
,它被链接到 ProviderManager
中,以便接受 AnonymousAuthenticationToken
实例。最后,AnonymousAuthenticationFilter
被链接在常规认证机制之后,如果 SecurityContextHolder
中没有现有的 Authentication
,则自动向其中添加一个 AnonymousAuthenticationToken
。过滤器和认证提供者的定义如下:
<bean id="anonymousAuthFilter"
class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>
<bean id="anonymousAuthenticationProvider"
class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>
key
在过滤器和身份验证提供者之间共享,以便由前者创建的令牌可以被后者接受。
key
属性的使用不应被视为提供了任何真正的安全性。它仅仅是一个记账练习。如果你在一种可能让认证客户端构造 Authentication
对象的情况下(例如通过 RMI 调用)共享一个包含 AnonymousAuthenticationProvider
的 ProviderManager
,那么恶意客户端可以提交自己创建的 AnonymousAuthenticationToken
(具有选定的用户名和权限列表)。如果 key
是可猜测的或可以被发现,该令牌将被匿名提供者接受。这在正常使用中不是问题。但是,如果你使用 RMI,你应该使用一个自定义的 ProviderManager
,省略匿名提供者,而不是共享你用于 HTTP 认证机制的那个。
userAttribute
以 usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]
的形式表示。等号后面的语法与 InMemoryDaoImpl
的 userMap
属性相同。
正如前面所解释的,匿名认证的好处是所有 URI 模式都可以应用安全性,如下例所示:
<bean id="filterSecurityInterceptor"
class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
<security:filter-security-metadata-source>
<security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
<security:intercept-url pattern='/**' access='ROLE_USER'/>
</security:filter-security-metadata-source>" +
</property>
</bean>
AuthenticationTrustResolver
匿名认证讨论的最后是 AuthenticationTrustResolver
接口及其对应的 AuthenticationTrustResolverImpl
实现。此接口提供了一个 isAnonymous(Authentication)
方法,允许感兴趣的类考虑到这种特殊的认证状态。ExceptionTranslationFilter
在处理 AccessDeniedException
实例时使用了这个接口。如果抛出了 AccessDeniedException
并且认证是匿名类型的,过滤器不会抛出 403(禁止)响应,而是启动 AuthenticationEntryPoint
以便主体可以正确地进行认证。这是一个必要的区分。否则,主体将始终被视为“已认证”,并且永远不会有机会通过表单、基本认证、摘要认证或其他正常的认证机制进行登录。
我们经常看到在早期的拦截器配置中,ROLE_ANONYMOUS
属性被替换为 IS_AUTHENTICATED_ANONYMOUSLY
,在定义访问控制时这两者实际上是相同的。这是一个使用 AuthenticatedVoter
的例子,我们在授权章节中进行了介绍。它使用 AuthenticationTrustResolver
来处理这个特定的配置属性,并授予匿名用户的访问权限。AuthenticatedVoter
方法更强大,因为它允许你区分匿名用户、记住我用户和完全认证用户。但是,如果你不需要这种功能,可以继续使用 ROLE_ANONYMOUS
,它由 Spring Security 的标准 RoleVoter
处理。
使用 Spring MVC 获取匿名认证
Spring MVC 解析类型为 Principal
的参数 使用其自己的参数解析器。
这意味着像这样的结构:
- Java
- Kotlin
@GetMapping("/")
public String method(Authentication authentication) {
if (authentication instanceof AnonymousAuthenticationToken) {
return "anonymous";
} else {
return "not anonymous";
}
}
@GetMapping("/")
fun method(authentication: Authentication?): String {
return if (authentication is AnonymousAuthenticationToken) {
"anonymous"
} else {
"not anonymous"
}
}
将始终返回 "not anonymous",即使对于匿名请求也是如此。原因是 Spring MVC 使用 HttpServletRequest#getPrincipal
解析参数,当请求是匿名时,该参数为 null
。
如果你希望在匿名请求中获取 Authentication
,请使用 @CurrentSecurityContext
代替:
- Java
- Kotlin
@GetMapping("/")
public String method(@CurrentSecurityContext SecurityContext context) {
return context.getAuthentication().getName();
}
@GetMapping("/")
fun method(@CurrentSecurityContext context : SecurityContext) : String =
context!!.authentication!!.name