Spring Security
以下介绍来自官方文档(博主稍微进行了补充):
Spring Security 是一个提供身份验证、授权和针对常见攻击进行保护的框架,对命令式(Servlet )、反应式(Webflux )应用程序都提供了一流的支持,它是保护基于Spring 的应用程序的事实标准。
Spring Security 为身份验证提供了全面的支持。身份验证是验证尝试访问特定资源的用户的身份的方式。验证用户身份的常用方法是要求用户输入用户名和密码。一旦执行身份验证,就知道其身份并可以执行授权。
Filters
Spring Security 的Servlet 支持是基于Servlet Filter 的,所以了解Filter 的作用是有助于理解Spring Security 的实现原理。下图显示了单个HTTP 请求的处理程序的典型分层。 客户端向应用程序发送一个请求,容器创建一个FilterChain ,其中包含多个Filter 和一个Servlet ,这些Filter 和Servlet 应根据请求URI 的路径处理HttpServletRequest 。在Spring MVC 应用程序中,Servlet 是DispatcherServlet 的一个实例。最多使用一个Servlet 处理单个HttpServletRequest 和HttpServletResponse 。但是,可以使用多个Filter 来:
DelegatingFilterProxy
Spring 提供了一个名为DelegatingFilterProxy 的Filter 实现,它允许在Servlet 容器的生命周期和Spring 的ApplicationContext 之间架桥。Servlet 容器允许使用自己的标准注册的Filter ,但它并不知道Spring 定义的bean ,DelegatingFilterProxy 可以通过标准的Servlet 容器机制进行注册,但是将所有工作委托给实现Filter 的Spring bean 。
下面是一张DelegatingFilterProxy 如何融入Filter 和FilterChain 的结构图。 DelegatingFilterProxy 从ApplicationContext 中查找Bean Filter0 ,然后调用Bean Filter0 。DelegatingFilterProxy 的伪代码如下所示。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response);
}
DelegatingFilterProxy 的另一个好处是它允许延迟查找Filter bean 实例。这一点很重要,因为容器需要在启动之前注册Filter 。但是,Spring 通常使用ContextLoaderListener 来加载Spring bean ,直到需要注册Filter 实例之后才会进行加载。
FilterChainProxy
Spring Security 的Servlet 支持包含在FilterChainProxy 中。FilterChainProxy 是Spring Security 提供的一种特殊Filter ,它允许通过SecurityFilterChain 委托给多个Filter 实例。由于FilterChainProxy 是一个bean ,它通常被包装在一个DelegatingFilterProxy 中。
SecurityFilterChain
FilterChainProxy 使用SecurityFilterChain 确定应为请求调用哪些Security Filter 。 SecurityFilterChain 中的Security Filter 通常是bean ,但它们是在FilterChainProxy 中注册的,而不是在DelegatingFilterProxy 中注册的。FilterChainProxy 为直接向Servlet 容器或DelegatingFilterProxy 注册Filter 提供了许多优势。
首先,它为Spring Security 的所有Servlet 支持提供了一个起点。因此,如果试图解决Spring Security 的Servlet 支持问题,那么在FilterChainProxy 中添加调试点是一个很好的起点。
其次,由于FilterChainProxy 是Spring Security 使用的核心,因此它可以执行一些必要的任务。例如,它清除SecurityContext 以避免内存泄漏。它还应用Spring Security 的HttpFirewall 保护应用程序免受某些类型的攻击。
此外,它在确定何时调用SecurityFilterChain 提供了更大的灵活性。在Servlet 容器中,仅根据URL 调用Filter 。但是,FilterChainProxy 可以通过利用RequestMatcher 接口,根据HttpServletRequest 中的任何内容确定调用。
事实上,FilterChainProxy 可以用来确定应该使用哪个SecurityFilterChain 。这允许在应用程序运行时为不同的片提供完全独立的配置。
在多个SecurityFilterChain 的视图中,FilterChainProxy 决定应该使用哪个SecurityFilterChain ,将仅调用匹配的第一个SecurityFilterChain 。如果请求的URL 为/api/messages/ ,它将首先与SecurityFilterChain0 的/api/** 模式匹配,因此仅调用SecurityFilterChain0 ,即使它也与SecurityFilterChainn 匹配。如果请求的URL 为/messages/ ,它将与SecurityFilterChain0 的/api/** 模式不匹配,因此FilterChainProxy 将继续尝试每个SecurityFilterChain ,SecurityFilterChainn 将被调用(假设在这之前没有任何一个SecurityFilterChainn 的模式与该请求匹配,SecurityFilterChainn 的/** 模式匹配该请求)。
Security Filters
Security Filters 通过SecurityFilterChain API 插入FilterChainProxy 。这些Security Filter 的顺序可以参考官方文档,数量比较多,这里就不一一列出来了。
每个Security Filter 都有各自的功能,博主以后也会介绍这些Security Filter 的功能和实现原理。
Handling Security Exceptions
ExceptionTranslationFilter (Security Filter )允许将AccessDeniedException 和AuthenticationException 转换为HTTP 响应。ExceptionTranslationFilter 作为Security Filters 之一插入到FilterChainProxy 中。
- 首先,
ExceptionTranslationFilter 调用FilterChain.doFilter(request, response) ,即调用应用程序的其余部分(出现异常才执行自己的逻辑)。 - 如果用户未经身份验证或是身份验证异常,则启动身份验证。
- 清除
SecurityContextHolder 的身份验证(SEC-112 :清除SecurityContextHolder 的身份验证,因为现有身份验证不再有效)。 - 将
HttpServletRequest 保存在RequestCache 中。当用户成功进行身份验证时,RequestCache 用于重现原始请求。 AuthenticationEntryPoint 用于从客户端请求凭据。例如,它可能会重定向到登录页面或发送WWW-Authenticate 标头。 - 否则,如果是
AccessDeniedException ,则拒绝访问。调用AccessDeniedHandler 来处理拒绝的访问。
Spring Security 的介绍就到这里,接下来博主带大家来体验一下Spring Security 的便捷。
创建工程
pom.xml :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kaven</groupId>
<artifactId>security</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
接口定义:
package com.kaven.security.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MessageController {
@GetMapping("/message")
public String getMessage() {
return "hello kaven, this is security";
}
}
启动类:
package com.kaven.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
这样一个简单的Spring Boot 工程就搭建好了。访问http://localhost:8080/message ,就会出现如下图所示的页面:
整合Spring Security
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
重新启动应用,日志输出和之前有所不同,如下图所示: 再次访问http://localhost:8080/message ,会被重定向到http://localhost:8080/login ,需要进行登录才能请求该接口,如下图所示: 可以在application.yml 配置文件中指定登录的用户名、密码以及授予该用户的权限列表:
spring:
security:
user:
name: kaven
password: itkaven
roles:
- "insert"
- "update"
- "delete"
源码与日志分析
为了探究重定向的原因,在application.yml 配置文件中加入如下所示的配置:
logging:
level:
org.springframework.security: DEBUG
该配置就是让Spring Security 项目的日志输出级别为DEBUG (默认为INFO ),可以看见更加详细的日志信息,重新启动应用,再次访问http://localhost:8080/message ,可以发现Filter Chain 中有15 个Filter 。 基于投票来决定是否授予访问权限(博主以后会详细介绍),默认情况下,没有赞成票就会抛出异常。 源码:
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
checkAllowIfAllAbstainDecisions();
}
private boolean allowIfAllAbstainDecisions = false;
protected final void checkAllowIfAllAbstainDecisions() {
if (!this.isAllowIfAllAbstainDecisions()) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
}
public boolean isAllowIfAllAbstainDecisions() {
return allowIfAllAbstainDecisions;
}
之后会进行重定向(说明执行了ExceptionTranslationFilter 的逻辑),如下图所示(会先缓存请求):
重定向到登录页面(http://localhost:8080/login ),LoginUrlAuthenticationEntryPoint 相关源码:
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
输入正确的用户名和密码后,就会被重定向到http://localhost:8080/message 。
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
DefaultSavedRequest 类被AbstractAuthenticationProcessingFilter 和SavedRequestAwareWrapper 用来在认证成功后重现请求。ExceptionTranslationFilter 在身份验证异常时存储此类的实例(缓存请求阶段)。当之前的请求与此次重定向的请求完全匹配时,就会成功获取授权,即可以成功访问到该接口。 Spring Security 的介绍、初体验以及关于重定向到登录页面的源码与日志分析就介绍到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。
|