feat: add backdoor login functionality for testing environment
- Implemented SysUserMapper to include a method for selecting available users for backdoor login. - Created XML mappings for the new backdoor user selection query. - Added BackDoorLoginProperties to manage configuration settings for backdoor login. - Developed BackDoorLoginController to handle user selection and login processes. - Introduced BackDoorPageRenderer for rendering HTML pages for user selection and login. - Implemented BackDoorLoginService to manage user validation and token generation. - Added HTML templates for user selection, login, and error handling. - Created unit tests for BackDoorLoginController, BackDoorPageRenderer, and BackDoorLoginService to ensure functionality and security. - Documented usage and configuration in test-backdoor-login.md.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.web.config.BackDoorLoginProperties;
|
||||
import com.ruoyi.web.service.BackDoorLoginService;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.LoginSession;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BackDoorLoginControllerTest
|
||||
{
|
||||
@Mock
|
||||
private BackDoorLoginService loginService;
|
||||
|
||||
private BackDoorLoginController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
BackDoorLoginProperties properties = new BackDoorLoginProperties();
|
||||
controller = new BackDoorLoginController(loginService, new BackDoorPageRenderer(), properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noIdRendersUserSelectionPageWithNoStoreHeaders()
|
||||
{
|
||||
when(loginService.listAvailableUsers("tester", 100)).thenReturn(Collections.emptyList());
|
||||
|
||||
ResponseEntity<String> response = controller.backDoor(null, "tester");
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertTrue(response.getBody().contains("选择一个 PC 用户登录"));
|
||||
assertEquals("UTF-8", response.getHeaders().getContentType().getCharset().name());
|
||||
assertTrue(response.getHeaders().getCacheControl().contains("no-store"));
|
||||
assertTrue(response.getHeaders().containsKey("Content-Security-Policy"));
|
||||
verify(loginService).listAvailableUsers("tester", 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedIdReturnsHtmlErrorWithoutAttemptingLogin()
|
||||
{
|
||||
ResponseEntity<String> response = controller.backDoor("not-a-number", null);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
assertTrue(response.getBody().contains("用户 ID 必须是正整数"));
|
||||
verify(loginService, never()).login(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validIdRendersSameOriginTokenBootstrapPage()
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(8L);
|
||||
user.setUserName("tester");
|
||||
when(loginService.login(8L)).thenReturn(new LoginSession(user, "signed-token"));
|
||||
|
||||
ResponseEntity<String> response = controller.backDoor("8", null);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertTrue(response.getBody().contains("const token = \"signed-token\""));
|
||||
assertTrue(response.getBody().contains("localStorage.setItem('access_token', token)"));
|
||||
assertFalse(response.getBody().contains("?token=signed-token"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.web.config.BackDoorLoginProperties;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.LoginSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BackDoorPageRendererTest
|
||||
{
|
||||
private final BackDoorPageRenderer renderer = new BackDoorPageRenderer();
|
||||
|
||||
@Test
|
||||
void userListEscapesDatabaseTextAndMasksPhone()
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(7L);
|
||||
user.setUserName("tester</td><script>alert(1)</script>");
|
||||
user.setNickName("测试 & 用户");
|
||||
user.setPhonenumber("13812345678");
|
||||
user.setAppUserId(9L);
|
||||
SysDept dept = new SysDept();
|
||||
dept.setDeptName("研发 <一部>");
|
||||
user.setDept(dept);
|
||||
|
||||
String html = renderer.renderUserList(Collections.singletonList(user), "<script>", 100);
|
||||
|
||||
assertFalse(html.contains("<script>alert(1)</script>"));
|
||||
assertTrue(html.contains("tester</td><script>alert(1)</script>"));
|
||||
assertTrue(html.contains("测试 & 用户"));
|
||||
assertTrue(html.contains("138****5678"));
|
||||
assertTrue(html.contains("研发 <一部>"));
|
||||
assertTrue(html.contains("?id=7"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginPageDoesNotExposeTokenInUrlAndEscapesScriptBoundary()
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(7L);
|
||||
user.setUserName("tester");
|
||||
LoginSession session = new LoginSession(user, "token</script><script>alert(1)</script>");
|
||||
BackDoorLoginProperties properties = new BackDoorLoginProperties();
|
||||
|
||||
String html = renderer.renderLogin(session, properties);
|
||||
|
||||
assertFalse(html.contains("token</script>"));
|
||||
assertTrue(html.contains("token\\u003c/script\\u003e"));
|
||||
assertTrue(html.contains("localStorage.setItem('access_token', token)"));
|
||||
assertTrue(html.contains("fetch(apiBase + '/getInfo'"));
|
||||
assertFalse(html.contains("?token="));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.ruoyi.web.service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.framework.web.service.SysLoginService;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.framework.web.service.UserDetailsServiceImpl;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.LoginSession;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BackDoorLoginServiceTest
|
||||
{
|
||||
@Mock
|
||||
private SysUserMapper userMapper;
|
||||
@Mock
|
||||
private UserDetailsServiceImpl userDetailsService;
|
||||
@Mock
|
||||
private TokenService tokenService;
|
||||
@Mock
|
||||
private SysLoginService sysLoginService;
|
||||
|
||||
private BackDoorLoginService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
service = new BackDoorLoginService(userMapper, userDetailsService, tokenService, sysLoginService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAvailableUsersTrimsKeywordAndCapsLimit()
|
||||
{
|
||||
List<SysUser> expected = Collections.singletonList(activeUser(12L));
|
||||
when(userMapper.selectBackDoorUserList("tester", 200)).thenReturn(expected);
|
||||
|
||||
List<SysUser> actual = service.listAvailableUsers(" tester ", 500);
|
||||
|
||||
assertSame(expected, actual);
|
||||
verify(userMapper).selectBackDoorUserList("tester", 200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginCreatesTwoHourTokenForExactUser()
|
||||
{
|
||||
SysUser user = activeUser(12L);
|
||||
LoginUser loginUser = org.mockito.Mockito.mock(LoginUser.class);
|
||||
when(userMapper.selectUserById(12L)).thenReturn(user);
|
||||
when(userDetailsService.createLoginUser(user)).thenReturn(loginUser);
|
||||
when(tokenService.createTokenHourTwo(loginUser)).thenReturn("test-token");
|
||||
|
||||
LoginSession session = service.login(12L);
|
||||
|
||||
assertSame(user, session.getUser());
|
||||
assertEquals("test-token", session.getToken());
|
||||
verify(sysLoginService).recordLoginInfo(12L);
|
||||
verify(tokenService).createTokenHourTwo(loginUser);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginRejectsDeletedUserBeforeTokenCreation()
|
||||
{
|
||||
SysUser user = activeUser(12L);
|
||||
user.setDelFlag("2");
|
||||
when(userMapper.selectUserById(12L)).thenReturn(user);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> service.login(12L));
|
||||
|
||||
assertEquals("用户已删除,不能登录", exception.getMessage());
|
||||
verify(tokenService, never()).createTokenHourTwo(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginRejectsDisabledUserBeforeTokenCreation()
|
||||
{
|
||||
SysUser user = activeUser(12L);
|
||||
user.setStatus("1");
|
||||
when(userMapper.selectUserById(12L)).thenReturn(user);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> service.login(12L));
|
||||
|
||||
assertEquals("用户已停用,不能登录", exception.getMessage());
|
||||
verify(tokenService, never()).createTokenHourTwo(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
private SysUser activeUser(Long userId)
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(userId);
|
||||
user.setUserName("tester");
|
||||
user.setNickName("测试用户");
|
||||
user.setStatus("0");
|
||||
user.setDelFlag("0");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user