Spring Security:身份验证入口AuthenticationEntryPoint介绍与Debug分析

Spring Security:身份验证入口AuthenticationEntryPoint介绍与Debug分析,第1张

Spring Security:身份验证入口AuthenticationEntryPoint介绍与Debug分析 ExceptionTranslationFilter

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应用程序中发挥作用,可以阅读下面这篇博客:

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 linkedHashMap entryPoints;
	// 默认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");
	}
}
DigestAuthenticationEntryPoint

由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
    security
    1.0-SNAPSHOT

    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.6.RELEASE
    

    
        8
        8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.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,输入正确的用户名和密码,点击登陆即可通过身份验证。

身份验证成功。

成功访问到资源。

Basic

修改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分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5712226.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存