本文就來解析一下SwitchUserFilter的源碼java
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
/** * 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)獲取目標用戶信息,而後判斷目標帳戶是否正常,正常則切換,不正常則拋異常
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")); } } }
/** * 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
/** * 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; }
這個方法不管是登陸切換,仍是註銷切換都須要調用。登陸切換會調動這個方法判斷是否已經切換過了.
/** * 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; }
於是會返回錯誤頁面
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