Spring Security实战--(五)认证和鉴权过程

前面几篇直接讲了Demo,但是可能还是有点混乱,这里再将整个认证过程梳理一下,加深对前面的理解

一、标准的身份验证方案

对一个系统来说,标准的安全身份验证方案应该按如下步骤:

  1. 用户使用用户名和密码登录;
  2. 系统验证用户的额密码正确,成功登录;
  3. 获取该用户的上下文,即该用户的角色列表;
  4. 为用户建立安全的上下文
  5. 用户可能会继续执行某些操作,该操作可能会受到访问控制机制的保护,该访问控制机制会根据当前安全上下文信息检查该操作所需的权限

其中,前四个步骤构成了认证过程,在SpringSecurity中流程如下:

  1. 获取用户名和密码,并将其组合到一个Authentication实例,一般为UsernamePasswordAuthenticationToken实例中
  2. token会被传送到AuthenticationManager实例进行验证
  3. 成功验证后,AuthenticationManager会返回一个Authentication实例
  4. 通过调用SecurityContextHolder.getContext().setAuthentication(…​)并传入返回的身份验证对象来建立安全上下文

来看一下涉及到的类

1.1 Authentication

继承了Principal类和序列化

public interface Authentication extends Principal, Serializable {
	
	/**
	 * 获得用户的权限信息列表
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 密码信息,但认证后通常会被移除
	 */
	Object getCredentials();

	/**
	 * 
	 * 存储额外的认证请求信息,可能是IP地址等
	 */
	Object getDetails();

	/**
	 * 犯规身份信息
	 */
	Object getPrincipal();

	/**
	 * @return true if the token has been authenticated and the
	 */
	boolean isAuthenticated();

	/**
	 */
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

从上面的源码中可以看出,由这个接口的实现类,我们可以得到用户相关的
权限列表信息,密码、用户其他详细信息等重要信息

1.2 AuthenticationManager

AuthenticationManager是一个接口,正常系统应用中,登录是可以通过多种密码验证方式,如手机号+验证码,用户名/邮箱+密码等。这个时候就需要调用SpringSecurity对它的默认实现ProviderManager,但是它本身并不处理身份验证请求,而是委托给已经配置的AuthenticationProvider列表,依次查看是否可以执行身份验证,每个AuthenticationProvider程序都会引发一场或返回完全填充的Authentication对象。如上面所说的几种验证方式,只要通过一个AuthenticationProvider并返回Authentication对象,即可通过登录验证

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {

	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
	private List<AuthenticationProvider> providers = Collections.emptyList();
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private AuthenticationManager parent;
	private boolean eraseCredentialsAfterAuthentication = true;

	public ProviderManager(List<AuthenticationProvider> providers) {
		this(providers, null);
	}

	public ProviderManager(List<AuthenticationProvider> providers,
			AuthenticationManager parent) {
		Assert.notNull(providers, "providers list cannot be null");
		this.providers = providers;
		this.parent = parent;
		checkState();
	}

	private void checkState() {
		if (parent == null && providers.isEmpty()) {
			throw new IllegalArgumentException(
					"A parent AuthenticationManager or a list "
							+ "of AuthenticationProviders is required");
		}
	}

	/**
	 * 
	 */
	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;

		//遍历所有的AuthenticationProvider,因此进行认证
		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;
				}
			}
			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;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// 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 e) {
				lastException = parentException = e;
			}
		}
		
		//如果有authentication信息
		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// 擦除密码
				((CredentialsContainer) result).eraseCredentials();
			}

			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// 抛出异常
		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

	public List<AuthenticationProvider> getProviders() {
		return providers;
	}
}

1.3 SecurityContextHolder

SecurityContextHolder用来存储应用程序当前安全上下文的详细信息,其中包括当前使用该应用程序的认证详细信息。默认情况下,其使用ThreadLocal来存储这些详细信息,将其与线程绑定。当用户请求被处理后,ThreadLocal中的认证信息会被清除,SpringSecurity会自动解决此问题。

因为当前的认证信息与线程绑定,所以获取当前用户信息可以使用以下方式:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

1.4 认证过程代码总结

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();

public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while(true) {
    System.out.println("Please enter your username:");
    String name = in.readLine();
    System.out.println("Please enter your password:");
    String password = in.readLine();
    try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
    } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
    }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
            SecurityContextHolder.getContext().getAuthentication());
}
}

class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}

public Authentication authenticate(Authentication auth) throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
    return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");
}
}

以上是用Code做一个简单的总结,整个过程以此流程在内部发生

二、鉴权

从上面可以看出,所有的Authentication实现都存储了GrantedAuthority对象列表,它代表了已授予委托人的权限。GrantedAuthority对象列表在AuthenticationManager中被插入,并在稍后由AccessDecisionManager读取并作出授权决定

GrantedAuthority接口只有一个方法:

public interface GrantedAuthority extends Serializable {
	
	String getAuthority();
}

这个方法使AccessDecisionManager获得一个代表GrantedAuthority的String。通过返回的String,GrantedAuthority可以很容易读取到信息

2.1 调用前处理

Spring Security提供了拦截器,该拦截器控制对安全对象(例如方法调用或Web请求)的访问。AccessDecisionManager会做出一个调用前处理来决定是否允许继续调用请求。

AccessDecisionManager

AccessDecisionManager负责做出最终的访问控制决策,接口包含以下三种方法:

public interface AccessDecisionManager {
	/**
	 * 通过传入的参数,对访问权限决策(投票)
	 */
	void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
			InsufficientAuthenticationException;

	/**
	 * 根据传入的类决定是否对该类做决策
	 */
	boolean supports(ConfigAttribute attribute);

	/**
	 * 根据传入的类决定是否对该类做决策
	 */
	boolean supports(Class<?> clazz);
}

其中decide方法中的参数,authentication代表请求访问的主体;object指代可以对其实施安全性(例如授权决策)的任何对象,最常见的示例是方法调用和Web请求;configAttributes代指属性列表,如配置的ROLE_xxx等属性

基于投票机制的实现

  1. AccessDecisionVoter

一系列的AccessDecisionVoter实现将投票决定授权决策,AccessDecisionManager将基于投票决定是否抛出AccessDeniedException异常

public interface AccessDecisionVoter<S> {

	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;

	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);

	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);
}

vote的实现返回一个int值,对应其中的静态常量ACCESS_GRANTED,ACCESS_ABSTAIN和ACCESS_DENIED 。如果vote实现对授权无意见,则返回ACCESS_ABSTAIN。如果有意见,必须返回ACCESS_GRANTED或ACCESS_DENIED。

  1. RoleVoter

由Spring提供的最常用的AccessDecisionVoter是简单的RoleVoter,它将配置属性视为简单的角色名称,如果用户被赋予了该角色,则投票将授予访问限制

如:如果属性以ROLE_开头,如果GrantedAuthority返回的String中含有一个或多个相同的,则投票授予访问权限

  1. AuthenticatedVoter

AuthenticatedVoter用来区分匿名,完全认证和记住我的认证用户。如有的站点使用记住我身份验证时进行某些访问限制,但是要求用户通过登录以惊醒完全访问来确认身份

2.3 调用前处理详解

调用前处理是SpringSecurity鉴权的重要过程,这里通过源码来分析下

调用入口在FilterSecurityInterceptor类中,继承了AbstractSecurityInterceptor,实现了Filter接口,其他方法省略,仅保留了调用前处理的方法源码

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
		Filter {
	
	//调用入口,查看下面的invoke方法
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}
	
	
	public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
			
			//调用beforeInvocation方法,详见下面的AbstractSecurityInterceptor类源码
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}

			super.afterInvocation(token, null);
		}
	}
}

beforeInvocation方法源码如下

protected InterceptorStatusToken beforeInvocation(Object object) {

	......
	
	Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
			.getAttributes(object);

	......

	Authentication authenticated = authenticateIfRequired();

	......

	// Attempt authorization
	try {
		//可以看到,在这里进入了decide方法
		this.accessDecisionManager.decide(authenticated, object, attributes);
	}
	catch (AccessDeniedException accessDeniedException) {
		publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
				accessDeniedException));

		throw accessDeniedException;
	}

	if (debug) {
		logger.debug("Authorization successful");
	}

	if (publishAuthorizationSuccess) {
		publishEvent(new AuthorizedEvent(object, attributes, authenticated));
	}

	// Attempt to run as a different user
	Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
			attributes);

	if (runAs == null) {
		if (debug) {
			logger.debug("RunAsManager did not change Authentication object");
		}

		// no further work post-invocation
		return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
				attributes, object);
	}
	else {
		if (debug) {
			logger.debug("Switching to RunAs Authentication: " + runAs);
		}

		SecurityContext origCtx = SecurityContextHolder.getContext();
		SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
		SecurityContextHolder.getContext().setAuthentication(runAs);

		// need to revert to token.Authenticated post-invocation
		return new InterceptorStatusToken(origCtx, true, attributes, object);
	}
}

默认的AccessDecisionManager在SpringSecurity中由AffirmativeBased实现,当然也其他实现,现在先看下这个

public class AffirmativeBased extends AbstractAccessDecisionManager {

	public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
		super(decisionVoters);
	}
	
	//decide过程
	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;

		for (AccessDecisionVoter voter : getDecisionVoters()) {
			//调用voter进行投票
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break;
			}
		}

		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}
}

分析下RoleVoter,其他是类似的

public class RoleVoter implements AccessDecisionVoter<Object> {

	private String rolePrefix = "ROLE_";

	public String getRolePrefix() {
		return rolePrefix;
	}

	......

	//判断是否基于ROLE_开头
	public boolean supports(ConfigAttribute attribute) {
		if ((attribute.getAttribute() != null)
				&& attribute.getAttribute().startsWith(getRolePrefix())) {
			return true;
		}
		else {
			return false;
		}
	}

	.......
	//进入角色判断
	public int vote(Authentication authentication, Object object,
			Collection<ConfigAttribute> attributes) {
		if (authentication == null) {
			//请求访问的主体为null,拒绝
			return ACCESS_DENIED;
		}
		int result = ACCESS_ABSTAIN;
		Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
		
		for (ConfigAttribute attribute : attributes) {
			//如果角色符合
			if (this.supports(attribute)) {
				//暂时仍是拒绝,需再判断权限
				result = ACCESS_DENIED;

				// Attempt to find a matching granted authority
				for (GrantedAuthority authority : authorities) {
					if (attribute.getAttribute().equals(authority.getAuthority())) {
						//权限通过后,则改为授权通过
						return ACCESS_GRANTED;
					}
				}
			}
		}

		return result;
	}
	
	//获取权限
	Collection<? extends GrantedAuthority> extractAuthorities(
			Authentication authentication) {
		return authentication.getAuthorities();
	}
}

你可能感兴趣的:(Spring,Security)