Servlet API 集成

Servlet 2.5+ 集成

本节介绍 Spring Security 如何与 Servlet 2.5 规范集成。

HttpServletRequest.getRemoteUser()

HttpServletRequest.getRemoteUser() 返回 SecurityContextHolder.getContext().getAuthentication().getName() 的结果,这通常是当前用户名。如果您想在应用程序中显示当前用户名,这会很有用。此外,您可以检查此返回值是否为 null,以确定用户是否已认证或是否为匿名用户。了解用户是否已认证对于确定是否应显示某些 UI 元素很有用(例如,仅当用户已认证时才应显示的注销链接)。

HttpServletRequest.getUserPrincipal()

HttpServletRequest.getUserPrincipal() 返回 SecurityContextHolder.getContext().getAuthentication() 的结果。这意味着它是一个 Authentication 对象,在使用基于用户名和密码的认证时,通常是 UsernamePasswordAuthenticationToken 的实例。如果您需要用户的额外信息,这会很有用。例如,您可能创建了一个自定义的 UserDetailsService,它返回包含用户名字和姓氏的自定义 UserDetails。您可以通过以下方式获取此信息:

  • Java

  • Kotlin

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();
val auth: Authentication = httpServletRequest.getUserPrincipal()
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
val userDetails: MyCustomUserDetails = auth.principal as MyCustomUserDetails
val firstName: String = userDetails.firstName
val lastName: String = userDetails.lastName

应该注意的是,在应用程序中执行如此多逻辑通常不是一个好的实践。相反,应该将其集中化以减少 Spring Security 和 Servlet API 的耦合。

HttpServletRequest.isUserInRole(String)

HttpServletRequest.isUserInRole(String) 用于确定 SecurityContextHolder.getContext().getAuthentication().getAuthorities() 是否包含传入 isUserInRole(String) 方法的角色的 GrantedAuthority。通常,用户不应该向此方法传递 ROLE_ 前缀,因为它会自动添加。例如,如果您想确定当前用户是否具有 "ROLE_ADMIN" 权限,可以使用以下方式:

  • Java

  • Kotlin

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");
val isAdmin: Boolean = httpServletRequest.isUserInRole("ADMIN")

这对于确定是否应该显示某些 UI 组件会很有用。例如,您可能仅在当前用户是管理员时才显示管理员链接。

Servlet 3+ 集成

以下部分描述了 Spring Security 集成的 Servlet 3 方法。

HttpServletRequest.authenticate(HttpServletResponse)

您可以使用 HttpServletRequest.authenticate(HttpServletResponse) 方法来确保用户已认证。如果他们未认证,将使用配置的 AuthenticationEntryPoint 来请求用户进行认证(重定向到登录页面)。

HttpServletRequest.login(String,String)

您可以使用 HttpServletRequest.login(String,String) 方法使用当前的 AuthenticationManager 认证用户。例如,以下代码将尝试使用用户名 user 和密码 password 进行认证:

  • Java

  • Kotlin

try {
httpServletRequest.login("user","password");
} catch(ServletException ex) {
// fail to authenticate
}
try {
    httpServletRequest.login("user", "password")
} catch (ex: ServletException) {
    // fail to authenticate
}

如果您希望 Spring Security 处理失败的认证尝试,则无需捕获 ServletException

HttpServletRequest.logout()

您可以使用 HttpServletRequest.logout() 方法注销当前用户。

通常,这意味着 SecurityContextHolder 被清除,HttpSession 失效,所有“记住我”的认证都被清理等等。然而,配置的 LogoutHandler 实现会根据您的 Spring Security 配置而有所不同。请注意,在调用 HttpServletRequest.logout() 后,您仍然负责写出响应。通常,这会涉及重定向到欢迎页面。

AsyncContext.start(Runnable)

AsyncContext.start(Runnable) 方法确保您的凭据传播到新的 Thread。通过使用 Spring Security 的并发支持,Spring Security 重写了 AsyncContext.start(Runnable),以确保在处理 Runnable 时使用当前的 SecurityContext。以下示例输出了当前用户的 Authentication:

  • Java

  • Kotlin

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
	public void run() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		try {
			final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
			asyncResponse.setStatus(HttpServletResponse.SC_OK);
			asyncResponse.getWriter().write(String.valueOf(authentication));
			async.complete();
		} catch(Exception ex) {
			throw new RuntimeException(ex);
		}
	}
});
val async: AsyncContext = httpServletRequest.startAsync()
async.start {
    val authentication: Authentication = SecurityContextHolder.getContext().authentication
    try {
        val asyncResponse = async.response as HttpServletResponse
        asyncResponse.status = HttpServletResponse.SC_OK
        asyncResponse.writer.write(String.valueOf(authentication))
        async.complete()
    } catch (ex: Exception) {
        throw RuntimeException(ex)
    }
}

Async Servlet 支持

如果您使用基于 Java 的配置,则已准备就绪。如果您使用 XML 配置,则需要进行一些更新。第一步是确保您已将 web.xml 文件更新为使用至少 3.0 的 schema:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

</web-app>

接下来,您需要确保您的 springSecurityFilterChain 已设置为处理异步请求:

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
	org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

现在 Spring Security 确保您的 SecurityContext 也会在异步请求中传播。

那么它是如何工作的呢?如果您不是真的感兴趣,可以随意跳过本节的其余部分 大部分功能内置于 Servlet 规范中,但 Spring Security 做了一些调整,以确保异步请求能够正常工作。在 Spring Security 3.2 之前,SecurityContextHolder 中的 SecurityContextHttpServletResponse 提交时会自动保存。这在异步环境中可能会导致问题。考虑以下示例:

  • Java

  • Kotlin

httpServletRequest.startAsync();
new Thread("AsyncThread") {
	@Override
	public void run() {
		try {
			// Do work
			TimeUnit.SECONDS.sleep(1);

			// Write to and commit the httpServletResponse
			httpServletResponse.getOutputStream().flush();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}.start();
httpServletRequest.startAsync()
object : Thread("AsyncThread") {
    override fun run() {
        try {
            // Do work
            TimeUnit.SECONDS.sleep(1)

            // Write to and commit the httpServletResponse
            httpServletResponse.outputStream.flush()
        } catch (ex: java.lang.Exception) {
            ex.printStackTrace()
        }
    }
}.start()

问题在于 Spring Security 不知道这个 Thread,因此 SecurityContext 未传播到它。这意味着,当我们提交 HttpServletResponse 时,没有 SecurityContext。当 Spring Security 在提交 HttpServletResponse 时自动保存 SecurityContext 时,它会丢失已登录的用户。

从 3.2 版本开始,一旦调用 HttpServletRequest.startAsync(),Spring Security 足够智能,不再在提交 HttpServletResponse 时自动保存 SecurityContext

Servlet 3.1+ 集成

以下部分描述了 Spring Security 集成的 Servlet 3.1 方法。

HttpServletRequest#changeSessionId()

HttpServletRequest.changeSessionId() 是 Servlet 3.1 及更高版本中,防御 会话固定 (Session Fixation) 攻击的默认方法。