序
本文就来解析一下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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。