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标头。
想要了解Spring Security的过滤器链如何在Spring应用程序中发挥作用,可以阅读下面这篇博客:
Spring Security:介绍 & 初体验 & 源码与日志分析 AuthenticationEntryPoint
ExceptionTranslationFilter会使用AuthenticationEntryPoint启动身份验证方案。
public interface AuthenticationEntryPoint { void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException; }BasicAuthenticationEntryPoint
由ExceptionTranslationFilter用于通过BasicAuthenticationFilter开始身份验证。一旦使用BASIC对用户代理进行身份验证,注销需要关闭浏览器或发送未经授权的 (401) 标头。 实现后者最简单的方法是调用BasicAuthenticationEntryPoint类的commence(HttpServletRequest, HttpServletResponse, AuthenticationException)方法。 这将向浏览器指示其凭据不再被授权,导致它提示用户再次登录。
public class BasicAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { // 领域名称 private String realmName; // 检查属性 public void afterPropertiesSet() { Assert.hasText(realmName, "realmName must be specified"); } public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 填充响应 response.addHeader("WWW-Authenticate", "Basic realm="" + realmName + """); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } ... }DelegatingAuthenticationEntryPoint
AuthenticationEntryPoint实现,它根据RequestMatcher匹配选择(委托)一个具体的AuthenticationEntryPoint。
public class DelegatingAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { private final Log logger = LogFactory.getLog(getClass()); // RequestMatcher与AuthenticationEntryPoint的映射 private final linkedHashMapDigestAuthenticationEntryPointentryPoints; // 默认AuthenticationEntryPoint private AuthenticationEntryPoint defaultEntryPoint; // 构造方法 public DelegatingAuthenticationEntryPoint( linkedHashMap entryPoints) { this.entryPoints = entryPoints; } // 构造方法 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // 迭代entryPoints for (RequestMatcher requestMatcher : entryPoints.keySet()) { if (logger.isDebugEnabled()) { logger.debug("Trying to match using " + requestMatcher); } // 如果RequestMatcher匹配请求 if (requestMatcher.matches(request)) { // 获取匹配请求的RequestMatcher对应的AuthenticationEntryPoint AuthenticationEntryPoint entryPoint = entryPoints.get(requestMatcher); if (logger.isDebugEnabled()) { logger.debug("Match found! Executing " + entryPoint); } // 委托给匹配请求的RequestMatcher对应的AuthenticationEntryPoint entryPoint.commence(request, response, authException); return; } } if (logger.isDebugEnabled()) { logger.debug("No match found. Using default entry point " + defaultEntryPoint); } // 没有匹配的入口,使用defaultEntryPoint defaultEntryPoint.commence(request, response, authException); } public void setDefaultEntryPoint(AuthenticationEntryPoint defaultEntryPoint) { this.defaultEntryPoint = defaultEntryPoint; } // 检查属性 public void afterPropertiesSet() { Assert.notEmpty(entryPoints, "entryPoints must be specified"); Assert.notNull(defaultEntryPoint, "defaultEntryPoint must be specified"); } }
由SecurityEnforcementFilter用于通过DigestAuthenticationFilter开始身份验证。发送回用户代理的随机数将在setNoncevaliditySeconds(int)指示的时间段内有效,默认情况下为300秒。 如果重放攻击是主要问题,则应使用更短的时间。如果性能更受关注,则可以使用更大的值。当nonce过期时,此类正确显示stale=true标头,因此正确实施的用户代理将自动与新的nonce值重新协商(即,不向用户显示新的密码对话框)。
public class DigestAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean, Ordered { private static final Log logger = LogFactory .getLog(DigestAuthenticationEntryPoint.class); // 用于验证用户身份的字符串键值 private String key; // 领域名称 private String realmName; // nonce有效时间 private int noncevaliditySeconds = 300; // private int order = Integer.MAX_VALUE; ... // 检查属性 public void afterPropertiesSet() { if ((realmName == null) || "".equals(realmName)) { throw new IllegalArgumentException("realmName must be specified"); } if ((key == null) || "".equals(key)) { throw new IllegalArgumentException("key must be specified"); } } public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { HttpServletResponse httpResponse = response; // 计算随机数(由于代理,请勿使用远程IP地址) // 随机数格式为:base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) // 过期时间 long expiryTime = System.currentTimeMillis() + (noncevaliditySeconds * 1000); // 由下面三个步骤计算随机数 String signaturevalue = DigestAuthUtils.md5Hex(expiryTime + ":" + key); String noncevalue = expiryTime + ":" + signaturevalue; String noncevaluebase64 = new String(base64.getEncoder().encode(noncevalue.getBytes())); // 用于填充响应的验证Header String authenticateHeader = "Digest realm="" + realmName + "", " + "qop="auth", nonce="" + noncevaluebase64 + """; if (authException instanceof NonceExpiredException) { authenticateHeader = authenticateHeader + ", stale="true""; } if (logger.isDebugEnabled()) { logger.debug("WWW-Authenticate header sent to user agent: " + authenticateHeader); } // 填充响应 httpResponse.addHeader("WWW-Authenticate", authenticateHeader); httpResponse.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } // 设置key属性 public void setKey(String key) { this.key = key; } ... }Http403ForbiddenEntryPoint
在预验证的验证案例中(与CAS不同),用户已经通过某种外部机制被识别,并且在调用security-enforcement过滤器时建立了一个安全上下文。因此,此类实际上并不负责身份验证的入口,就像其他提供者的情况一样。 如果用户被AbstractPreAuthenticatedProcessingFilter拒绝,它将被调用,从而导致null身份验证。commence方法将始终返回HttpServletResponse.SC_FORBIDDEN (403 错误,除非拥有授权否则服务器拒绝提供所请求的资源)。
public class Http403ForbiddenEntryPoint implements AuthenticationEntryPoint { private static final Log logger = LogFactory.getLog(Http403ForbiddenEntryPoint.class); public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Pre-authenticated entry point called. Rejecting access"); } response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); } }HttpStatusEntryPoint
发送通用HttpStatus作为响应的AuthenticationEntryPoint。对于由于浏览器拦截响应而无法使用Basic身份验证的Javascript客户端很有用。
public final class HttpStatusEntryPoint implements AuthenticationEntryPoint { // 用于设置响应的状态码 private final HttpStatus httpStatus; public HttpStatusEntryPoint(HttpStatus httpStatus) { Assert.notNull(httpStatus, "httpStatus cannot be null"); this.httpStatus = httpStatus; } public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { // 根据httpStatus属性的值,设置响应的状态码 response.setStatus(httpStatus.value()); } }LoginUrlAuthenticationEntryPoint
由ExceptionTranslationFilter用于通过UsernamePasswordAuthenticationFilter开始表单登录身份验证。在loginFormUrl属性中保存登录表单的URL,并使用它来构建到登录页面的重定向URL。或者,可以在此属性中设置绝对URL,并将其专门使用。
使用相对URL时,可以将forceHttps属性设置为true,以强制用于登录表单的协议为HTTPS,即使原始截获的资源请求使用HTTP协议。发生这种情况时,在成功登录(通过 HTTPS)后,原始资源仍将通过原始请求URL作为HTTP访问。如果使用绝对URL,则forceHttps的值将不起作用。
public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { private static final Log logger = LogFactory .getLog(LoginUrlAuthenticationEntryPoint.class); // 向调用者提供有关哪些HTTP端口与系统上的哪些HTTPS端口相关联的信息 private PortMapper portMapper = new PortMapperImpl(); // 端口解析器,基于请求解析出端口 private PortResolver portResolver = new PortResolverImpl(); // 登陆页面URL private String loginFormUrl; // 默认为false,即不强制Https转发或重定向 private boolean forceHttps = false; // 默认为false,即不是转发到登陆页面,而是进行重定向 private boolean useForward = false; private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); public LoginUrlAuthenticationEntryPoint(String loginFormUrl) { Assert.notNull(loginFormUrl, "loginFormUrl cannot be null"); this.loginFormUrl = loginFormUrl; } // 检查属性 public void afterPropertiesSet() { Assert.isTrue( StringUtils.hasText(loginFormUrl) && UrlUtils.isValidRedirectUrl(loginFormUrl), "loginFormUrl must be specified and must be a valid redirect URL"); if (useForward && UrlUtils.isAbsoluteUrl(loginFormUrl)) { throw new IllegalArgumentException( "useForward must be false if using an absolute loginFormURL"); } Assert.notNull(portMapper, "portMapper must be specified"); Assert.notNull(portResolver, "portResolver must be specified"); } protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) { return getLoginFormUrl(); } public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { String redirectUrl = null; // 如果使用转发 if (useForward) { if (forceHttps && "http".equals(request.getScheme())) { // 首先将当前请求重定向到HTTPS // 当收到该请求时,将使用到登录页面的转发 redirectUrl = buildHttpsRedirectUrlForRequest(request); } // 重定向地址为null if (redirectUrl == null) { // 获取登陆表单URL String loginForm = determineUrlToUseForThisRequest(request, response, authException); if (logger.isDebugEnabled()) { logger.debug("Server side forward to: " + loginForm); } // RequestDispatcher用于接收来自客户端的请求并将它们发送到服务器上的任何资源 RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm); // 进行转发 dispatcher.forward(request, response); return; } } else { // 重定向到登录页面 // 如果forceHttps为真,则使用https redirectUrl = buildRedirectUrlToLoginPage(request, response, authException); } // 进行重定向 redirectStrategy.sendRedirect(request, response, redirectUrl); } // 构建重定向URL protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { // 通过determineUrlToUseForThisRequest方法获取URL String loginForm = determineUrlToUseForThisRequest(request, response, authException); // 如果是绝对URL,直接返回 if (UrlUtils.isAbsoluteUrl(loginForm)) { return loginForm; } // 如果是相对URL // 构造重定向URL int serverPort = portResolver.getServerPort(request); String scheme = request.getScheme(); RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); urlBuilder.setScheme(scheme); urlBuilder.setServerName(request.getServerName()); urlBuilder.setPort(serverPort); urlBuilder.setContextPath(request.getContextPath()); urlBuilder.setPathInfo(loginForm); if (forceHttps && "http".equals(scheme)) { Integer httpsPort = portMapper.lookupHttpsPort(serverPort); if (httpsPort != null) { // 覆盖重定向URL中的scheme和port urlBuilder.setScheme("https"); urlBuilder.setPort(httpsPort); } else { logger.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port " + serverPort); } } return urlBuilder.getUrl(); } protected String buildHttpsRedirectUrlForRequest(HttpServletRequest request) throws IOException, ServletException { int serverPort = portResolver.getServerPort(request); Integer httpsPort = portMapper.lookupHttpsPort(serverPort); if (httpsPort != null) { RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); urlBuilder.setScheme("https"); urlBuilder.setServerName(request.getServerName()); urlBuilder.setPort(httpsPort); urlBuilder.setContextPath(request.getContextPath()); urlBuilder.setServletPath(request.getServletPath()); urlBuilder.setPathInfo(request.getPathInfo()); urlBuilder.setQuery(request.getQueryString()); return urlBuilder.getUrl(); } // 通过警告消息进入服务器端转发 logger.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port " + serverPort); return null; } public void setForceHttps(boolean forceHttps) { this.forceHttps = forceHttps; } ... public void setUseForward(boolean useForward) { this.useForward = useForward; } ... }Debug分析
项目结构图:
pom.xml:
4.0.0 com.kaven security1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent2.3.6.RELEASE 8 8 org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-securityorg.projectlombok lombok
application.yml:
spring: security: user: name: kaven password: itkaven logging: level: org: springframework: security: DEBUG
MessageController(定义接口):
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 spring 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); } }formLogin
SecurityConfig(Spring Security的配置类,不是必须的,因为有默认的配置):
package com.kaven.security.config; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 任何请求都需要进行验证 http.authorizeRequests() .anyRequest() .authenticated() .and() // 记住身份验证 .rememberMe(Customizer.withDefaults()) // 基于表单登陆的身份验证方式 .formLogin(Customizer.withDefaults()); } }
Debug方式启动应用,访问http://localhost:8080/message,请求会被ExceptionTranslationFilter进行处理,该过滤器会调用身份验证入口AuthenticationEntryPoint的commence方法,该身份验证入口是LoginUrlAuthenticationEntryPoint实例,并且该实例loginFormUrl属性的值为/login。
该LoginUrlAuthenticationEntryPoint实例会将请求重定向到http://localhost:8080/login。
浏览器上的请求便被重定向到http://localhost:8080/login,输入正确的用户名和密码,点击登陆即可通过身份验证。
身份验证成功。
成功访问到资源。
修改SecurityConfig类,如下所示:
@Override protected void configure(HttpSecurity http) throws Exception { // 任何请求都需要进行验证 http.authorizeRequests() .anyRequest() .authenticated() .and() // 记住身份验证 .rememberMe(Customizer.withDefaults()) // 基于Basic方式进行身份验证 .httpBasic(Customizer.withDefaults()); }
Debug方式启动应用,访问http://localhost:8080/message,请求会被ExceptionTranslationFilter进行处理,该过滤器会调用身份验证入口AuthenticationEntryPoint的commence方法,该身份验证入口是DelegatingAuthenticationEntryPoint实例,并且该实例的defaultEntryPoint属性为BasicAuthenticationEntryPoint实例。
该DelegatingAuthenticationEntryPoint实例会委托它的defaultEntryPoint属性进行处理,即BasicAuthenticationEntryPoint实例。
Basic身份验证如下图所示:
输入用户名和密码进行登陆,登陆请求会被BasicAuthenticationFilter进行处理,该过滤器会创建UsernamePasswordAuthenticationToken实例(身份验证令牌)用于验证。
最后会验证成功。
成功访问到资源。
身份验证入口AuthenticationEntryPoint介绍与Debug分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)