欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Security 基本流程源码解析 - 认证部分

程序员文章站 2022-05-05 15:11:24
...

一、Spring Security 简介

Spring Security 是一个强大、容易定制的、基于 Spring 开发的实现认证登录与资源授权的应用安全框架;

核心功能主要是:认证(你是谁)、授权(你能访问什么网页或接口)、安全防护(防止跨站攻击等)

Spring Security 与 Spring Boot 的集成做得很不错,不需要xml配置;

以下的解析将以 Spring Boot 为基础;以 UsernamePasswordAuthenticationFilter 过滤器为例;

官网:https://projects.spring.io/spring-security/


二、认证流程解析

1. 基本流程

如果要我们基于原生的 JavaEE 开发一个认证授权模块,肯定会想到可以用 filter 过滤器来实现。Spring Security 就是通过一个过滤器链来实现授权认证功能的;

Spring Security 基本流程源码解析 - 认证部分

1)上下文对象 SecurityContext 和认证主体对象 Authentication 贯穿了整个 Spring Security 认证流程;每个用户都会有他的上下文对象,这个上下文对象保存在 SecurityContextHolder 中;

public interface SecurityContext extends Serializable {
	Authentication getAuthentication();
	void setAuthentication(Authentication authentication);
}

可以看到 SecurityContext 接口只有两个方法,就是用来保存认证主体对象的,所以接下来我们看看 Authentication 接口:

public interface Authentication extends Principal, Serializable {

	// 获取权限集合,用户都有哪些权限
	Collection<? extends GrantedAuthority> getAuthorities();
	
	Object getCredentials();
	
	Object getDetails();

    // 是否已经通过认证
	boolean isAuthenticated();

	// 设置认证状态
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

关于 SecurityContextHolder,在多用户系统中,它将 SecurityContext 保存在 ThreadLocal 中,使得每个线程都有一个SecurityContext;

2)如果某一个主体通过了(上图第二个方块中的,如UsernamePasswordAuthenticationFilter等的)任意一个过滤器的认证,则其 Authentication 对象的 isAuthenticated 被设置为 true,表示认证成功;

3)如果一个过滤器也没认证成功,则 FilterSecurityInterceptor(虽然叫Interceptor,但其实也是Filter)会拦住它并抛出异常;如果认证成功则放行;

4)在响应阶段,ExceptionTransactionFilter 会根据配置处理 FilterSecurityInterceptor 抛出的异常,比如跳转到指定的登录页面,或返回失败响应;

5)如果登录成功,没有异常,则 SecurityContextPersistenceFilter 会将 SecurityContext 放入 session,下次直接取出即可;

不同的认证方式由不同的过滤器实现,如:BasicAuthenticationFilter 实现 http basic 认证,UsernamePasswordAuthenticationFilter 实现用户名密码表单认证;


2. 源码解析

现在我们以 UsernamePasswordAuthenticationFilter 提供的认证方式为例,跟随源码了解具体的认证流程(先跳过SecurityContextPersistentFilter):

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter

由于 UsernamePasswordAuthenticationFilter 是一个过滤器,所以在其父类 AbstractAuthenticationProcessingFilter 中找到 doFilter 方法如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    // 判断 request 访问的是否为登录接口,如果是则直接放行
	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 immediately as subclass has indicated that it hasn't completed
			// authentication
			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) {
		// Authentication failed
		unsuccessfulAuthentication(request, response, failed);
		return;
	}
	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}
	successfulAuthentication(request, response, chain, authResult);
}

观察 doFilter 方法:首先,requiresAuthentication() 方法判断访问的是否为登陆接口,若是则放行,追踪该方法可以发现,在 UsernamePasswordAuthenticationFilter 的构造函数指定了默认登录接口为 "/login",方式为"post",如下:

public UsernamePasswordAuthenticationFilter() {
	super(new AntPathRequestMatcher("/login", "POST"));
}

(除了登陆接口,其它的不需要认证的接口是怎么被放行的呢?这是后面的 AnonymousAuthenticationFilter 的工作,暂且不谈)

然后,通过 authResult = attemptAuthentication(request, response); 尝试认证, 其代码如下(是在子类中实现的):

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();

    UsernamePasswordAuthenticationToken authRequest = new         
        UsernamePasswordAuthenticationToken(username, password);

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

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

attemptAuthentication 方法首先从 request 中获取用户名和密码,然后实例化了一个认证凭证 UsernamePasswordAuthenticationToken 对象:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    ......
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;     // 用户名
		this.credentials = credentials; // 密码
		setAuthenticated(false);
	}
    ......
}

public abstract class AbstractAuthenticationToken implements Authentication,
		CredentialsContainer {...}

可以看到 UsernamePasswordAuthenticationToken 实现了 Authentication 接口;

setDetails 方法将 UsernamePasswordAuthenticationToken  的 "private Object details" 字段设置为一个 WebAuthenticationDetails对象,这个对象包含一个 remoteAddress 和一个 sessionID;

最后通过 return this.getAuthenticationManager().authenticate(authRequest); 对Token进行验证,返回认证后的 Authentication 对象;


现在,看 AuthenticationManager 是如何对 UsernamePasswordAuthenticationToken 进行认证的:

首先,AuthenticationManager 是一个接口,只有一个 authenticate 方法用来作认证,如下:

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

ProviderManager 实现了 AuthenticaionManager:

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
	......
	private List<AuthenticationProvider> providers = Collections.emptyList();
    ......
}

ProviderManager 保管了一个 AuthenticationProvider 列表,每一种登录认证方式都可以尝试对登录认证主体进行认证。只要有一种方式被认证成功,Authentication对象就成为被认可的主体;

ProviderManager 对 authenticate 方法的实现的主要代码如下:

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    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()) {
        if (!provider.supports(toTest)) {
            continue;
        }

        if (debug) {
            logger.debug("Authentication attempt using "
                    + provider.getClass().getName());
        }

        try {
            result = provider.authenticate(authentication);

            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }
    ......
}

用一个for循环,让 AuthenticationProvider 们分别去看自己支不支持认证这个 Authentication 对象,如果支持就尝试认证;

AuthenticationProvider 的接口如下:

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

那么是哪个 AuthenticationProvider 实现类负责认证 UsernamePasswordAuthenticationToken 呢?在 idea 编辑器中,按住 ctrl + alt 并点击 AuthenticationProvider 可以看到它的各种实现类,其中的 DaoAuthenticationProvider 专门负责认证此类 token;

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	private PasswordEncoder passwordEncoder;  // 密码一般是加密存在数据库中的
    ......
}

在其父类中可以找到其对 supports 的实现:

public boolean supports(Class<?> authentication) {
	return (UsernamePasswordAuthenticationToken.class
			.isAssignableFrom(authentication));
}
protected final UserDetails retrieveUser(String username,
                                         UsernamePasswordAuthenticationToken 
                                         authentication) throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        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);
    }
}

可以看到在其 retrieveUser 方法中,通过 DetailService 调用 loadUserByUsername 方法,从数据库获取用户信息,所以我们需要实现这个 DetailService 接口,重写 loadUserByUsername 方法,其返回的 UserDetails 如下:

public interface UserDetails extends Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();
	
	String getPassword();
	
	String getUsername();
	
	boolean isAccountNonExpired();

	boolean isAccountNonLocked();

	boolean isCredentialsNonExpired();

	boolean isEnabled();
}

其中保存了用户的用户名、密码、权限、是否被锁定等信息;