feat: Implement app backdoor login and user listing for mini-programs with corresponding tests
This commit is contained in:
@@ -3,11 +3,15 @@ package com.ruoyi.web.controller.system;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.web.config.BackDoorLoginProperties;
|
||||
import com.ruoyi.web.service.BackDoorLoginService;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.AppLoginSession;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.LoginSession;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
@@ -19,6 +23,8 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -88,6 +94,54 @@ public class BackDoorLoginController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序测试授权用户列表。
|
||||
*
|
||||
* 与 /backDoor 一样仅在 backdoor.login.enabled=true 时存在,且只返回已关联
|
||||
* AppUser 的最小化展示字段。
|
||||
*/
|
||||
@GetMapping("/app/backDoor/users")
|
||||
public AjaxResult appBackDoorUsers(
|
||||
@RequestParam(value = "keyword", required = false) String keyword)
|
||||
{
|
||||
try
|
||||
{
|
||||
return AjaxResult.success(backDoorLoginService.listAvailableAppUsers(keyword, properties.getMaxUsers()));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("加载小程序测试授权用户列表失败", e);
|
||||
return AjaxResult.error("加载用户列表失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序测试授权登录。返回 AppUser 对应的 LoginSiteUser Token,而非 PC SysUser Token。
|
||||
*/
|
||||
@PostMapping("/app/backDoor/login")
|
||||
public AjaxResult appBackDoorLogin(@RequestBody(required = false) JSONObject body)
|
||||
{
|
||||
Long userId = body == null ? null : body.getLong("userId");
|
||||
try
|
||||
{
|
||||
AppLoginSession session = backDoorLoginService.loginApp(userId);
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put(Constants.TOKEN, session.getToken());
|
||||
ajax.put("appUserId", session.getAppUser().getUserId());
|
||||
ajax.put("isCompanyUser", session.getAppUser().getIsCompanyUser());
|
||||
return ajax;
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("小程序测试授权登录失败,userId={}", userId, e);
|
||||
return AjaxResult.error("授权登录失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<String> renderUserList(String keyword, int page)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.ruoyi.web.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.AppUser;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.enums.UserStatus;
|
||||
@@ -10,6 +12,7 @@ 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.system.service.ISysUserService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -31,16 +34,19 @@ public class BackDoorLoginService
|
||||
private final UserDetailsServiceImpl userDetailsService;
|
||||
private final TokenService tokenService;
|
||||
private final SysLoginService loginService;
|
||||
private final ISysUserService sysUserService;
|
||||
|
||||
public BackDoorLoginService(SysUserMapper userMapper,
|
||||
UserDetailsServiceImpl userDetailsService,
|
||||
TokenService tokenService,
|
||||
SysLoginService loginService)
|
||||
SysLoginService loginService,
|
||||
ISysUserService sysUserService)
|
||||
{
|
||||
this.userMapper = userMapper;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.tokenService = tokenService;
|
||||
this.loginService = loginService;
|
||||
this.sysUserService = sysUserService;
|
||||
}
|
||||
|
||||
public List<SysUser> listAvailableUsers(String keyword, int limit)
|
||||
@@ -54,7 +60,59 @@ public class BackDoorLoginService
|
||||
return userMapper.selectBackDoorUserList(normalizedKeyword, safeLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回可用于小程序测试授权的用户。
|
||||
*
|
||||
* 小程序会话的主体是 AppUser,不能复用 PC 后门签发的 SysUser Token;
|
||||
* 因此这里只暴露已经关联 app_user 的有效测试账号。
|
||||
*/
|
||||
public List<AppLoginUser> listAvailableAppUsers(String keyword, int limit)
|
||||
{
|
||||
return listAvailableUsers(keyword, limit).stream()
|
||||
.filter(user -> user.getAppUserId() != null && user.getAppUserId() > 0)
|
||||
.map(AppLoginUser::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public LoginSession login(Long userId)
|
||||
{
|
||||
SysUser user = getEffectiveUser(userId);
|
||||
|
||||
LoginUser loginUser = (LoginUser) userDetailsService.createLoginUser(user);
|
||||
loginService.recordLoginInfo(user.getUserId());
|
||||
String token = tokenService.createTokenHourTwo(loginUser);
|
||||
|
||||
log.warn("测试环境后门登录:userId={}, userName={}", user.getUserId(), user.getUserName());
|
||||
return new LoginSession(user, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅供测试环境小程序使用的后门授权。
|
||||
*
|
||||
* 先校验与 PC 后门相同的 SysUser 有效性,再通过 app_user_id 找到业务用户,
|
||||
* 由既有小程序登录逻辑签发 LoginSiteUser Token。
|
||||
*/
|
||||
public AppLoginSession loginApp(Long userId)
|
||||
{
|
||||
SysUser user = getEffectiveUser(userId);
|
||||
Long appUserId = user.getAppUserId();
|
||||
if (appUserId == null || appUserId <= 0)
|
||||
{
|
||||
throw new ServiceException("该用户未关联小程序业务账号,不能授权登录");
|
||||
}
|
||||
|
||||
AppUser appUser = sysUserService.selectAppUserById(appUserId);
|
||||
if (appUser == null)
|
||||
{
|
||||
throw new ServiceException("关联的小程序业务账号不存在,不能授权登录");
|
||||
}
|
||||
|
||||
String token = loginService.loginUserIdApp(appUser);
|
||||
log.warn("测试环境小程序后门授权:sysUserId={}, appUserId={}", user.getUserId(), appUser.getUserId());
|
||||
return new AppLoginSession(user, appUser, token);
|
||||
}
|
||||
|
||||
private SysUser getEffectiveUser(Long userId)
|
||||
{
|
||||
if (userId == null || userId <= 0)
|
||||
{
|
||||
@@ -64,13 +122,7 @@ public class BackDoorLoginService
|
||||
SysUser user = userMapper.selectUserById(userId);
|
||||
validateUser(user);
|
||||
validateEffectiveAccount(user);
|
||||
|
||||
LoginUser loginUser = (LoginUser) userDetailsService.createLoginUser(user);
|
||||
loginService.recordLoginInfo(user.getUserId());
|
||||
String token = tokenService.createTokenHourTwo(loginUser);
|
||||
|
||||
log.warn("测试环境后门登录:userId={}, userName={}", user.getUserId(), user.getUserName());
|
||||
return new LoginSession(user, token);
|
||||
return user;
|
||||
}
|
||||
|
||||
private void validateUser(SysUser user)
|
||||
@@ -123,4 +175,77 @@ public class BackDoorLoginService
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
/** 用于小程序后门列表的最小化用户信息,避免返回手机号、证件号等字段。 */
|
||||
public static class AppLoginUser
|
||||
{
|
||||
private final Long userId;
|
||||
private final Long appUserId;
|
||||
private final String userName;
|
||||
private final String nickName;
|
||||
private final String displayName;
|
||||
|
||||
public AppLoginUser(SysUser user)
|
||||
{
|
||||
this.userId = user.getUserId();
|
||||
this.appUserId = user.getAppUserId();
|
||||
this.userName = user.getUserName();
|
||||
this.nickName = user.getNickName();
|
||||
this.displayName = StringUtils.defaultIfBlank(user.getNickName(), user.getUserName());
|
||||
}
|
||||
|
||||
public Long getUserId()
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
|
||||
public Long getAppUserId()
|
||||
{
|
||||
return appUserId;
|
||||
}
|
||||
|
||||
public String getUserName()
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
public String getNickName()
|
||||
{
|
||||
return nickName;
|
||||
}
|
||||
|
||||
public String getDisplayName()
|
||||
{
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AppLoginSession
|
||||
{
|
||||
private final SysUser sysUser;
|
||||
private final AppUser appUser;
|
||||
private final String token;
|
||||
|
||||
public AppLoginSession(SysUser sysUser, AppUser appUser, String token)
|
||||
{
|
||||
this.sysUser = sysUser;
|
||||
this.appUser = appUser;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public SysUser getSysUser()
|
||||
{
|
||||
return sysUser;
|
||||
}
|
||||
|
||||
public AppUser getAppUser()
|
||||
{
|
||||
return appUser;
|
||||
}
|
||||
|
||||
public String getToken()
|
||||
{
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ package com.ruoyi.web.controller.system;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.AppUser;
|
||||
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.AppLoginSession;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.LoginSession;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -78,4 +82,24 @@ class BackDoorLoginControllerTest
|
||||
assertFalse(response.getBody().contains("/management/"));
|
||||
assertFalse(response.getBody().contains("?token=signed-token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appBackDoorLoginReturnsSiteToken()
|
||||
{
|
||||
SysUser sysUser = new SysUser();
|
||||
sysUser.setUserId(8L);
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setUserId(66L);
|
||||
appUser.setIsCompanyUser("1");
|
||||
when(loginService.loginApp(8L)).thenReturn(new AppLoginSession(sysUser, appUser, "site-token"));
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("userId", 8L);
|
||||
|
||||
AjaxResult response = controller.appBackDoorLogin(body);
|
||||
|
||||
assertEquals(200, response.get("code"));
|
||||
assertEquals("site-token", response.get("token"));
|
||||
assertEquals(66L, response.get("appUserId"));
|
||||
assertEquals("1", response.get("isCompanyUser"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.web.service;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.AppUser;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
@@ -10,6 +11,8 @@ 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.system.service.ISysUserService;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.AppLoginSession;
|
||||
import com.ruoyi.web.service.BackDoorLoginService.LoginSession;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -35,13 +38,15 @@ class BackDoorLoginServiceTest
|
||||
private TokenService tokenService;
|
||||
@Mock
|
||||
private SysLoginService sysLoginService;
|
||||
@Mock
|
||||
private ISysUserService sysUserService;
|
||||
|
||||
private BackDoorLoginService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
service = new BackDoorLoginService(userMapper, userDetailsService, tokenService, sysLoginService);
|
||||
service = new BackDoorLoginService(userMapper, userDetailsService, tokenService, sysLoginService, sysUserService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,6 +118,41 @@ class BackDoorLoginServiceTest
|
||||
verify(tokenService, never()).createTokenHourTwo(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginAppCreatesSiteTokenForLinkedAppUser()
|
||||
{
|
||||
SysUser user = activeUser(12L);
|
||||
user.setAppUserId(66L);
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setUserId(66L);
|
||||
appUser.setName("测试求职者");
|
||||
when(userMapper.selectUserById(12L)).thenReturn(user);
|
||||
when(userMapper.selectBackDoorEffectiveUserId("tester")).thenReturn(12L);
|
||||
when(sysUserService.selectAppUserById(66L)).thenReturn(appUser);
|
||||
when(sysLoginService.loginUserIdApp(appUser)).thenReturn("site-test-token");
|
||||
|
||||
AppLoginSession session = service.loginApp(12L);
|
||||
|
||||
assertSame(user, session.getSysUser());
|
||||
assertSame(appUser, session.getAppUser());
|
||||
assertEquals("site-test-token", session.getToken());
|
||||
verify(sysLoginService).loginUserIdApp(appUser);
|
||||
verify(tokenService, never()).createTokenHourTwo(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginAppRejectsUserWithoutAppUserLink()
|
||||
{
|
||||
SysUser user = activeUser(12L);
|
||||
when(userMapper.selectUserById(12L)).thenReturn(user);
|
||||
when(userMapper.selectBackDoorEffectiveUserId("tester")).thenReturn(12L);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> service.loginApp(12L));
|
||||
|
||||
assertEquals("该用户未关联小程序业务账号,不能授权登录", exception.getMessage());
|
||||
verify(sysLoginService, never()).loginUserIdApp(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
private SysUser activeUser(Long userId)
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
|
||||
Reference in New Issue
Block a user