SwitchUserFilter源碼解析

本文就來解析一下SwitchUserFilter的源碼java

SwitchUserFilter

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

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.javaspring

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.javasession

/**
 * 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裏頭進行鑑權ui

  • 帳號關聯

經過SwitchUserGrantedAuthority來保存切換以前的帳號信息this

  • 狀態切換(登陸切換/註銷切換)

獲取目標用戶的UsernamePasswordAuthenticationToken,以後調用url

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

註銷切換則是經過SwitchUserGrantedAuthority獲取原帳號的UsernamePasswordAuthenticationTokencode

相關文章
相關標籤/搜索