✨ 新增管理员代登录功能
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
package com.bms.web.controller.system;
|
||||
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.constant.Constants;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.enums.BusinessType;
|
||||
import com.bms.common.enums.UserStatus;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.framework.web.service.TokenService;
|
||||
import com.bms.framework.web.service.UserDetailsServiceImpl;
|
||||
import com.bms.system.service.ISysUserService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 管理员以目标用户身份排查问题。 */
|
||||
@RestController
|
||||
@RequestMapping("/system/user/impersonation")
|
||||
public class SysImpersonationController {
|
||||
private static final long MAX_DURATION_MILLIS = 30 * 60 * 1000L;
|
||||
|
||||
private final ISysUserService userService;
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
private final TokenService tokenService;
|
||||
|
||||
public SysImpersonationController(ISysUserService userService, UserDetailsServiceImpl userDetailsService,
|
||||
TokenService tokenService) {
|
||||
this.userService = userService;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('system:user:impersonate')")
|
||||
@Log(title = "用户代登录", businessType = BusinessType.GRANT, isSaveResponseData = false)
|
||||
@PostMapping("/{userId}")
|
||||
public AjaxResult start(@PathVariable Long userId) {
|
||||
LoginUser operator = SecurityUtils.getLoginUser();
|
||||
// 项目中超级管理员是 user_id=1;代登录不能嵌套,也不能用于进入管理员账号。
|
||||
if (!SecurityUtils.isAdmin() || operator.isImpersonating()) {
|
||||
throw new ServiceException("仅超级管理员可以代登录");
|
||||
}
|
||||
if (userId == null || SecurityUtils.isAdmin(userId) || userId.equals(operator.getUserId())) {
|
||||
throw new ServiceException("不能代登录管理员账号或当前账号");
|
||||
}
|
||||
userService.checkUserDataScope(userId);
|
||||
SysUser target = userService.selectUserById(userId);
|
||||
if (target == null || !UserStatus.OK.getCode().equals(target.getDelFlag())
|
||||
|| !UserStatus.OK.getCode().equals(target.getStatus())) {
|
||||
throw new ServiceException("目标用户不存在、已删除或已停用");
|
||||
}
|
||||
if (target.getRoles() != null && target.getRoles().stream().anyMatch(role -> role != null
|
||||
&& (role.isAdmin() || Constants.SUPER_ADMIN.equals(role.getRoleKey())))) {
|
||||
throw new ServiceException("不能代登录管理员角色账号");
|
||||
}
|
||||
LoginUser actingUser = (LoginUser) userDetailsService.createLoginUser(target);
|
||||
actingUser.setImpersonatorId(operator.getUserId());
|
||||
actingUser.setImpersonatorName(operator.getUsername());
|
||||
actingUser.setImpersonatorToken(operator.getToken());
|
||||
actingUser.setImpersonationExpireTime(System.currentTimeMillis() + MAX_DURATION_MILLIS);
|
||||
AjaxResult result = AjaxResult.success();
|
||||
result.put(Constants.TOKEN, tokenService.createToken(actingUser));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Log(title = "退出用户代登录", businessType = BusinessType.OTHER, isSaveResponseData = false)
|
||||
@PostMapping("/exit")
|
||||
public AjaxResult exit() {
|
||||
LoginUser user = SecurityUtils.getLoginUser();
|
||||
if (!user.isImpersonating()) {
|
||||
throw new ServiceException("当前不是代登录会话");
|
||||
}
|
||||
tokenService.delLoginUser(user.getToken());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ public class SysLoginController
|
||||
ajax.put("user", user);
|
||||
ajax.put("roles", roles);
|
||||
ajax.put("permissions", permissions);
|
||||
ajax.put("impersonating", loginUser.isImpersonating());
|
||||
ajax.put("pwdChrtype", getSysAccountChrtype());
|
||||
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
|
||||
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package com.bms.web.controller.system;
|
||||
|
||||
import com.bms.common.constant.CacheConstants;
|
||||
import com.bms.common.constant.Constants;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
import com.bms.common.core.domain.entity.SysUser;
|
||||
import com.bms.common.core.domain.entity.SysRole;
|
||||
import com.bms.common.core.domain.model.LoginUser;
|
||||
import com.bms.common.core.redis.RedisCache;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.framework.web.service.TokenService;
|
||||
import com.bms.framework.web.service.UserDetailsServiceImpl;
|
||||
import com.bms.framework.config.FastJson2JsonRedisSerializer;
|
||||
import com.bms.system.service.ISysUserService;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class SysImpersonationTest {
|
||||
private final ISysUserService users = mock(ISysUserService.class);
|
||||
private final UserDetailsServiceImpl details = mock(UserDetailsServiceImpl.class);
|
||||
private final TokenService tokens = mock(TokenService.class);
|
||||
private final SysImpersonationController controller = new SysImpersonationController(users, details, tokens);
|
||||
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlySuperAdministratorCanStart() {
|
||||
authenticate(loginUser(2L, "sales"));
|
||||
assertThrows(ServiceException.class, () -> controller.start(3L));
|
||||
verifyNoInteractions(users, details, tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDisabledAndAdministratorTargets() {
|
||||
authenticate(loginUser(1L, "admin"));
|
||||
SysUser disabled = user(2L, "sales");
|
||||
disabled.setStatus("1");
|
||||
when(users.selectUserById(2L)).thenReturn(disabled);
|
||||
assertThrows(ServiceException.class, () -> controller.start(2L));
|
||||
assertThrows(ServiceException.class, () -> controller.start(1L));
|
||||
verifyNoInteractions(details, tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
void issuedSessionUsesTargetIdentityAndRetainsAdministrator() {
|
||||
LoginUser admin = loginUser(1L, "admin");
|
||||
admin.setToken("parent-session");
|
||||
authenticate(admin);
|
||||
SysUser target = user(2L, "sales");
|
||||
LoginUser targetLogin = new LoginUser(2L, 10L, target, Collections.singleton("bill:list"));
|
||||
when(users.selectUserById(2L)).thenReturn(target);
|
||||
when(details.createLoginUser(target)).thenReturn(targetLogin);
|
||||
when(tokens.createToken(targetLogin)).thenReturn("target-jwt");
|
||||
long started = System.currentTimeMillis();
|
||||
|
||||
AjaxResult result = controller.start(2L);
|
||||
|
||||
assertEquals("target-jwt", result.get(Constants.TOKEN));
|
||||
assertEquals(2L, targetLogin.getUserId());
|
||||
assertEquals(1L, targetLogin.getImpersonatorId());
|
||||
assertEquals("parent-session", targetLogin.getImpersonatorToken());
|
||||
assertTrue(targetLogin.getImpersonationExpireTime() >= started + 29 * 60_000L);
|
||||
assertTrue(targetLogin.getImpersonationExpireTime() <= System.currentTimeMillis() + 30 * 60_000L);
|
||||
verify(users).checkUserDataScope(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTargetWithAdministratorRole() {
|
||||
authenticate(loginUser(1L, "admin"));
|
||||
SysUser target = user(2L, "other-admin");
|
||||
SysRole role = new SysRole();
|
||||
role.setRoleId(1L);
|
||||
target.setRoles(Collections.singletonList(role));
|
||||
when(users.selectUserById(2L)).thenReturn(target);
|
||||
assertThrows(ServiceException.class, () -> controller.start(2L));
|
||||
verifyNoInteractions(details, tokens);
|
||||
}
|
||||
|
||||
@Test
|
||||
void impersonationContextSurvivesRedisSerialization() {
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorName("admin");
|
||||
acting.setImpersonatorToken("parent");
|
||||
acting.setImpersonationExpireTime(System.currentTimeMillis() + 60_000L);
|
||||
FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
|
||||
LoginUser restored = (LoginUser) serializer.deserialize(serializer.serialize(acting));
|
||||
assertTrue(restored.isImpersonating());
|
||||
assertEquals(1L, restored.getImpersonatorId());
|
||||
assertEquals("parent", restored.getImpersonatorToken());
|
||||
assertEquals(acting.getImpersonationExpireTime(), restored.getImpersonationExpireTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSessionWhenAdministratorSessionWasRevoked() {
|
||||
RedisCache redis = mock(RedisCache.class);
|
||||
TokenService service = tokenService(redis);
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorToken("parent");
|
||||
acting.setImpersonationExpireTime(System.currentTimeMillis() + 60_000L);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "acting")).thenReturn(acting);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "parent")).thenReturn(null);
|
||||
|
||||
assertNull(service.getLoginUser(request("acting")));
|
||||
verify(redis).deleteObject(CacheConstants.LOGIN_TOKEN_KEY + "acting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsExpiredSessionEvenWhenAdministratorIsStillLoggedIn() {
|
||||
RedisCache redis = mock(RedisCache.class);
|
||||
TokenService service = tokenService(redis);
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorToken("parent");
|
||||
acting.setImpersonationExpireTime(System.currentTimeMillis() - 1);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "acting")).thenReturn(acting);
|
||||
when(redis.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + "parent")).thenReturn(loginUser(1L, "admin"));
|
||||
|
||||
assertNull(service.getLoginUser(request("acting")));
|
||||
verify(redis).deleteObject(CacheConstants.LOGIN_TOKEN_KEY + "acting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exitRevokesOnlyTheImpersonationSession() {
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setToken("acting");
|
||||
acting.setImpersonatorId(1L);
|
||||
acting.setImpersonatorToken("parent");
|
||||
authenticate(acting);
|
||||
|
||||
assertEquals(200, controller.exit().get("code"));
|
||||
verify(tokens).delLoginUser("acting");
|
||||
verify(tokens, never()).delLoginUser("parent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshCannotExtendPastAbsoluteDeadline() {
|
||||
RedisCache redis = mock(RedisCache.class);
|
||||
TokenService service = tokenService(redis);
|
||||
LoginUser acting = loginUser(2L, "sales");
|
||||
acting.setToken("acting");
|
||||
acting.setImpersonatorId(1L);
|
||||
long deadline = System.currentTimeMillis() + 45_000L;
|
||||
acting.setImpersonationExpireTime(deadline);
|
||||
|
||||
service.refreshToken(acting);
|
||||
|
||||
assertEquals(deadline, acting.getExpireTime());
|
||||
ArgumentCaptor<Integer> ttl = ArgumentCaptor.forClass(Integer.class);
|
||||
verify(redis).setCacheObject(eq(CacheConstants.LOGIN_TOKEN_KEY + "acting"), same(acting), ttl.capture(), eq(TimeUnit.SECONDS));
|
||||
assertTrue(ttl.getValue() > 0 && ttl.getValue() <= 45);
|
||||
}
|
||||
|
||||
private static final String SECRET = "test-secret-for-impersonation-verification-012345678901234567890123456789";
|
||||
|
||||
private TokenService tokenService(RedisCache redis) {
|
||||
TokenService service = new TokenService();
|
||||
ReflectionTestUtils.setField(service, "redisCache", redis);
|
||||
ReflectionTestUtils.setField(service, "header", "Authorization");
|
||||
ReflectionTestUtils.setField(service, "secret", SECRET);
|
||||
ReflectionTestUtils.setField(service, "expireTime", 1440);
|
||||
return service;
|
||||
}
|
||||
|
||||
private MockHttpServletRequest request(String uuid) {
|
||||
String jwt = Jwts.builder().setClaims(Map.of(Constants.LOGIN_USER_KEY, uuid))
|
||||
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + jwt);
|
||||
return request;
|
||||
}
|
||||
|
||||
private LoginUser loginUser(long id, String name) {
|
||||
SysUser user = user(id, name);
|
||||
return new LoginUser(id, 10L, user, Collections.emptySet());
|
||||
}
|
||||
|
||||
private SysUser user(long id, String name) {
|
||||
SysUser user = new SysUser(id);
|
||||
user.setUserName(name);
|
||||
user.setStatus("0");
|
||||
user.setDelFlag("0");
|
||||
return user;
|
||||
}
|
||||
|
||||
private void authenticate(LoginUser user) {
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user