SwitchUserFilter Source Code Analysis

  spring-security

Order

This article will analyze the source code of 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);
    }
}

First, it will determine whether the url is /login/impersonate or/login/impersonate, if not, it will not enter this 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;
    }

Read the username parameter from the url, then call userdetails service. loaduserbyusername (username) to obtain the target user information, and then judge whether the target account is normal, switch if normal, and throw exception if abnormal

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

Find the target account, add SwitchUserGrantedAuthority, and then create 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;
    }

This method needs to be called either for login switch or logout switch. Login switching will mobilize this method to determine whether the switch has been made.

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

This method will check whether the current account has SwitchUserGrantedAuthority, and if so, find out the account before switching.
For login handover, this method is used to judge whether the handover has already taken place (If you call this method to switch yourself, an AuthenticationCredential NotFoundException exception will be thrown here. createSwitchUserToken will catch this exception and switch the login state to the current login state. However, there is an extra SwitchUserGrantedAuthority than before)。
For logout switching, the identity before switching is found through this. If it cannot be found, an AuthenticationCredential NotFoundException is thrown, but the outer layer is not captured.

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

The error page will be returned.

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 + "]";
    }
}

This saves the association of account switching.

Summary

  • Switching authority judgment

This is configured in security config and authenticated in FilterSecurityInterceptor.

  • Account association

The account information before switching is saved through SwitchUserGrantedAuthority

  • State switch (Login switch/logout switch)

Gets the usernamepasswordauthenticationtoken of the target user, and then calls

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

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

The two methods switch the login status in context again, and the other is the processing after the login is successfully called. There is no change in sessionId. However, if the login is normal, the sessionId will be switched.

Login switching is to obtain the target user information through userdetails service. loaduserbyusername (username), and then create usernamepasswordauthenticationtoken;

The logout switch is to obtain the UserNamePasswordAuthenticationToken of the original account through SwitchUserGrantedAuthority.