本文就来解析一下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


codecraft
11.9k 声望2k 粉丝

当一个代码的工匠回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧,这样,当他老的时候,可以很自豪告诉世人,我曾经将代码注入生命去打造互联网的浪潮之巅,那是个很疯狂的时代,我在一波波的浪潮上留下...


引用和评论

0 条评论