大家一起学——Spring Security(一)

316 阅读18分钟

最近在使用Spring Security的时候发现,在一些流程和架构上有些朦胧,决定复习一下,在这里记录下学习心得与理解,方便自己日后复习,也与大家共同探讨,这里是雪碧桑。

一个健壮的商业系统自然离不开优秀的认证和鉴权设计,这都是老生常谈的东西了。我们不着重讨论认证和鉴权的概念与具体方案,把重心放在SpringSecurity框架的架构原理以及使用上来。

!!!前排长文预警!!!


开始旅程

我们的旅程从一个简单的demo开始,它将展示SpringSecurity的易用性、健壮性。随后我们一步步深入源码,逐渐改进demo,在此过程中讨论Security的结构和原理。最后通过学习的知识实现SpringSecurity的最佳实践——JWT单点登录。

举个栗子

我们将使用SpringBoot + SpringSecurity来实现一个简单的demo程序。这个程序十分简单,它向外暴露一个名为为resource的Rest接口。用户要想访问resource接口,必须通过用户名+密码的认证。对,就这么简单!

编写demo

在这里我们使用Gradle作为demo程序的构建工具,如果不熟悉Gradle也没有关系,我们不会用到Gradle的高级特性。也可以使用熟悉的Maven来做构建工具。

  1. 使用IDEA 创建一个SpringBoot项目名为:security-demo

    创建项目

    在依赖选择时别忘记选择web和security依赖!

    选择依赖

    在这里我还选择了

    • Lombok——在编译时帮助你生成Getter、Setter、构造方法等的小工具

    • Spring Configuration Processor——它能使你读取yml格式的配置文件,也正是我们要使用的SpringBoot配置方式。

    点击Finish,我们的工程就建好了,只需等待Gradle引入依赖再刷新项目,我们就可以开始编码了。

  2. 编写我们的resource接口

    创建一个controller包,在其中创建一个ResourceController类并编写代码:

    创建ResourceController类

    @RestController
    @RequestMapping("resource")
    public class ResourceController {
        @GetMapping
        public String getResource(String name) {
            return "Hello world! Hello " + name + "!";
        }
    }
    

    可以看到我们的代码十分简单,它创建了一个Rest接口,指定url为resource、请求方式为Get、接收一个名为name的参数并返回一串带name的问候语。

  3. 完成了!一个需要用户名密码认证的resource接口!

测试demo

你可能会问,认证流程呢?密码验证代码呢?表单页面呢?这些Security都帮我们完成了,当我们的项目引入SpringSecurity依赖,它就会自动为我们保护所有现存的接口,并暴露一个login接口和一个简单的表单页面实现登录。当我们请求/logout时登出。用户名和密码?默认的用户名是user,密码每次启动项目时会随机生成并打印在控制台,我们现在就试试吧!

  1. 启动demo项目

  2. 在浏览器中输入localhost:8080/resource?name=sprite

    此时你会发现浏览器重定向到一个登录页面:

    登陆页面

    这里就是Security为我们生成的简易登录页面,接下来我们使用它登录。

  3. 输入用户名user,在控制台找到密码粘贴到密码输入框,点击Sign in登录

    控制台中的密码

    登录

    如果一切顺利,你就请求到了resource接口,并看到了返回的数据:

    接口请求成功

  4. 请求/logout登出

    当我们请求/logout,会看到登出的确认界面。

    登出确认页面 点击Log Out登出,再次请求resource接口就会重定向到login界面了。

    至此,我们的demo就完成了。你会发现我们并没有做多少工作,这就是Security强大的地方。它易用且健壮,在我们只是引入依赖的情况下,提供最基础的接口认证功能。

实现原理

我们暂时放下我们的demo开发,来看看Security是怎么做到如此简便的实现接口认证功能的。你会发现,在目前的情况下,我们甚至无法进行断点测试,断哪里好呢?所以在此之前,我们需要先了解一些基本概念。

Filter结构

我们在测试resource接口时发现,在没有登陆之前我们的请求被重定向到了登录页面,说明我们的请求被拦截下来了。那么在java web开发中哪里最适合拦截请求呢?答案就是Servlet Filter,还记得在没有这些安全框架时,我们是如何实现认证与鉴权的吗?不就是写一些Servlet Filter配置到Web容器拦截请求吗?Security也是这也做的,它自动配置了一个名为springSecurityFilterChain的Filter,类型是FilterChainProxy。从它的名称可以看出它只是一个代理类,并不是真正下地干活的人,我们来看看它的源码。

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    if (!clearContext) {
        doFilterInternal(request, response, chain);
        return;
    }
    try {
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        // 在这里调用了doFilterInternal方法
        doFilterInternal(request, response, chain);
    }
    catch (RequestRejectedException ex) {
        this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
    }
    finally {
        // 请求处理结束后清理认证数据
        SecurityContextHolder.clearContext();
        request.removeAttribute(FILTER_APPLIED);
    }
}

接下来看看doFilterInternal方法。

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
    List<Filter> filters = getFilters(firewallRequest);
    if (filters == null || filters.size() == 0) {
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
        }
        firewallRequest.reset();
        chain.doFilter(firewallRequest, firewallResponse);
        return;
    }
    if (logger.isDebugEnabled()) {
        logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
    }
    VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
    virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}

看起来非常乱不是吗?其实这个Filter就干了一件事:

如果filters中有Filter就用VirtualFilterChain遍历filters执行,没有就让当前的FilterChain继续执行。

VirtualFilterChain遍历filters执行完成后也会让当前的FilterChain继续执行。

这里和SpringMVC代理容器中的Filters执行有着类似的实现方式,相当于在原本的Filter链条中加入了一个子链。

总结一下,SpringSecurity自己维护了一个Filter链,这个链条的入口和出口就是FilterChainProxy,而实际做认证和鉴权操作的,是在这个链里面的Filter。

Filter链结构图

默认的Filter们

现在我们利用断点测试,看看默认情况下,Security维护了那些Filter

Security维护的Filters

可以看到一共有15个Filter,还是非常多的,其中有一部分是固定做一些通用处理的,我们马上就会说明。

在这之前需要提及一个概念,SecurityContext——安全上下文。Security会把用户认证后的相关信息储存在SecurityContext,它的存在贯穿整个请求,SecurityContextSecurityContextHolder持有,SecurityContextHolder会通过ThreadLocal的方式储存SecurityContext。这也就保证了在处理请求的任何地方都可以通过SecurityContextHolder.getContext()获取到该请求对应的SecurityContextSecurityContext默认是有的缓存机制的。在请求结束时,会把本次的SecurityContext储存在HttpSession中。下次同一个用户发起的请求时,从HttpSession中获取SecurityContext

Security提供了三种SecurityContextHolder策略,当然最常用的也是默认的是ThreadLocal的方式。

下面我们来看看SpringSecurity维护的这些Filter

  • WebAsyncManagerIntegrationFilter:将SecurityContext集成到Spring异步任务中,保证我们使用Spring的异步任务时,可以正常拿到SecurityContext中的用户信息,不是本文重点不做赘述。
  • SecurityContextPersistenceFilter:请求来临时,尝试从HttpSession获取SecurityContext,获取不到创建SecurityContext,将SecurityContext放入SecurityContextHolder。请求结束时清空SecurityContextHolder,将SecurityContext放入HttpSession

值得一提的是,Security默认使用HttpSessionSecurityContextRepository,是使用HttpSession进行SecurityContext缓存,但是时下流行无状态服务,不启用Session。之后会使用JWT标准解决这个问题。

  • HeaderWriterFilter:添加一些响应时的Header,具体取决于Security配置,不重要但必须有。

  • CsrfFilter:处理跨站请求伪造的Filter,在前后端分离的项目中设置不启用,实现方式不做赘述。

  • LogoutFilter:默认的登出操作Filter,匹配/logout请求地址,清理Session

  • UsernamePasswordAuthenticationFilter:重中之重!是我们之后的主要分析的案例!用于处理用户名和密码认证。

  • DefaultLoginPageGeneratingFilter:默认的登录页面生成。

  • DefaultLogoutPageGeneratingFilter:默认的登出页面生成。

  • BasicAuthenticationFilter:处理Basic标准下的认证,Basic是一种认证方案的标准。

  • RequestCacheAwareFilter:做请求缓存命中的Filter,发现有缓存的请求时,跳转到指定的请求。在我们登录成功之后,会自动跳转到登陆前的请求,默认情况下使用HttpSession。它与ExceptionTranslationFilter协同工作,ExceptionTranslationFilter详情看后文。

  • SecurityContextHolderAwareRequestFilter:对Request进行包装,添加一些获取认证信息的方法。

  • AnonymousAuthenticationFilter:如果之前的Filter都没有处理认证的话,这个Filter会给予请求一个匿名认证。

  • SessionManagementFilter:提供Session的固化保护和Session的并发控制,就是保证Session不会永久停留在系统中,控制Session同时存在的数量。后续会配置不启用这个Filter,因为JWT是无状态的。

  • ExceptionTranslationFilter:这也是Fliter中的一个重点,它负责拦截两种异常:AuthenticationExceptionAccessDeniedExceptionAuthenticationException会在认证不通过时抛出,AccessDeniedException会在鉴权不通过时抛出。这两种异常我们会在之后分析UsernamePasswordAuthenticationFilter时详细讲解,在这里简单了解下即可。

  • FilterSecurityInterceptor:最终干活的拦截器了,这里就是具体判断这个请求有没有认证,认证过的话有没有权限来访问指定的URL。它会根据你对Security的配置,拦截指定的请求并进行认证和鉴权的比对。

    比如我们的demo项目,Security默认匹配全路径,所以我们没有登录时,resource接口最终会被FilterSecurityInterceptor拦截下来,进行认证判断后发现用户未登录,抛出AuthenticationExceptionExceptionTranslationFilter捕获并处理,处理结果就是重定向到登录页面并缓存我们原本的请求,方便在登录成功后跳转。

demo分析

是不是到此为止还看的一头雾水?不必太在意那些默认Filter的具体细节,了解一下结构和作用即可。接下来我们将逐步分析在我们请求resource接口的时候,Security帮我们做了什么。

这里引入另一个概念——Authentication认证结果,也就是请求主体信息。我们都知道,认证意味着知道发出请求的主体是谁。Security在实现认证时,就是请求穿过一系列的Filter,如果其中一个Filter恰好可以认证当前请求主题是谁,那就认证成功了。具体到处理认证的Filter就是拿到一个请求先去判断自己是否可以认证,如果可以,认证成功就储存认证结果到SecurityContext,失败就抛出异常。之后的认证Filter检查SecurityConetxt发现已经经过认证了,就不再重复认证。我们可以通过Authentication获取一些用户的信息,比如用户名、角色、权限等等。

想象一种情况,我们没有登录系统,直接请求resource接口,这个过程中谁发挥了主要作用呢?

当我们在没有登录的情况下直接请求resource,请求一路穿过上面的层层Filter,到达AnonymousAuthenticationFilterAnonymousAuthenticationFilter检查SecurityContextHolder发现这个请求没有被前面的任何一个Filter认证,给予这个请求一个匿名认证。还记得吗?Security的默认配置下,所有的接口都是被认证保护的,即需要且只需要登录就可以请求所有的接口。所以请求到达最后一个FilterSecurityInterceptor被拦截下来进行认证比对,FilterSecurityInterceptor发现该请求持有匿名身份未认证,抛出AuthenticationExceptionAuthenticationException将会被ExceptionTranslationFilter捕获并处理,ExceptionTranslationFilterAuthenticationException交给默认的authenticationEntryPoint处理并缓存这次请求。默认的authenticationEntryPoint会将请求重定向到登录页面。所以你就看到了登录页面,而且你原本的请求也被缓存下来等待你登录成功后跳转。

接下来我们执行登录操作,看看Security会如何处理。

当我们输入用户名和密码点击登录,我们的请求以此穿过上面的Filter,到达UsernamePasswordAuthenticationFilter。这个Filter只匹配/login的Post请求,其他的一律放行。之前提到这个Filter是重中之重。

下面是一波源码解析,这边建议你先去室外吹吹风缓缓劲(:。

首先看看UsernamePasswordAuthenticationFilter的声明:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter

可以看到它继承自AbstractAuthenticationProcessingFilterdoFilter方法是由AbstractAuthenticationProcessingFilter实现的:

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
        // 不匹配/login URL直接跳过这个Filter
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
            // 这里调用了attemptAuthentication
            // attemptAuthentication是由子类实现的
            // Authentication 是认证成功之后,获取到的用户信息
            // 如果认证成功就返回Authentication,没成功就抛出异常
            // 如果既没成功也没失败就返回 null 交给之后的Filter去认证
			Authentication authenticationResult = attemptAuthentication(request, response);
            // 处理null的情况
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
            // 通过不同的Session策略,比如删除Csrf token,更换SessionID
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
            // 默认是false,大部分情况也不会修改
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
            // 验证成功
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
			unsuccessfulAuthentication(request, response, failed);
		}
        // 如果子类的处理抛出AuthenticationException 异常,执行认证失败的方法
		catch (AuthenticationException ex) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, ex);
		}
	}

再来看看验证成功后的successfulAuthentication方法:

	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
        // 将认证信息存储在SecurityContextHolder
		SecurityContextHolder.getContext().setAuthentication(authResult);
        // 打印日志
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
		}
        // 处理记住我功能
		this.rememberMeServices.loginSuccess(request, response, authResult);
        // 发布一个Spring事件
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
        // !!! 注意这里交给successHandler进行处理了
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
	}

默认的successHandlerSavedRequestAwareAuthenticationSuccessHandler,它会使请求重定向到登录之前请求的接口。

这里重定向的接口,也就是ExceptionTranslationFilter处理AuthenticationException时缓存的请求。如果你还记得RequestCacheAwareFilter的话,你就大概能猜到在默认情况下RequestCacheAwareFilter什么都不用做,因为登录成功后直接由successHandler转发到了缓存的请求。

然后是失败后处理:

	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		this.logger.trace("Failed to process authentication request", failed);
		this.logger.trace("Cleared SecurityContextHolder");
		this.logger.trace("Handling authentication failure");
		this.rememberMeServices.loginFail(request, response);
		this.failureHandler.onAuthenticationFailure(request, response, failed);
	}

和成功时一样,交给failureHandler进行处理,默认情况下就是返回401。

总结一下,AbstractAuthenticationProcessingFilter把最重要的认证交给子类去实现,自己只做了认证成功之后的处理。根据情况最终交给successHandlerfailureHandler去做后续处理。

下面我们来看看UsernamePasswordAuthenticationFilter实现的attemptAuthentication方法:

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
        // 确认是Post请求
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username : "";
		username = username.trim();
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
        // 将用户名和密码组装成UsernamePasswordAuthenticationToken
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
        // 将认证移交给getAuthenticationManager处理
		return this.getAuthenticationManager().authenticate(authRequest);
	}

我在写文章的同时一边分析代码,感觉到这里开始难度陡增,希望大家可以看懂我写的拙文。

读源码的过程真的是痛苦但收获颇丰,可以说是看到了世界的参差。

其实本质上UsernamePasswordAuthenticationToken就是一个未认证的Authentication

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer

可以看到最终的认证交给了AuthenticationManagerAuthenticationManager是一个接口,其中只有一个方法,就是对指定的Authentication进行认证,返回认证后的Authentication

public interface AuthenticationManager {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

但这里获取到的AuthenticationManager只是一个代理,是AuthenticationManager的孙子类ProviderManagerProviderManager负责为UsernamePasswordAuthenticationToken找到合适的AuthenticationManager处理认证。ProviderManager在程序中的结构如下:

image-20210827120532129

每个ProviderManager又维护了一个AuthenticationProvider集合,AuthenticationProviderAuthenticationManager差不多,也是处理认证,只是多了一个support方法,检查它是否支持处理某种Authentication的认证:

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	boolean supports(Class<?> authentication);

}

我们来看看ProviderManager的核心代码(删除了日志打印):

// 遍历AuthenticationProvider集合
for (AuthenticationProvider provider : getProviders()) {
   	// 检查AuthenticationProvider是否可以处理
    // 不能处理就跳过
    if (!provider.supports(toTest)) {
        continue;
    }
    try {
        result = provider.authenticate(authentication);
        if (result != null) {
            copyDetails(authentication, result);
            break;
        }
    }
    catch (AccountStatusException | InternalAuthenticationServiceException ex) {
        prepareException(ex, authentication);
        // SEC-546: Avoid polling additional providers if auth failure is due to
        // invalid account status
        throw ex;
    }
    catch (AuthenticationException ex) {
        lastException = ex;
    }
}
// 如果最后还是没能找到合适的AuthenticationProvider,且有parent
if (result == null && this.parent != null) {
    // Allow the parent to try.
    try {
        // 传递到parent执行
        parentResult = this.parent.authenticate(authentication);
        result = parentResult;
    }
    catch (ProviderNotFoundException ex) {
        // ignore as we will throw below if no other exception occurred prior to
        // calling parent and the parent
        // may throw ProviderNotFound even though a provider in the child already
        // handled the request
    }
    catch (AuthenticationException ex) {
        parentException = ex;
        lastException = ex;
    }
}

UsernamePasswordAuthenticationFilter把认证移交给ProviderManager处理时,会从最下面的ProviderManager开始遍历调用其中AuthenticationProvider集合的supports方法,确认AuthenticationProvider能否认证UsernamePasswordAuthenticationToken。如果当前ProviderManager中的AuthenticationProvider集合不能处理UsernamePasswordAuthenticationToken,将会继续在parent中寻找AuthenticationProvider,一直找到最顶上的ProviderManager还不能处理就返回Null,交给其他认证Filter处理。

遍历顺序大概如下:

真实画技,假一赔十

ProviderManager遍历顺序

幸运的是,我们的UsernamePasswordAuthenticationToken是有人处理的,在遍历到第二个ProviderManager时找到了它的归宿——DaoAuthenticationProvider。我们来看一下DaoAuthenticationProvider的声明:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider

以及它的父类声明:

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware

可以看到DaoAuthenticationProvider是间接实现了AuthenticationProviderauthenticate方法实现在AbstractUserDetailsAuthenticationProvider中:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 断言,只能处理UsernamePasswordAuthenticationToken的Authentication
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));
    // 通过用户名尝试读取缓存中的用户信息
    String username = determineUsername(authentication);
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    // 缓存中读取不到
    if (user == null) {
        cacheWasUsed = false;
        try {
            // 调用需要子类实现的方法来读取用户信息
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException ex) {
            this.logger.debug("Failed to find user '" + username + "'");
            if (!this.hideUserNotFoundExceptions) {
                throw ex;
            }
            throw new BadCredentialsException(this.messages    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }
    try {
        // 预先检查
        this.preAuthenticationChecks.check(user);
        // 调用需要子类实现的认证检查
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    // 如果使用了缓存中的数据认证失败,再用获取的数据重试一次(严谨
    catch (AuthenticationException ex) {
        if (!cacheWasUsed) {
            throw ex;
        }
        // There was a problem, so try again after checking
        // we're using latest data (i.e. not from the cache)
        cacheWasUsed = false;
        user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        this.preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    // 滞后检查
    this.postAuthenticationChecks.check(user);
    // 更新缓存
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

其实最重要的两方法是retrieveUseradditionalAuthenticationChecks,这两个方法都是子类来实现的,我们来看看DaoAuthenticationProvider都做了什么:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   prepareTimingAttackProtection();
   try {
       // 直接从UserDetailsService中读取用户信息
      UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
      if (loadedUser == null) {
         throw new InternalAuthenticationServiceException(
               "UserDetailsService returned null, which is an interface contract violation");
      }
      return loadedUser;
   }
   catch (UsernameNotFoundException ex) {
      mitigateAgainstTimingAttack(authentication);
      throw ex;
   }
   catch (InternalAuthenticationServiceException ex) {
      throw ex;
   }
   catch (Exception ex) {
      throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
   }
}

其实看到这里,有些拥有Security使用经验的人大都知道,UserDetailsService大部分情况都是由我们自己来提供的。目前系统默认的UserDetailsService会返回固定的用户名user+随机密码UserDetails

再来看看认证检查:

protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 检查用户输入的密码是否为null
    if (authentication.getCredentials() == null) {
        this.logger.debug("Failed to authenticate since no credentials provided");
        throw new BadCredentialsException(this.messages
      .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad 				credentials"));
    }
    // 获取用户输入的密码
    String presentedPassword = authentication.getCredentials().toString();
    // 使用passwordEncoder来对两个密码进行比对,比对不成功抛出异常
    if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        this.logger.debug("Failed to authenticate since password does not match stored value");
        throw new BadCredentialsException(this.messages                                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    }
}

同样的,拥有Security使用经验的人大都知道,passwordEncoder大部分情况下也是我们提供给Security,其中封装了密码的编码和比对算法。默认情况下Securiy使用NoOpPasswordEncoder,就是密码不编码,直接字符串比对。当然Security还贴心的为我们准备了各种开箱即用的PasswordEncoder,比如比较常用的BCryptPasswordEncoder,它使用BCrypt算法对密码进行编码和比对。BCrypt算法的细节就不多赘述了。

写到这里,我们的认证流程算是走到最低层了,密码比对成功无异常,层层返回,执行AbstractUserDetailsAuthenticationProvider

return createSuccessAuthentication(principalToReturn, authentication, user);

再从ProviderManager中层层返回到UsernamePasswordAuthenticationFilter,一直到UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter,执行其中的successfulAuthentication方法把认证结果储存在SecurityContext中,再把后续操作交给successHandler处理(默认是跳转到登陆之前请求的接口)。

这样整个用户名密码认证的流程就结束了。请求结束后,Security的第二个Filter——SecurityContextPersistenceFilter会帮我们把SecurityConetxt保存在Session中,下次用户认证直接通过Session,登出操作也只是简单的清空Session


读到这里,我想你应该了解Security最核心的认证流程。原本是想一气呵成直接写完的,结果不知不觉洋洋洒洒写了这么多了,我想还是分两篇吧,下篇我们再讨论JWT单点登录吧。

雪碧自己也是职场小白,刚刚进入行业没多久,本着大家一起学习的心态写下这篇文章。如果文章中有疏漏和错误还请大家不要吝啬批评。