一、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 安全认证关闭 spring security 认证流程_spring boot

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

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