feat: Add password protection for backdoor login and user retrieval endpoints with corresponding tests
This commit is contained in:
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user