diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java index 587f0bf..f9e446c 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/BackDoorLoginController.java @@ -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 renderUserList(String keyword, int page) { try diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java index b388244..259b5a9 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/service/BackDoorLoginService.java @@ -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 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 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; + } + } } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java index d4cbf5b..c874c59 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/BackDoorLoginControllerTest.java @@ -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")); + } } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java index 27b84dc..5279b8e 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/web/service/BackDoorLoginServiceTest.java @@ -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();