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 处理认证失败的尝试,则无需捕获 |
HttpServletRequest.logout()
您可以使用 logout 方法注销当前用户。
通常,这意味着会清除 SecurityContextHolder、使 HttpSession 失效、清理所有“记住我”(Remember Me)身份验证等。
然而,所配置的 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 版本的 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 之前,SecurityContext 中的 SecurityContextHolder 会在 HttpServletResponse 一经提交时自动保存。
这在异步环境中可能会引发问题。
请考虑以下示例:
-
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()
问题是这个 Thread 不被 Spring Security 所知,因此 SecurityContext 不会传播到该线程中。
这意味着,当我们提交 HttpServletResponse 时,不存在 SecurityContext。
当 Spring Security 在提交 SecurityContext 时自动保存 HttpServletResponse,就会丢失已登录的用户信息。
从 3.2 版本起,Spring Security 已足够智能,不再在调用 SecurityContext 后,于提交 HttpServletResponse 时自动保存 HttpServletRequest.startAsync()。
Servlet 3.1+ 集成
以下部分描述了 Spring Security 所集成的 Servlet 3.1 方法。
HttpServletRequest#changeSessionId()
HttpServletRequest.changeSessionId() 是 Servlet 3.1 及更高版本中用于防范 会话固定(Session Fixation) 攻击的默认方法。