1.前言
本文主要讲解 spring-security 在不做任何配置情况下,它的启动流程和认证过程。
1. 准备工作
这里是基于springboot 2.2.5版本对应 spring-security 5.2.2版本演示的 (按我下面导入即可,版本是它自己匹配的)
引入依赖
<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.2.5.RELEASE</spring-boot.version> </properties> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
编写程序
这里我们直接创建一个springboot 启动程序即可,可以不做任何配置,在导入spring-security 依赖后,它会自动给我们进行了一些默认配置。我在这里简单配置了一下端口号
# 应用名称 spring.application.name=spring-security # 应用服务 WEB 访问端口 server.port=8082
编写一个controller
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } }
- 启动项目
2. 访问 /hello
我们访问 http://localhost:8082/hello , 发现它会给我们重定向到 http://localhost:8082/login 这个页面。
-思考:我们只写了一个处理 /hello 的方法
2. 流程分析
- 我们都知道spring-security 主要是基于 一层层的 Filters 来对web 请求做处理的,完成其中的认证授权等一系列功能。
1. 自定义一个Filter
@Component
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("进入自定义的filter");
filterChain.doFilter(servletRequest,servletResponse);
}
}
我们自定义的过滤器逻辑很简单,只做一个简单的输出 (在这个地方打个断点,方便我们后续调试),然后放行,把它添加到容器中
@Configuration
public class MyConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
// 这个方法表示把我们自定义的过滤器 加在 UsernamePasswordAuthenticationFilter 这个过滤器之前
http.addFilterBefore(new MyFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
2. debug模式重启项目,访问 http://localhost:8082/login ,随便输入参数,提交表单
- 我们点开filterChain(过滤器链,主要是用来管理过滤器的),我们发现除掉我们自定义的过滤器,它自己给我们添加了十五个 过滤器
至于为什么是这个顺序?
FilterComparator() {
FilterComparator.Step order = new FilterComparator.Step(100, 100);
this.put(ChannelProcessingFilter.class, order.next());
this.put(ConcurrentSessionFilter.class, order.next());
this.put(WebAsyncManagerIntegrationFilter.class, order.next());
this.put(SecurityContextPersistenceFilter.class, order.next());
this.put(HeaderWriterFilter.class, order.next());
this.put(CorsFilter.class, order.next());
this.put(CsrfFilter.class, order.next());
this.put(LogoutFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next());
this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter", order.next());
this.put(X509AuthenticationFilter.class, order.next());
this.put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter", order.next());
this.put(UsernamePasswordAuthenticationFilter.class, order.next());
this.put(ConcurrentSessionFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
this.put(DefaultLoginPageGeneratingFilter.class, order.next());
this.put(DefaultLogoutPageGeneratingFilter.class, order.next());
this.put(ConcurrentSessionFilter.class, order.next());
this.put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());
this.put(BasicAuthenticationFilter.class, order.next());
this.put(RequestCacheAwareFilter.class, order.next());
this.put(SecurityContextHolderAwareRequestFilter.class, order.next());
this.put(JaasApiIntegrationFilter.class, order.next());
this.put(RememberMeAuthenticationFilter.class, order.next());
this.put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next());
this.put(SessionManagementFilter.class, order.next());
this.put(ExceptionTranslationFilter.class, order.next());
this.put(FilterSecurityInterceptor.class, order.next());
this.put(SwitchUserFilter.class, order.next());
}
3. WebAsyncManagerIntegrationFilte
- GenericFilterBean 子类,将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager(spring 中用于管理异步请求的核心类) 进行集成。
4. SecurityContextPersistenceFilter
- GenericFilterBean 子类,在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个仓储中,然后将SecurityContextHolder中的信息清除
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// FILTER_APPLIED : 如果有值,说明当前请求已经被这个 Filter 拦截过,直接放行
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
// 给 FILTER_APPLIED 设置一个值,表示当前请求已被 当前Filter 处理过
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// private boolean forceEagerSessionCreation = false;
// 默认是false , 日志记录一下 早起创建的session
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
// 创建一个 HttpRequestResponseHolder
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
// private SecurityContextRepository repo; 默认实现是 : HttpSessionSecurityContextRepository
// 初次进入的时候,它会先尝试从session 中获取,第一次获取不到,它会创建一个默认的没有权限认证的 SecurityContext
// 认证通过后 它会保存在sesion 中,下次进入可以直接取出来
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 把从对应的 SecurityContextRepository 获取的securityContext存入SecurityContextHolder中
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 放行
chain.doFilter(holder.getRequest(), holder.getResponse());
}
// 这里代码 是当当前请求被所有过滤器处理完毕后,才会执行的
finally {
// 从 SecurityContextHolder 中获取 所有Filter执行完毕后的 SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
// 清除 SecurityContextHolder 中的 SecurityContext
SecurityContextHolder.clearContext();
// 把 前面获取的 SecurityContext 保存到 SecurityContextRepository repo
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
// 清除掉这个标记,下次进入还会拦截
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
5. HeaderWriterFilter
GenericFilterBean 子类,主要是处理请求头信息的
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// shouldWriteHeadersEagerly : 指示是否需要在请求开始前,写入请求头数据
if (this.shouldWriteHeadersEagerly) {
// 如果需要的话 就写入请求头放行
doHeadersBefore(request, response, filterChain);
} else {
// 如果不需要,它会把请求包装一下,等所有过滤器链执行完毕后,在写入请求头信息
doHeadersAfter(request, response, filterChain);
}
}
private void doHeadersBefore(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
writeHeaders(request, response);
filterChain.doFilter(request, response);
}
private void doHeadersAfter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,
response);
HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request,
headerWriterResponse);
try {
filterChain.doFilter(headerWriterRequest, headerWriterResponse);
} finally {
headerWriterResponse.writeHeaders();
}
}
6. CsrfFilter
GenericFilterBean 子类,用于处理跨站请求伪造
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
// 从token 仓库中查看这个请求是否携带有 csrfToken
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
// 如果没有的话,就给这个 request 创建一个默认的token,uuid随机生成的
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
// 把token 信息设置到 请求中
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
// 决定策略实现的规则是否与提供的请求匹配。 如果匹配直接放行
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 根据前面设置的csrfToken ,从请求头中获取名为csrfToken.getHeaderName()的属性值 默认"X-CSRF-TOKEN"
String actualToken = request.getHeader(csrfToken.getHeaderName());
// 如果为null的话 就会从请求参数中获取
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
// 这里会拿 actualToken 和 前面创建的默认的 csrfToken的token 进行比对,如果不相等,说明这里被修改过 打印日志,回显异常
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
// 走到这里说明校验通过,放行
filterChain.doFilter(request, response);
}
7. LogoutFilter
主要是用来处理登出的。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 确认是否进行登出操作,里面主要是一些路径方法匹配
if (requiresLogout(request, response)) {
// SecurityContextHolder的容器中的一些权限认证信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}
// 这个LogoutHandler是一个复合handler 里面维护了一个 List<LogoutHandler>
// 遍历执行每一个 LogoutHandler 的登出操作,处理各种认证信息
this.handler.logout(request, response, auth);
// 默认情况下是一个 SimpleUrlLogoutSuccessHandler,处理重定向信息
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
// 如果没有登出操作直接放行。
chain.doFilter(request, response);
}
8. UsernamePasswordAuthenticationFilter
这个应该是我们大家比较了解,也是重写比较多的一个 Filter,用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理post方式“/login”的请求。
从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 校验它是 post 方式
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 从request 中获取 参数名为 username,和 password 对应的参数
String username = obtainUsername(request);
String password = obtainPassword(request);
// 当它们是null时,给他们一个 ""字符串
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 创建一个 UsernamePasswordAuthenticationToken 这里会保存用户的权限认证等信息
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 默认会得到 一个 ProviderManager,调用它的 authenticate(),因为我们没配置他第一次进入是会
// 从 InMemoryUserDetailsManager的一个 map中根据用户名查找用户信息,用户名:user,密码:前面启动项目打印的那个
// 拿着这个查找后的用户信息,和我们输入的密码进行比对,相同则认证成功,否则失败
return this.getAuthenticationManager().authenticate(authRequest);
}
this.getAuthenticationManager()
-----》ProviderManager
----》result = provider.authenticate(authentication)
-----》 AbstractUserDetailsAuthenticationProvider
----》UserDetails user = this.userCache.getUserFromCache(username); 先从缓存中判断没有再往下执行
----》 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
—》DaoAuthenticationProvider
—》UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
—》InMemoryUserDetailsManager
—》UserDetails user = (UserDetails)this.users.get(username.toLowerCase()); 这个users是一个map,通过username 从这里面获取 用户信息
// private final Map<String, MutableUserDetails> users = new HashMap();
—》接着回到 AbstractUserDetailsAuthenticationProvider
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); 检查当前用户密码和从map中查到的信息是否匹配
9. DefaultLoginPageGeneratingFilter
这个是当我们没有配置登录页面时,系统会在启动的时候帮我们加入这个过滤器,帮我们生成一个默认的登录页面。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
// 访问登录页 || 或者前面登录失败 || 登录退出成功
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
// 只有这几种情况会生成一个登录页面 返回给前台
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
return;
}
10 DefaultLogoutPageGeneratingFilter
当我们没有配置退出登录页面时,生成一个的用户退出登录页面,默认情况下,当用户请求为GET /logout时,该过滤器会起作用,生成并展示相应的用户退出登录表单页面。
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.matcher.matches(request)) {
renderLogout(request, response);
} else {
filterChain.doFilter(request, response);
}
}
private void renderLogout(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String page = "<!DOCTYPE html>\n"
+ "<html lang=\"en\">\n"
+ " <head>\n"
+ " <meta charset=\"utf-8\">\n"
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"
+ " <meta name=\"description\" content=\"\">\n"
+ " <meta name=\"author\" content=\"\">\n"
+ " <title>Confirm Log Out?</title>\n"
+ " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
+ " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
+ " </head>\n"
+ " <body>\n"
+ " <div class=\"container\">\n"
+ " <form class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n"
+ " <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n"
+ renderHiddenInputs(request)
+ " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n"
+ " </form>\n"
+ " </div>\n"
+ " </body>\n"
+ "</html>";
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(page);
}
11 BasicAuthenticationFilter
这个拦截器主要是处理请求头上认证信息,如果有,则通过basic64 编码器解密请求头所携带的信息,
封装成一个 UsernamePasswordAuthenticationToken 对象,然后比对认证是否成功,认证成功的话把认证成功的信息放到当前上下文中。
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
try {
// 尝试获取请求头封装的信息返回的 UsernamePasswordAuthenticationToken ,若为null,直接放行,详解见下面
UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);
// 如果没有直接返回
if (authRequest == null) {
chain.doFilter(request, response);
return;
}
// 获取用户名
String username = authRequest.getName();
if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}
// 仅当用户名与SecurityContextHolder和用户不匹配时或者用户名没经过认证 返回true 需进行验证
if (authenticationIsRequired(username)) {
// 具体的校验逻辑 可自己重写,认证成功后把它放到容器中即可。
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}
return;
}
chain.doFilter(request, response);
}
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
// public static final String AUTHORIZATION = "Authorization";
String header = request.getHeader(AUTHORIZATION);
// 如果这个 Authorization 请求头为null 直接返回。
if (header == null) {
return null;
}
// 获取 Authorization Basic 后面的信息
header = header.trim();
if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
return null;
}
// base64 解密请求头认证信息
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
}
catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}
// getCredentialsCharset(request) 默认 UTF-8
String token = new String(decoded, getCredentialsCharset(request));
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
// 构造这个对象 UsernamePasswordAuthenticationToken 最后返回
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim), token.substring(delim + 1));
result.setDetails(this.authenticationDetailsSource.buildDetails(request));
return result;
}
12 RequestCacheAwareFilter
主要作用是用于用户登录成功后,重新恢复因为登录被打断的请求,被打断也是有前提条件的,支持打断后可以被恢复的异常有AuthenticationException、AccessDeniedException,这个操作是ExceptionTranslationFilter中触发的,并且RequestCacheAwareFilter只支持GET方法,而默认TokenEndpoint支持Post获取Token信息,进行登录.
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 匹配有没有需要恢复登录的请求,有的话直接替换到请求路径
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
13 SecurityContextHolderAwareRequestFilter
Spring Security TokenEndpoint中获取token的请求,有这样一个参数:Principal。 对于一个普通HttpServletRequest,是没有Principal参数类型的。SecurityContextHolderAwareRequestFilter通过HttpServletRequestFactory将HttpServletRequest请求包装成SecurityContextHolderAwareRequestWrapper,它实现了HttpServletRequest,并进行了扩展,添加一些额外的方法,比如:getPrincipal()方法等。这样就可以那些需要Principal等参数的Controller就可以接收到对应参数了。除了这个地方的应用,在其他地方,也可以直接调用request#getUserPrincipal()获取对应信息。