SwitchUserFilter源码解析

447 阅读5分钟

本文就来解析一下SwitchUserFilter的源码

SwitchUserFilter

spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/switchuser/SwitchUserFilter.java

public class SwitchUserFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
    //......
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		// check for switch or exit request
		if (requiresSwitchUser(request)) {
			// if set, attempt switch and store original
			try {
				Authentication targetUser = attemptSwitchUser(request);

				// update the current context to the new target user
				SecurityContextHolder.getContext().setAuthentication(targetUser);

				// redirect to target url
				this.successHandler.onAuthenticationSuccess(request, response,
						targetUser);
			}
			catch (AuthenticationException e) {
				this.logger.debug("Switch User failed", e);
				this.failureHandler.onAuthenticationFailure(request, response, e);
			}

			return;
		}
		else if (requiresExitUser(request)) {
			// get the original authentication object (if exists)
			Authentication originalUser = attemptExitUser(request);

			// update the current context back to the original user
			SecurityContextHolder.getContext().setAuthentication(originalUser);

			// redirect to target url
			this.successHandler.onAuthenticationSuccess(request, response, originalUser);

			return;
		}

		chain.doFilter(request, response);
	}
}

首先会判断url是不是/login/impersonate或者/logout/impersonate,如果不是则不会进入这个filter

attemptSwitchUser

/**
	 * Attempt to switch to another user. If the user does not exist or is not active,
	 * return null.
	 *
	 * @return The new <code>Authentication</code> request if successfully switched to
	 * another user, <code>null</code> otherwise.
	 *
	 * @throws UsernameNotFoundException If the target user is not found.
	 * @throws LockedException if the account is locked.
	 * @throws DisabledException If the target user is disabled.
	 * @throws AccountExpiredException If the target user account is expired.
	 * @throws CredentialsExpiredException If the target user credentials are expired.
	 */
	protected Authentication attemptSwitchUser(HttpServletRequest request)
			throws AuthenticationException {
		UsernamePasswordAuthenticationToken targetUserRequest;

		String username = request.getParameter(this.usernameParameter);

		if (username == null) {
			username = "";
		}

		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Attempt to switch to user [" + username + "]");
		}

		UserDetails targetUser = this.userDetailsService.loadUserByUsername(username);
		this.userDetailsChecker.check(targetUser);

		// OK, create the switch user token
		targetUserRequest = createSwitchUserToken(request, targetUser);

		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Switch User Token [" + targetUserRequest + "]");
		}

		// publish event
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(
					SecurityContextHolder.getContext().getAuthentication(), targetUser));
		}

		return targetUserRequest;
	}

从url读取username参数,然后调用userDetailsService.loadUserByUsername(username)获取目标用户信息,然后判断目标账户是否正常,正常则切换,不正常则抛异常

AccountStatusUserDetailsChecker

spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/authentication/AccountStatusUserDetailsChecker.java

public class AccountStatusUserDetailsChecker implements UserDetailsChecker {

	protected final MessageSourceAccessor messages = SpringSecurityMessageSource
			.getAccessor();

	public void check(UserDetails user) {
		if (!user.isAccountNonLocked()) {
			throw new LockedException(messages.getMessage(
					"AccountStatusUserDetailsChecker.locked", "User account is locked"));
		}

		if (!user.isEnabled()) {
			throw new DisabledException(messages.getMessage(
					"AccountStatusUserDetailsChecker.disabled", "User is disabled"));
		}

		if (!user.isAccountNonExpired()) {
			throw new AccountExpiredException(
					messages.getMessage("AccountStatusUserDetailsChecker.expired",
							"User account has expired"));
		}

		if (!user.isCredentialsNonExpired()) {
			throw new CredentialsExpiredException(messages.getMessage(
					"AccountStatusUserDetailsChecker.credentialsExpired",
					"User credentials have expired"));
		}
	}
}

createSwitchUserToken

/**
	 * Create a switch user token that contains an additional <tt>GrantedAuthority</tt>
	 * that contains the original <code>Authentication</code> object.
	 *
	 * @param request The http servlet request.
	 * @param targetUser The target user
	 *
	 * @return The authentication token
	 *
	 * @see SwitchUserGrantedAuthority
	 */
	private UsernamePasswordAuthenticationToken createSwitchUserToken(
			HttpServletRequest request, UserDetails targetUser) {

		UsernamePasswordAuthenticationToken targetUserRequest;

		// grant an additional authority that contains the original Authentication object
		// which will be used to 'exit' from the current switched user.

		Authentication currentAuth;

		try {
			// SEC-1763. Check first if we are already switched.
			currentAuth = attemptExitUser(request);
		}
		catch (AuthenticationCredentialsNotFoundException e) {
			currentAuth = SecurityContextHolder.getContext().getAuthentication();
		}

		GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority(
				this.switchAuthorityRole, currentAuth);

		// get the original authorities
		Collection<? extends GrantedAuthority> orig = targetUser.getAuthorities();

		// Allow subclasses to change the authorities to be granted
		if (this.switchUserAuthorityChanger != null) {
			orig = this.switchUserAuthorityChanger.modifyGrantedAuthorities(targetUser,
					currentAuth, orig);
		}

		// add the new switch user authority
		List<GrantedAuthority> newAuths = new ArrayList<GrantedAuthority>(orig);
		newAuths.add(switchAuthority);

		// create the new authentication token
		targetUserRequest = new UsernamePasswordAuthenticationToken(targetUser,
				targetUser.getPassword(), newAuths);

		// set details
		targetUserRequest
				.setDetails(this.authenticationDetailsSource.buildDetails(request));

		return targetUserRequest;
	}

找出目标账号,添加SwitchUserGrantedAuthority,然后创建UsernamePasswordAuthenticationToken

attemptExitUser

/**
	 * Attempt to exit from an already switched user.
	 *
	 * @param request The http servlet request
	 *
	 * @return The original <code>Authentication</code> object or <code>null</code>
	 * otherwise.
	 *
	 * @throws AuthenticationCredentialsNotFoundException If no
	 * <code>Authentication</code> associated with this request.
	 */
	protected Authentication attemptExitUser(HttpServletRequest request)
			throws AuthenticationCredentialsNotFoundException {
		// need to check to see if the current user has a SwitchUserGrantedAuthority
		Authentication current = SecurityContextHolder.getContext().getAuthentication();

		if (null == current) {
			throw new AuthenticationCredentialsNotFoundException(
					this.messages.getMessage("SwitchUserFilter.noCurrentUser",
							"No current user associated with this request"));
		}

		// check to see if the current user did actual switch to another user
		// if so, get the original source user so we can switch back
		Authentication original = getSourceAuthentication(current);

		if (original == null) {
			this.logger.debug("Could not find original user Authentication object!");
			throw new AuthenticationCredentialsNotFoundException(
					this.messages.getMessage("SwitchUserFilter.noOriginalAuthentication",
							"Could not find original Authentication object"));
		}

		// get the source user details
		UserDetails originalUser = null;
		Object obj = original.getPrincipal();

		if ((obj != null) && obj instanceof UserDetails) {
			originalUser = (UserDetails) obj;
		}

		// publish event
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(
					new AuthenticationSwitchUserEvent(current, originalUser));
		}

		return original;
	}

这个方法无论是登录切换,还是注销切换都需要调用。登录切换会调动这个方法判断是否已经切换过了.

getSourceAuthentication

/**
	 * Find the original <code>Authentication</code> object from the current user's
	 * granted authorities. A successfully switched user should have a
	 * <code>SwitchUserGrantedAuthority</code> that contains the original source user
	 * <code>Authentication</code> object.
	 *
	 * @param current The current <code>Authentication</code> object
	 *
	 * @return The source user <code>Authentication</code> object or <code>null</code>
	 * otherwise.
	 */
	private Authentication getSourceAuthentication(Authentication current) {
		Authentication original = null;

		// iterate over granted authorities and find the 'switch user' authority
		Collection<? extends GrantedAuthority> authorities = current.getAuthorities();

		for (GrantedAuthority auth : authorities) {
			// check for switch user type of authority
			if (auth instanceof SwitchUserGrantedAuthority) {
				original = ((SwitchUserGrantedAuthority) auth).getSource();
				this.logger.debug("Found original switch user granted authority ["
						+ original + "]");
			}
		}

		return original;
	}

这个方法会检查,当前账号是否具有SwitchUserGrantedAuthority,如果有则找出切换前的账号。 对于登录切换,通过这个方法判断是否已经切换过(如果你调用这个方法自己切换自己,则这里会抛出AuthenticationCredentialsNotFoundException异常,createSwitchUserToken会捕获这个异常,然后将登录态切换成当前的登录态;不过比没切换之前多了个SwitchUserGrantedAuthority)。 而对于注销切换,则通过这个找出切换前的身份,如果找不到则抛出AuthenticationCredentialsNotFoundException,但是外层没有捕获

		if (requiresExitUser(request)) {
			// get the original authentication object (if exists)
			Authentication originalUser = attemptExitUser(request);

			// update the current context back to the original user
			SecurityContextHolder.getContext().setAuthentication(originalUser);

			// redirect to target url
			this.successHandler.onAuthenticationSuccess(request, response, originalUser);

			return;
		}

因而会返回错误页面

SwitchUserGrantedAuthority

spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/switchuser/SwitchUserGrantedAuthority.java

/**
 * Custom {@code GrantedAuthority} used by
 * {@link org.springframework.security.web.authentication.switchuser.SwitchUserFilter}
 * <p>
 * Stores the {@code Authentication} object of the original user to be used later when
 * 'exiting' from a user switch.
 *
 * @author Mark St.Godard
 *
 * @see org.springframework.security.web.authentication.switchuser.SwitchUserFilter
 */
public final class SwitchUserGrantedAuthority implements GrantedAuthority {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================
	private final String role;
	private final Authentication source;

	// ~ Constructors
	// ===================================================================================================

	public SwitchUserGrantedAuthority(String role, Authentication source) {
		this.role = role;
		this.source = source;
	}

	// ~ Methods
	// ========================================================================================================

	/**
	 * Returns the original user associated with a successful user switch.
	 *
	 * @return The original <code>Authentication</code> object of the switched user.
	 */
	public Authentication getSource() {
		return source;
	}

	public String getAuthority() {
		return role;
	}

	public int hashCode() {
		return 31 ^ source.hashCode() ^ role.hashCode();
	}

	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if (obj instanceof SwitchUserGrantedAuthority) {
			SwitchUserGrantedAuthority swa = (SwitchUserGrantedAuthority) obj;
			return this.role.equals(swa.role) && this.source.equals(swa.source);
		}

		return false;
	}

	public String toString() {
		return "Switch User Authority [" + role + "," + source + "]";
	}
}

这个保存了账户切换的关联关系

小结

  • 切换权限判断 这个通过security config里头配置,在FilterSecurityInterceptor里头进行鉴权
  • 账号关联 通过SwitchUserGrantedAuthority来保存切换之前的账号信息
  • 状态切换(登录切换/注销切换) 获取目标用户的UsernamePasswordAuthenticationToken,之后调用
				// update the current context to the new target user
				SecurityContextHolder.getContext().setAuthentication(targetUser);

				// redirect to target url
				this.successHandler.onAuthenticationSuccess(request, response,
						targetUser);

这两个方法一个再上下文切换登录态,一个是调用登录成功之后的处理。这里没有改变sessionId。但是如果是正常登陆的话,会切换sessionId的。

登录切换是通过userDetailsService.loadUserByUsername(username)获取目标用户信息,然后创建UsernamePasswordAuthenticationToken;

注销切换则是通过SwitchUserGrantedAuthority获取原账号的UsernamePasswordAuthenticationToken