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() 是否包含一个 GrantedAuthority,其角色与传入 isUserInRole(String) 的角色相匹配。通常,用户不应将 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)
}
}
异步 Servlet 支持
如果你使用基于Java的配置,那么你已经准备就绪。如果你使用XML配置,则需要进行一些更新。第一步是确保你已经更新了你的 web.xml 文件,以使用至少3.0版本的架构:
<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 之前,一旦 HttpServletResponse 被提交,SecurityContextHolder 中的 SecurityContext 就会自动保存。这在异步环境中可能会导致问题。请看以下示例:
- 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版本起,Spring Security 已足够智能,一旦调用 HttpServletRequest.startAsync(),便不再在提交 HttpServletResponse 时自动保存 SecurityContext。
Servlet 3.1+ 集成
以下部分描述了 Spring Security 所集成的 Servlet 3.1 方法。
HttpServletRequest#changeSessionId()
HttpServletRequest.changeSessionId() 是 Servlet 3.1 及更高版本中用于防御会话固定攻击的默认方法。