序
本文就来解析一下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 newAuthentication
request if successfully switched to * another user,null
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 GrantedAuthority * that contains the originalAuthentication
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 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 ListnewAuths = new ArrayList (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 originalAuthentication
object ornull
* otherwise. * * @throws AuthenticationCredentialsNotFoundException If no *Authentication
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 originalAuthentication
object from the current user's * granted authorities. A successfully switched user should have a *SwitchUserGrantedAuthority
that contains the original source user *Authentication
object. * * @param current The currentAuthentication
object * * @return The source userAuthentication
object ornull
* otherwise. */ private Authentication getSourceAuthentication(Authentication current) { Authentication original = null; // iterate over granted authorities and find the 'switch user' authority Collection 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} ** 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
Authentication
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