文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Security登录认证流程详细分析详解

2024-04-02 19:55

关注

最近在写毕业设计的时候用这个框架,小伙伴给我提了个多种登录方式的需求,说仅仅只有账号、密码登录不太行,说让我增加几种方式,如:手机短信验证登录、邮箱验证登录、第三方登录等等(前两个已经实现,第三方登录还没搞定)一开始也挺让人懵逼,无从下手的。

看了好几篇博客,都弄的不完整,或者就是太高级了,我不太能行。之后就是看博客,说弄懂原理、流程后,写多种方式其实也蛮简单。然后我就老老实实的去Debug了。

这样子的效果是十分好的,多Debug几回,无论是对使用,还是对于编写代码,以及对这个技术的理解都会加深一些,以前一些迷惑也会恍然大悟。

Debug的过程要找到一个脉络,不要心急,前期多做个笔记,不会多查一下,那样一切都会显得非常轻松的。

前文:?SpringBoot整合Security,实现权限控制

本文适合需要入门及已经会简单使用Security的小伙伴们。

对于一门技术,会使用是说明我们对它已经有了一个简单了解,把脉络都掌握清楚,我们才能更好的使用它,以及更好的实现定制化。

接下来就让?来带大家一起看看吧。

Security如何处理表单提交账号和密码,以及保存用户身份信息的。

如有不足之处,请大家批评指正。

一、?前言:流程图:

image-20210911135505297

二、?前台发送请求

用户向/login接口使用POST方式提交用户名、密码。/login是没指定时默认的接口

三、请求到达UsernamePasswordAuthenticationFilter过滤器

请求首先会来到:?UsernamePasswordAuthenticationFilter


public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
			"POST");

    //可以通过对应的set方法修改
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

	private boolean postOnly = true;

       //  初始化一个用户密码 认证过滤器  默认的登录uri 是 /login 请求方式是POST
	public UsernamePasswordAuthenticationFilter() {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

	public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		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 : "";
       	//把账号名、密码封装到一个认证Token对象中,这是一个通行证,但是此时的状态时不可信的,通过认证后才会变为可信的
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
		// Allow subclasses to set the "details" property
        //记录远程地址,如果会话已经存在(它不会创建),还将设置会话 ID
		setDetails(request, authRequest);
        //使用 父类中的 AuthenticationManager 对Token 进行认证 
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	
	@Nullable
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(this.passwordParameter);
	}
	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(this.usernameParameter);
	}

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

四、?制作UsernamePasswordAuthenticationToken

将获取到的数据制作成一个令牌UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

之前我们在图中讲了我们实际封装的是一个Authentication对象,UsernamePasswordAuthenticationToken是一个默认实现类。

我们简单看一下他们的结构图:

image-20210910154410287

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // 这里就是用户名和密码 自定义时 根据自己需求进行重写
	private final Object principal;
	private Object credentials;

	
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}
	// ...
}

目前是处于未授权状态的。我们后面要做的就是对它进行认证授权。

五、?父类中的 AuthenticationManager 对Token 进行认证

AuthenticationManager是身份认证器,认证的核心接口

我们继续对return this.getAuthenticationManager().authenticate(authRequest);进行分析.

//我们可以看到 AuthenticationManager 实际上就是一个接口,所以它并不做真正的事情,只是提供了一个标准,我们就继续去看看它的实现类,看看是谁帮它做了事。
public interface AuthenticationManager {
    //尝试对传递的Authentication对象进行身份Authentication ,如果成功则返回完全填充的Authentication对象(包括授予的权限)。
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

六、我们找到了AuthenticationManager 实现类ProviderManager

我们找到ProviderManager实现了AuthenticationManager。(但是你会发现它也不做事,又交给了别人做?)

ProviderManager并不是自己直接对请求进行验证,而是将其委派给一个 AuthenticationProvider列表。列表中的每一个 AuthenticationProvider将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication对象的所有属性。

在这个阅读中,我删除了许多杂七杂八的代码,一些判断,异常处理,我都去掉了,只针对最重要的那几个看。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    //省略了一些代码
    private List<AuthenticationProvider> providers = Collections.emptyList();
    
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
 
        //我们遍历AuthenticationProvider 列表中每个Provider依次进行认证
       // 不过你会发现 AuthenticationProvider 也是一个接口,它的实现类才是真正做事的人 ,下文有
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			//...
			try {
                //provider.authenticate()
                //参数:身份验证 - 身份验证请求对象。
                //返回:一个完全经过身份验证的对象,包括凭据。 如果AuthenticationProvider无法支持对传递的Authentication对象进行身份验证,则可能返回null  ,我们接着看它的实现类是什么样子的
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
			//....
			}
		}
        // 如果 AuthenticationProvider 列表中的Provider都认证失败,且之前有构造一个 AuthenticationManager 实现类,那么利用AuthenticationManager 实现类 继续认证
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
			// ...
			}
		}
         //认证成功
		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				//成功认证后删除验证信息
				((CredentialsContainer) result).eraseCredentials();
			}
            //发布登录成功事件
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}
		// 没有认证成功,抛出异常
		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}
		prepareException(lastException, authentication);
		throw lastException;
	}
}

七、?AuthenticationProvider接口

public interface AuthenticationProvider {

	
	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	
	boolean supports(Class<?> authentication);

}

注意boolean supports(Class<?> authentication);方式上完整JavaDoc的注释是:

如果有多个 AuthenticationProvider都支持同一个Authentication 对象,那么第一个 能够成功验证Authentication的 Provder 将填充其属性并返回结果从而覆盖早期支持的 AuthenticationProvider抛出的任何可能的 AuthenticationException。一旦成功验证后,将不会尝试后续的 AuthenticationProvider。如果所有的 AuthenticationProvider都没有成功验证 Authentication,那么将抛出最后一个Provider抛出的AuthenticationException。(AuthenticationProvider可以在Spring Security配置类中配置)

机译不是很好理解,我们翻译成通俗易懂点:

当然有时候我们有多个不同的 AuthenticationProvider,它们分别支持不同的 Authentication对象,那么当一个具体的 AuthenticationProvier传进入 ProviderManager的内部时,就会在 AuthenticationProvider列表中挑选其对应支持的provider对相应的 Authentication对象进行验证

这个知识和实现多种登录方式相关联,我简单的说一下我的理解。

我们这里讲解的是默认的登录方式,用到的是UsernamePasswordAuthenticationFilter和UsernamePasswordAuthenticationToken以及后文中的DaoAuthenticationProvider这些,来进行身份的验证,但是如果我们后期需要添加手机短信验证码登录或者邮件验证码或者第三方登录等等。

那么我们也会重新继承AbstractAuthenticationProcessingFilter、AbstractAuthenticationToken、AuthenticationProvider进行重写,因为不同的登录方式认证逻辑是不一样的,AuthenticationProvider也会不一样,我们使用用户名和密码登录,Security 提供了一个 AuthenticationProvider的简单实现 DaoAuthenticationProvider,它使用了一个 UserDetailsService来查询用户名、密码和 GrantedAuthority,实际使用中我们都会实现UserDetailsService接口,从数据库中查询相关用户信息,AuthenticationProvider的认证核心就是加载对应的 UserDetails来检查用户输入的密码是否与其匹配。

流程图大致如下:

八、?DaoAuthenticationProvider

AuthenticationProvider它的实现类、继承类很多,我们直接看和User相关的,会先找到AbstractUserDetailsAuthenticationProvider这个抽象类。

我们先看看这个抽象类,然后再看它的实现类,看他们是如何一步一步递进的。


public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {

    //...省略了一些代码
	private UserCache userCache = new NullUserCache();

	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    
	//认证方法
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		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;
            //retrieveUser 是个没有抽象的方法 稍后我们看看它的实现类是如何实现的
			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();
		}
        //创建一个成功的Authentication对象。
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	private String determineUsername(Authentication authentication) {
		return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
	}

	
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		//身份信息在这里也加入进去了
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		this.logger.debug("Authenticated user");
		return result;
	}



	
	protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;

    //...
}

DaoAuthenticationProvider:真正做事情的人


public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    // ...省略了一些代码
    
	
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            //UserDetailsService简单说就是加载对应的UserDetails的接口(一般从数据库),而UserDetails包含了更详细的用户信息
            //通过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);
		}   
	}
   	// 重新父类的方法,对密码进行一些加密操作
    @Override
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
                                                         UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null
            && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user);
    }

	//...
}

九、?UserDetailsService和UserDetails接口

UserDetailsService简单说就是定义了一个加载对应的UserDetails的接口,我们在使用中,大都数都会实现这个接口,从数据库中查询相关的用户信息。

//加载用户特定数据的核心接口。
public interface UserDetailsService {
		//根据用户名定位用户
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetails也是一个接口,实际开发中,同样对它也会进行实现,进行定制化的使用。


public interface UserDetails extends Serializable {
	//返回授予用户的权限。 
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();

	//指示用户的帐户是否已过期。 无法验证过期帐户
	boolean isAccountNonExpired();

	//指示用户是被锁定还是未锁定。 无法对锁定的用户进行身份验证。
	boolean isAccountNonLocked();

	//指示用户的凭据(密码)是否已过期。 过期的凭据会阻止身份验证。
	boolean isCredentialsNonExpired();

	//指示用户是启用还是禁用。 无法对禁用的用户进行身份验证。
	boolean isEnabled();
}

10、?返回过程

1、DaoAuthenticationProvider类下UserDetails retrieveUser()方法中通过this.getUserDetailsService().loadUserByUsername(username);获取到用户信息后;

2、将UserDetails返回给父类AbstractUserDetailsAuthenticationProvider中的调用处(即Authentication authenticate(Authentication authentication)方法中)

3、AbstractUserDetailsAuthenticationProvider拿到返回的UserDetails后,最后返回给调用者的是return createSuccessAuthentication(principalToReturn, authentication, user); 这里就是创建了一个可信的 UsernamePasswordAuthenticationToken,即身份凭证。

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

4、我们再回到ProviderManagerAuthentication authenticate(Authentication authentication)方法中的调用处,这个时候我们的用户信息已经是验证过的,我们接着向上层调用处返回。

5、回到UsernamePasswordAuthenticationFilter中的return this.getAuthenticationManager().authenticate(authRequest);语句中,这个时候还得继续向上层返回

6、返回到AbstractAuthenticationProcessingFilter中,我们直接按ctrl+b看是谁调用了它。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
    implements ApplicationEventPublisherAware, MessageSourceAware {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            // 这里就是调用处。
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                return;
            }
            // session相关,这里我们不深聊
            //发生新的身份验证时执行与 Http 会话相关的功能。
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // Authentication success
            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);
        }
        catch (AuthenticationException ex) {
            // Authentication failed
            //验证失败调用
            unsuccessfulAuthentication(request, response, ex);
        }
    }
}
//成功身份验证的默认行为。
	//1、在SecurityContextHolder上设置成功的Authentication对象
	//2、通知配置的RememberMeServices登录成功
	//3、通过配置的ApplicationEventPublisher触发InteractiveAuthenticationSuccessEvent
	//4、将附加行为委托给AuthenticationSuccessHandler 。
//子类可以覆盖此方法以在身份验证成功后继续FilterChain 。
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                        Authentication authResult) throws IOException, ServletException {
    //将通过验证的Authentication保存至安全上下文
    SecurityContextHolder.getContext().setAuthentication(authResult);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

其实不管是验证成功调用或是失败调用,大都数我们在实际使用中,都是需要重写的,返回我们自己想要返回给前端的数据。

到此这篇关于Security 登录认证流程详细分析详解的文章就介绍到这了,更多相关Security 登录认证内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯