zoukankan      html  css  js  c++  java
  • Spring Security:用户名密码登陆源码

    用户名密码登陆大致流程

    image-20210303140955882

    分析

    Spring Security项目启动后,控制台会打印一些过滤器链,顺序如下

    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
    org.springframework.security.web.context.SecurityContextPersistenceFilter
    org.springframework.security.web.header.HeaderWriterFilter
    org.springframework.security.web.csrf.CsrfFilter
    org.springframework.security.web.authentication.logout.LogoutFilter
    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    org.springframework.security.web.savedrequest.RequestCacheAwareFilter
    org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
    org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
    org.springframework.security.web.authentication.AnonymousAuthenticationFilter
    org.springframework.security.web.session.SessionManagementFilter
    org.springframework.security.web.access.ExceptionTranslationFilter
    org.springframework.security.web.access.intercept.FilterSecurityInterceptor
    

    一眼看去,与用户名密码登陆有关的过滤器就是UsernamePasswordAuthenticationFilter,父类是AbstractAuthenticationProcessingFilter。看Filter的源码就要关注它的doFilter的实现,而UsernamePasswordAuthenticationFilterdoFilter在它的父类实现了,源码如下:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    			throws IOException, ServletException {
    
    		HttpServletRequest request = (HttpServletRequest) req;
    		HttpServletResponse response = (HttpServletResponse) res;
    		//判断请求是否需要认证,不需要直接放行
    		if (!requiresAuthentication(request, response)) {
    			chain.doFilter(request, response);
    			return;
    		}
    
    		if (logger.isDebugEnabled()) {
    			logger.debug("Request is to process authentication");
    		}
    
    		Authentication authResult;
    
    		try {
    			authResult = attemptAuthentication(request, response);
    			if (authResult == null) {
    				//立即返回,因为子类已指示它尚未完成身份验证
    				return;
    			}
    			sessionStrategy.onAuthentication(authResult, request, response);
    		}
    		catch (InternalAuthenticationServiceException failed) {
    			logger.error(
    					"An internal error occurred while trying to authenticate the user.",
    					failed);
                //认证失败后的处理
    			unsuccessfulAuthentication(request, response, failed);
    			return;
    		}
    		catch (AuthenticationException failed) {
                //认证失败后的处理
    			unsuccessfulAuthentication(request, response, failed);
    			return;
    		}
    		if (continueChainBeforeSuccessfulAuthentication) {
                //放行
    			chain.doFilter(request, response);
    		}
           //认证成功后的处理
    		successfulAuthentication(request, response, chain, authResult);
    	}
    

    一眼看去,就发现了问题所在:AuthenticationattemptAuthentication方法,我们看UsernamePasswordAuthenticationFilter是怎么实现的?

    	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();
            //创建AuthenticationToken,临时存储username和password
    		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    				username, password);
    
    		//设置详情
    		setDetails(request, authRequest);
    
    		return this.getAuthenticationManager().authenticate(authRequest);
    	}
    

    setDetails方法

    	protected void setDetails(HttpServletRequest request,
    			UsernamePasswordAuthenticationToken authRequest) {
    		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    	}
    

    buildDetails方法在WebAuthenticationDetailsSource实现

    public class WebAuthenticationDetailsSource implements
    		AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    
    	public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
    		return new WebAuthenticationDetails(context);
    	}
    }
    

    WebAuthenticationDetails的构造方法:主要是设置远程请求ip和sessionId

    	public WebAuthenticationDetails(HttpServletRequest request) {
    		this.remoteAddress = request.getRemoteAddr();
    		HttpSession session = request.getSession(false);
    		this.sessionId = (session != null) ? session.getId() : null;
    	}
    

    所以setDetails方法主要是设置远程请求ip和sessionId

    继续往下看attemptAuthentication方法:

    return this.getAuthenticationManager().authenticate(authRequest);
    

    this.getAuthenticationManager()会拿到ProviderManager类。为什么会默认获取到ProviderManager,可以关注WebSecurityConfigurerAdapter类,流程有点复杂,这里不赘述(有兴趣可以自己打断点调试)。

    获取到ProviderManager后,执行它的authenticate方法。以下是authenticate的部分代码:

    第一步:

    		Class<? extends Authentication> toTest = authentication.getClass();
    		AuthenticationException lastException = null;
    		AuthenticationException parentException = null;
    		Authentication result = null;
    		Authentication parentResult = null;
    		boolean debug = logger.isDebugEnabled();		
            for (AuthenticationProvider provider : getProviders()) {
                //拿到所有的provider,逐一匹配provider
    			if (!provider.supports(toTest)) {
    				continue;
    			}
    
    			if (debug) {
    				logger.debug("Authentication attempt using "
    						+ provider.getClass().getName());
    			}
                //匹配成功后,开始执行authenticate
    			try {
                    //这里provider会调用AbstractUserDetailsAuthenticationProvider的认证方法
    				result = provider.authenticate(authentication);
    
    				if (result != null) {
    					copyDetails(authentication, result);
    					break;
    				}
    			}
    			catch (AccountStatusException | InternalAuthenticationServiceException e) {
    				prepareException(e, authentication);
    				// SEC-546: Avoid polling additional providers if auth failure is due to
    				// invalid account status
    				throw e;
    			} catch (AuthenticationException e) {
    				lastException = e;
    			}
    		}
    

    源码如下:

    	public Authentication authenticate(Authentication authentication)
    			throws AuthenticationException {
    		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    				() -> messages.getMessage(
    						"AbstractUserDetailsAuthenticationProvider.onlySupports",
    						"Only UsernamePasswordAuthenticationToken is supported"));
    
    		// Determine username
    		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
    				: authentication.getName();
    
    		boolean cacheWasUsed = true;
            //从缓存中查询
    		UserDetails user = this.userCache.getUserFromCache(username);
    
    		if (user == null) {
    			cacheWasUsed = false;
    
    			try {
                    //从数据库中根据用户名查询用户
    				user = retrieveUser(username,
    						(UsernamePasswordAuthenticationToken) authentication);
    			}
    			catch (UsernameNotFoundException notFound) {
    				logger.debug("User '" + username + "' not found");
    
    				if (hideUserNotFoundExceptions) {
    					throw new BadCredentialsException(messages.getMessage(
    							"AbstractUserDetailsAuthenticationProvider.badCredentials",
    							"Bad credentials"));
    				}
    				else {
    					throw notFound;
    				}
    			}
    		}
    
    		try {
                //这里的user可能是从数据库查到的,也可能是从缓存中拿到的
                //前置检查
    			preAuthenticationChecks.check(user);
                //匹配用户输入密码和数据库中的密码
    			additionalAuthenticationChecks(user,
    					(UsernamePasswordAuthenticationToken) authentication);
    		}
    		catch (AuthenticationException exception) {
                //这里是因为可能缓存和数据库密码不一致抛出异常,所以在这里直接查询数据库再进行比对
    			if (cacheWasUsed) {
    				// 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);
    				preAuthenticationChecks.check(user);
    				additionalAuthenticationChecks(user,
    						(UsernamePasswordAuthenticationToken) authentication);
    			}
    			else {
    				throw exception;
    			}
    		}
    		//post和pre:认证检查,主要检查用户有没有过期,有没有被锁住,用户是否可用等等
    		postAuthenticationChecks.check(user);
    
    		if (!cacheWasUsed) {
                //放入缓存
    			this.userCache.putUserInCache(user);
    		}
    
    		Object principalToReturn = user;
    
    		if (forcePrincipalAsString) {
    			principalToReturn = user.getUsername();
    		}
    		//创建Authentication(UsernamePasswordAuthenticationToken)
    		return createSuccessAuthentication(principalToReturn, authentication, user);
    	}
    

    retrieveUser方法默认由DaoAuthenticationProvider实现,源码如下:

    	protected final UserDetails retrieveUser(String username,
    			UsernamePasswordAuthenticationToken authentication)
    			throws AuthenticationException {
    		prepareTimingAttackProtection();
    		try {
                //调用UserDetailsService的loadUserByUsername方法,查询到UserDetails
    			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);
    		}
    	}
    

    AbstractUserDetailsAuthenticationProvidercreateSuccessAuthentication源码:

    	protected Authentication createSuccessAuthentication(Object principal,
    			Authentication authentication, UserDetails user) {
    		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
    				principal, authentication.getCredentials(),
    				authoritiesMapper.mapAuthorities(user.getAuthorities()));
    		result.setDetails(authentication.getDetails());
    
    		return result;
    	}
    

    实际上DaoAuthenticationProvider也对createSuccessAuthentication方法进行了重写,主要对修改密码情况进行了处理,无关痛痒。

  • 相关阅读:
    Vue
    Vue
    Vue
    Vue
    Vue
    Vue
    Vue
    Vue
    Vue
    建立索引该如何选取字段
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/14474866.html
Copyright © 2011-2022 走看看