feat: Add password protection for backdoor login and user retrieval endpoints with corresponding tests

This commit is contained in:
2026-07-21 14:03:07 +08:00
parent 9039effee4
commit 585ddaae9a
4 changed files with 103 additions and 3 deletions

View File

@@ -1,5 +1,9 @@
package com.ruoyi.web.config; package com.ruoyi.web.config;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -16,6 +20,10 @@ public class BackDoorLoginProperties
private boolean enabled; private boolean enabled;
private int maxUsers = DEFAULT_MAX_USERS; private int maxUsers = DEFAULT_MAX_USERS;
private String portalBasePath = "/shihezi"; private String portalBasePath = "/shihezi";
/**
* 测试授权口令。未配置时,小程序测试授权接口应拒绝所有请求。
*/
private String password;
public boolean isEnabled() public boolean isEnabled()
{ {
@@ -47,6 +55,31 @@ public class BackDoorLoginProperties
this.portalBasePath = portalBasePath; this.portalBasePath = portalBasePath;
} }
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
/**
* 验证小程序测试授权口令。
*
* 未设置口令时必须拒绝访问,避免某个测试 profile 误开启后门后暴露用户列表。
*/
public boolean matchesPassword(String candidate)
{
if (StringUtils.isBlank(password) || candidate == null)
{
return false;
}
return MessageDigest.isEqual(password.getBytes(StandardCharsets.UTF_8),
candidate.trim().getBytes(StandardCharsets.UTF_8));
}
private String normalizeBasePath(String value, String defaultValue) private String normalizeBasePath(String value, String defaultValue)
{ {
if (value == null || value.trim().isEmpty()) if (value == null || value.trim().isEmpty())

View File

@@ -100,12 +100,16 @@ public class BackDoorLoginController
* 与 /backDoor 一样仅在 backdoor.login.enabled=true 时存在,且只返回已关联 * 与 /backDoor 一样仅在 backdoor.login.enabled=true 时存在,且只返回已关联
* AppUser 的最小化展示字段。 * AppUser 的最小化展示字段。
*/ */
@GetMapping("/app/backDoor/users") @PostMapping("/app/backDoor/users")
public AjaxResult appBackDoorUsers( public AjaxResult appBackDoorUsers(@RequestBody(required = false) JSONObject body)
@RequestParam(value = "keyword", required = false) String keyword)
{ {
if (!matchesAppBackDoorPassword(body))
{
return AjaxResult.error("测试授权密码错误");
}
try try
{ {
String keyword = body == null ? null : body.getString("keyword");
return AjaxResult.success(backDoorLoginService.listAvailableAppUsers(keyword, properties.getMaxUsers())); return AjaxResult.success(backDoorLoginService.listAvailableAppUsers(keyword, properties.getMaxUsers()));
} }
catch (Exception e) catch (Exception e)
@@ -121,6 +125,10 @@ public class BackDoorLoginController
@PostMapping("/app/backDoor/login") @PostMapping("/app/backDoor/login")
public AjaxResult appBackDoorLogin(@RequestBody(required = false) JSONObject body) public AjaxResult appBackDoorLogin(@RequestBody(required = false) JSONObject body)
{ {
if (!matchesAppBackDoorPassword(body))
{
return AjaxResult.error("测试授权密码错误");
}
Long userId = body == null ? null : body.getLong("userId"); Long userId = body == null ? null : body.getLong("userId");
try try
{ {
@@ -142,6 +150,11 @@ public class BackDoorLoginController
} }
} }
private boolean matchesAppBackDoorPassword(JSONObject body)
{
return properties.matchesPassword(body == null ? null : body.getString("password"));
}
private ResponseEntity<String> renderUserList(String keyword, int page) private ResponseEntity<String> renderUserList(String keyword, int page)
{ {
try try

View File

@@ -151,3 +151,4 @@ backdoor:
enabled: true enabled: true
max-users: 100 max-users: 100
portal-base-path: /shihezi portal-base-path: /shihezi
password: zkr2024

View File

@@ -8,6 +8,7 @@ import com.ruoyi.common.core.domain.entity.AppUser;
import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.web.config.BackDoorLoginProperties; import com.ruoyi.web.config.BackDoorLoginProperties;
import com.ruoyi.web.service.BackDoorLoginService; import com.ruoyi.web.service.BackDoorLoginService;
import com.ruoyi.web.service.BackDoorLoginService.AppLoginUser;
import com.ruoyi.web.service.BackDoorLoginService.AppLoginSession; import com.ruoyi.web.service.BackDoorLoginService.AppLoginSession;
import com.ruoyi.web.service.BackDoorLoginService.LoginSession; import com.ruoyi.web.service.BackDoorLoginService.LoginSession;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -37,6 +38,7 @@ class BackDoorLoginControllerTest
void setUp() void setUp()
{ {
BackDoorLoginProperties properties = new BackDoorLoginProperties(); BackDoorLoginProperties properties = new BackDoorLoginProperties();
properties.setPassword("zkr2024");
controller = new BackDoorLoginController(loginService, new BackDoorPageRenderer(), properties); controller = new BackDoorLoginController(loginService, new BackDoorPageRenderer(), properties);
} }
@@ -94,6 +96,7 @@ class BackDoorLoginControllerTest
when(loginService.loginApp(8L)).thenReturn(new AppLoginSession(sysUser, appUser, "site-token")); when(loginService.loginApp(8L)).thenReturn(new AppLoginSession(sysUser, appUser, "site-token"));
JSONObject body = new JSONObject(); JSONObject body = new JSONObject();
body.put("userId", 8L); body.put("userId", 8L);
body.put("password", "zkr2024");
AjaxResult response = controller.appBackDoorLogin(body); AjaxResult response = controller.appBackDoorLogin(body);
@@ -102,4 +105,54 @@ class BackDoorLoginControllerTest
assertEquals(66L, response.get("appUserId")); assertEquals(66L, response.get("appUserId"));
assertEquals("1", response.get("isCompanyUser")); assertEquals("1", response.get("isCompanyUser"));
} }
@Test
void appBackDoorUsersRequiresPasswordBeforeLoadingUserData()
{
JSONObject body = new JSONObject();
body.put("keyword", "tester");
body.put("password", "wrong-password");
AjaxResult response = controller.appBackDoorUsers(body);
assertEquals(500, response.get("code"));
assertEquals("测试授权密码错误", response.get("msg"));
verify(loginService, never()).listAvailableAppUsers(org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.anyInt());
}
@Test
void appBackDoorUsersReturnsLinkedAppUsersAfterPasswordVerification()
{
SysUser sysUser = new SysUser();
sysUser.setUserId(8L);
sysUser.setAppUserId(66L);
sysUser.setUserName("tester");
sysUser.setNickName("测试用户");
when(loginService.listAvailableAppUsers("tester", 100))
.thenReturn(Collections.singletonList(new AppLoginUser(sysUser)));
JSONObject body = new JSONObject();
body.put("keyword", "tester");
body.put("password", "zkr2024");
AjaxResult response = controller.appBackDoorUsers(body);
assertEquals(200, response.get("code"));
assertEquals(1, ((java.util.List<?>) response.get("data")).size());
verify(loginService).listAvailableAppUsers("tester", 100);
}
@Test
void appBackDoorLoginRequiresPasswordBeforeCreatingToken()
{
JSONObject body = new JSONObject();
body.put("userId", 8L);
body.put("password", "wrong-password");
AjaxResult response = controller.appBackDoorLogin(body);
assertEquals(500, response.get("code"));
assertEquals("测试授权密码错误", response.get("msg"));
verify(loginService, never()).loginApp(org.mockito.ArgumentMatchers.any());
}
} }