新增学校统一身份认证登录
- GxmuAuthProperties 承载接入配置,认证客户端按微服务认证平台 API v2.3 调 cas.gxmu.edu.cn - /api/sso/login 与 /api/sso/available 免鉴权放行 - 校验通过后按学号认领学生,名册外的账号默认拒绝登录
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package com.gxwebsoft.gxmu.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 统一身份认证客户端的**联网**验证。
|
||||
*
|
||||
* <p>默认<b>不执行</b>(类名以 IT 结尾,surefire 不会扫到)。手动运行:
|
||||
*
|
||||
* <pre>
|
||||
* ./mvnw test -Dtest=LyuapAuthClientIT -Dgxmu.auth.it=true
|
||||
* </pre>
|
||||
*
|
||||
* <p>它存在的意义:其余测试只证明了 MD5/Base64 的密文与文档一致,
|
||||
* 而 Hutool 的表单请求构造(form 编码、UTF-8、超时)从未真正打到过认证平台。
|
||||
* 这个测试用真实接口把这一段补上——不需要数据库,也不需要真实学生密码,
|
||||
* 只验证"请求发得出去、平台认得我们的 appid 与 secret、报文解析得回来"。
|
||||
*
|
||||
* <p>需要一个不存在的学号来避免误伤真实用户,示例值 {@code 0}。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-09-18
|
||||
*/
|
||||
@EnabledIfSystemProperty(named = "gxmu.auth.it", matches = "true")
|
||||
class LyuapAuthClientIT {
|
||||
|
||||
/**
|
||||
* 与 application.yml 的默认值保持一致;可用环境变量覆盖。
|
||||
*/
|
||||
private static final String APP_ID = System.getenv().getOrDefault(
|
||||
"GXMU_AUTH_APPID", "7D8F6695611309E0");
|
||||
private static final String APP_SECRET = System.getenv().getOrDefault(
|
||||
"GXMU_AUTH_APPSECRET", "24f9dab5-3319-42cc-908d-2748417b7f92");
|
||||
|
||||
/**
|
||||
* 一定不存在的学号,避免拿真实用户做实验
|
||||
*/
|
||||
private static final String UNKNOWN_USERNAME = "0";
|
||||
|
||||
/**
|
||||
* 真人在册学号(2026-09-18 由校方提供),用于验证成功分支的解析
|
||||
*/
|
||||
private static final String REAL_STUDENT_NO = "320241167";
|
||||
|
||||
private LyuapAuthClient newClient() {
|
||||
GxmuAuthProperties properties = new GxmuAuthProperties();
|
||||
properties.setAppId(APP_ID);
|
||||
properties.setAppSecret(APP_SECRET);
|
||||
properties.setTimeoutMs(15000);
|
||||
LyuapAuthClient client = new LyuapAuthClient();
|
||||
ReflectionTestUtils.setField(client, "properties", properties);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* appid 与 secret 被平台认下:查询一个不存在的学号,得到的是"用户不存在",
|
||||
* 而不是"appid 不存在"或"secret 校验失败"。
|
||||
*/
|
||||
@Test
|
||||
void getUserInfo_appIdAndSecretAreAccepted() {
|
||||
LyuapAuthClient client = newClient();
|
||||
GxmuAuthException error = assertThrows(GxmuAuthException.class,
|
||||
() -> client.getUserInfo(UNKNOWN_USERNAME));
|
||||
assertEquals("学号不存在,请确认后重试", error.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功分支:真实学号的 getUserInfo 必须解析出用户对象。
|
||||
*
|
||||
* <p>平台把用户信息放在 message 里且是转义 JSON 字符串——这一段只有打到真接口才能发现,
|
||||
* mock 出来的报文永远是"我以为的样子"。
|
||||
*/
|
||||
@Test
|
||||
void getUserInfo_parsesRealStudent() {
|
||||
LyuapAuthClient client = newClient();
|
||||
LyuapUser user = client.getUserInfo(REAL_STUDENT_NO);
|
||||
assertNotNull(user, "真实学号应能取到用户信息");
|
||||
assertEquals(REAL_STUDENT_NO, user.getYhbh());
|
||||
assertNotNull(user.getYhmc(), "姓名不应为空");
|
||||
assertTrue(user.isStudent(), "YHLXMC 实际为: " + user.getYhlxmc());
|
||||
assertNotNull(user.getSjh(), "手机号不应为空,否则认领的手机号匹配分支不可用");
|
||||
System.out.println("真实用户: " + user.getYhbh() + " / " + user.getYhmc()
|
||||
+ " / 类型=" + user.getYhlxmc() + " / 状态ZT=" + user.getZt()
|
||||
+ " / 部门=" + user.getBmmc() + " / 在册=" + user.isActive());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void secret_computedByClientIsAcceptedByPlatform() {
|
||||
String secret = LyuapAuthClient.secret(UNKNOWN_USERNAME, APP_SECRET, new java.util.Date());
|
||||
assertNotNull(secret);
|
||||
assertEquals(32, secret.length());
|
||||
|
||||
// 只要平台回的是业务码(而不是 secret 校验失败),就说明加密串是对的
|
||||
LyuapAuthClient client = newClient();
|
||||
GxmuAuthException error = assertThrows(GxmuAuthException.class,
|
||||
() -> client.getUserInfo(UNKNOWN_USERNAME));
|
||||
assertTrue(error.getMessage().contains("学号不存在"), "实际: " + error.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码字段的编码规则也要被平台接受:假密码 + 不存在的用户,
|
||||
* 平台应当回"用户不存在"而不是"加密串/参数"类错误。
|
||||
*/
|
||||
@Test
|
||||
void checkPassword_encodingIsAcceptedByPlatform() {
|
||||
LyuapAuthClient client = newClient();
|
||||
GxmuAuthException error = assertThrows(GxmuAuthException.class,
|
||||
() -> client.checkPassword(UNKNOWN_USERNAME, "not-a-real-password"));
|
||||
assertTrue(error.getMessage().contains("学号不存在")
|
||||
|| error.getMessage().contains("学号或密码错误"),
|
||||
"实际: " + error.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.gxwebsoft.gxmu.auth;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 统一身份认证客户端的纯逻辑测试(不依赖数据库与网络)。
|
||||
*
|
||||
* <p>加密规则的期望值由 python hashlib/base64 独立算出,与《微服务认证平台API接口说明文档 v2.3》§3 对齐。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-09-15
|
||||
*/
|
||||
class LyuapAuthClientTest {
|
||||
|
||||
private static final String APP_SECRET = "709ca5e8410c447b93f9672b2bb8112a";
|
||||
|
||||
@Test
|
||||
void secret_matchesDocumentRule() {
|
||||
Date date = DateUtil.parse("2026-09-15", "yyyy-MM-dd");
|
||||
assertEquals("60E9C2F39FE790BE79CE98FAD9B90379",
|
||||
LyuapAuthClient.secret("lyadmin", APP_SECRET, date));
|
||||
}
|
||||
|
||||
@Test
|
||||
void secret_isThirtyTwoUpperCaseChars() {
|
||||
Date date = DateUtil.parse("2026-01-01", "yyyy-MM-dd");
|
||||
String secret = LyuapAuthClient.secret("201510051", APP_SECRET, date);
|
||||
assertEquals("901F8C2461C5F7729B743D2F7372C931", secret);
|
||||
assertEquals(32, secret.length());
|
||||
assertEquals(secret.toUpperCase(), secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void secret_returnsNullWhenInputIncomplete() {
|
||||
Date date = new Date();
|
||||
assertNull(LyuapAuthClient.secret(null, APP_SECRET, date));
|
||||
assertNull(LyuapAuthClient.secret("test", null, date));
|
||||
assertNull(LyuapAuthClient.secret("test", APP_SECRET, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档 §3.1:{@code Base64(Base64(数据) + appid)}
|
||||
*/
|
||||
@Test
|
||||
void encodePassword_matchesDocumentRule() {
|
||||
assertEquals("TVRJek5EVTJteWFwcGlk", LyuapAuthClient.encodePassword("123456", "myappid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodePassword_returnsNullWhenInputIncomplete() {
|
||||
assertNull(LyuapAuthClient.encodePassword(null, "myappid"));
|
||||
assertNull(LyuapAuthClient.encodePassword("123456", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unwrapContent_returnsInnerContentObject() {
|
||||
JSONObject content = LyuapAuthClient.unwrapContent(
|
||||
"{\"content\":{\"code\":\"2\",\"message\":\"appid不存在或被禁用\"}}");
|
||||
assertEquals("2", content.getString("code"));
|
||||
assertEquals("appid不存在或被禁用", content.getString("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unwrapContent_fallsBackToRootAndNull() {
|
||||
assertEquals("1", LyuapAuthClient.unwrapContent("{\"code\":\"1\"}").getString("code"));
|
||||
assertNull(LyuapAuthClient.unwrapContent(""));
|
||||
assertNull(LyuapAuthClient.unwrapContent("<html>not json</html>"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2026-09-18 真实学号 320241167 的原始响应。
|
||||
*
|
||||
* <p>平台把用户信息放在 message 里,而且是**转义过的 JSON 字符串**。
|
||||
* 之前只认 "message 是 JSONObject" 的写法,真实调用会一路走到
|
||||
* "统一身份认证平台未返回用户信息"。这条用例就是钉住这个 bug。
|
||||
*/
|
||||
@Test
|
||||
void parseUser_readsUserFromEscapedJsonStringInMessage() {
|
||||
String body = "{\"content\":{\"code\":\"0\",\"message\":\"{\\\"XBM\\\":\\\"1\\\",\\\"SFZH\\\":\\\"431126200608030379\\\","
|
||||
+ "\\\"YHLXMC\\\":\\\"学生\\\",\\\"BM\\\":\\\"gxmu320241167\\\",\\\"YHMC\\\":\\\"柏泽旭\\\","
|
||||
+ "\\\"SJH\\\":\\\"19114820589\\\",\\\"DZYX\\\":\\\"320241167@gxmu.edu.cn\\\",\\\"SFBDSJ\\\":1,"
|
||||
+ "\\\"ZT\\\":\\\"1\\\",\\\"YHBH\\\":\\\"320241167\\\",\\\"BMMC\\\":\\\"2024级康复治疗技术18班\\\"}\"}}";
|
||||
JSONObject content = LyuapAuthClient.unwrapContent(body);
|
||||
assertEquals("0", content.getString("code"));
|
||||
|
||||
LyuapUser user = LyuapAuthClient.parseUser(content);
|
||||
assertEquals("320241167", user.getYhbh());
|
||||
assertEquals("柏泽旭", user.getYhmc());
|
||||
assertEquals("19114820589", user.getSjh());
|
||||
assertEquals("2024级康复治疗技术18班", user.getBmmc());
|
||||
assertEquals("320241167@gxmu.edu.cn", user.getDzyx());
|
||||
assertEquals("431126200608030379", user.getSfzh());
|
||||
assertTrue(user.isStudent());
|
||||
assertTrue(user.isActive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 2026-09-18 真实学号 20205280988(ZT=0,已停用/毕业)。
|
||||
*/
|
||||
@Test
|
||||
void parseUser_readsInactiveUserAndMarksItInactive() {
|
||||
String body = "{\"content\":{\"code\":\"0\",\"message\":\"{\\\"YHLXMC\\\":\\\"学生\\\",\\\"YHMC\\\":\\\"陈燕梅\\\","
|
||||
+ "\\\"SJH\\\":\\\"18178009874\\\",\\\"ZT\\\":\\\"0\\\",\\\"YHBH\\\":\\\"20205280988\\\"}\"}}";
|
||||
LyuapUser user = LyuapAuthClient.parseUser(LyuapAuthClient.unwrapContent(body));
|
||||
assertEquals("20205280988", user.getYhbh());
|
||||
assertEquals("陈燕梅", user.getYhmc());
|
||||
assertTrue(user.isStudent());
|
||||
assertFalse(user.isActive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误提示文本不能被当成用户对象。
|
||||
*/
|
||||
@Test
|
||||
void parseUser_ignoresPlainTextMessage() {
|
||||
assertNull(LyuapAuthClient.parseUser(LyuapAuthClient.unwrapContent(
|
||||
"{\"content\":{\"code\":\"4\",\"message\":\"用户不存在\"}}")));
|
||||
assertNull(LyuapAuthClient.parseUser(LyuapAuthClient.unwrapContent(
|
||||
"{\"content\":{\"code\":\"3\",\"message\":\"secret加密串校验失败\"}}")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档 2.1.4 的示例:用户字段直接躺在 message 对象里。
|
||||
*/
|
||||
@Test
|
||||
void parseUser_readsUserFromMessageObject() {
|
||||
JSONObject content = LyuapAuthClient.unwrapContent("{\"content\":{\"code\":\"1\",\"message\":{"
|
||||
+ "\"YHBH\":\"test0001\",\"YHMC\":\"张三\",\"YHLXMC\":\"学生\",\"ZT\":\"1\","
|
||||
+ "\"SJH\":\"1358787454\",\"BMMC\":\"第一临床医学院\"}}}");
|
||||
LyuapUser user = LyuapAuthClient.parseUser(content);
|
||||
assertEquals("test0001", user.getYhbh());
|
||||
assertEquals("张三", user.getYhmc());
|
||||
assertEquals("1358787454", user.getSjh());
|
||||
assertTrue(user.isStudent());
|
||||
assertTrue(user.isActive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档 2.1.3 的表格:用户字段在 data 里。两种都要认。
|
||||
*/
|
||||
@Test
|
||||
void parseUser_readsUserFromDataObject() {
|
||||
JSONObject content = LyuapAuthClient.unwrapContent("{\"content\":{\"code\":\"0\",\"data\":{"
|
||||
+ "\"YHBH\":\"201510051\",\"YHMC\":\"李四\",\"YHLXMC\":\"4\"}}}");
|
||||
LyuapUser user = LyuapAuthClient.parseUser(content);
|
||||
assertEquals("201510051", user.getYhbh());
|
||||
assertTrue(user.isStudent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseUser_returnsNullWhenNoUserObject() {
|
||||
assertNull(LyuapAuthClient.parseUser(LyuapAuthClient.unwrapContent(
|
||||
"{\"content\":{\"code\":\"4\",\"message\":\"用户不存在\"}}")));
|
||||
assertNull(LyuapAuthClient.parseUser(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isSuccess_acceptsCodeZeroAndSuccessText() {
|
||||
assertTrue(LyuapAuthClient.isSuccess("0", "操作成功!"));
|
||||
// 文档 2.1.4 自相矛盾:CODE 表写 0 成功,示例却是 1
|
||||
assertTrue(LyuapAuthClient.isSuccess("1", "操作成功!"));
|
||||
assertFalse(LyuapAuthClient.isSuccess("6", "密码错误"));
|
||||
assertFalse(LyuapAuthClient.isSuccess(null, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2026-09-18 用真实 appid 实测到的报文,与文档 2.4.4 的 CODE 表完全不同:
|
||||
* 1=username参数不能为空、2=appid不存在或被禁用、3=secret加密串校验失败、4=用户不存在。
|
||||
*/
|
||||
@Test
|
||||
void describeFailure_prefersPlatformMessage() {
|
||||
assertEquals("统一身份认证应用未注册或已停用,请联系管理员",
|
||||
LyuapAuthClient.describeFailure("2", "appid不存在或被禁用", "兜底"));
|
||||
assertEquals("统一身份认证平台校验失败,请稍后重试",
|
||||
LyuapAuthClient.describeFailure("3", "secret加密串校验失败", "兜底"));
|
||||
assertEquals("请输入学号",
|
||||
LyuapAuthClient.describeFailure("1", "username参数不能为空", "兜底"));
|
||||
assertEquals("请输入密码",
|
||||
LyuapAuthClient.describeFailure("3", "密码不能为空", "兜底"));
|
||||
assertEquals("学号不存在,请确认后重试",
|
||||
LyuapAuthClient.describeFailure(null, "用户不存在", "兜底"));
|
||||
assertEquals("学号不存在,请确认后重试",
|
||||
LyuapAuthClient.describeFailure("4", "没有对应的用户", "兜底"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void describeFailure_fallsBackToCodeThenDefault() {
|
||||
assertEquals("学号或密码错误",
|
||||
LyuapAuthClient.describeFailure("6", "操作失败!", "兜底"));
|
||||
assertEquals("兜底", LyuapAuthClient.describeFailure(null, null, "兜底"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lyuapUser_studentAndActiveJudgement() {
|
||||
LyuapUser teacher = new LyuapUser();
|
||||
teacher.setYhlxmc("教职工");
|
||||
assertFalse(teacher.isStudent());
|
||||
|
||||
LyuapUser numeric = new LyuapUser();
|
||||
numeric.setYhlxmc("3");
|
||||
assertTrue(numeric.isStudent());
|
||||
|
||||
LyuapUser graduate = new LyuapUser();
|
||||
graduate.setYhlxmc("研究生");
|
||||
assertTrue(graduate.isStudent());
|
||||
|
||||
// ZT=0 只是平台侧的状态值。注意它**不是登录闸门**(已毕业学生就是 ZT=0,仍要放行),
|
||||
// 登录放行与否看本系统账号的 status,见 SsoLoginServiceImplTest。
|
||||
LyuapUser inactive = new LyuapUser();
|
||||
inactive.setZt("0");
|
||||
assertFalse(inactive.isActive());
|
||||
|
||||
LyuapUser active = new LyuapUser();
|
||||
active.setZt("1");
|
||||
assertTrue(active.isActive());
|
||||
|
||||
// 平台没给状态时算激活
|
||||
assertTrue(new LyuapUser().isActive());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package com.gxwebsoft.gxmu.auth;
|
||||
|
||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.core.utils.CacheClient;
|
||||
import com.gxwebsoft.common.system.entity.Role;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.entity.UserRole;
|
||||
import com.gxwebsoft.common.system.result.LoginResult;
|
||||
import com.gxwebsoft.common.system.service.LoginRecordService;
|
||||
import com.gxwebsoft.common.system.service.RoleService;
|
||||
import com.gxwebsoft.common.system.service.UserRoleService;
|
||||
import com.gxwebsoft.common.system.service.UserService;
|
||||
import com.gxwebsoft.gxmu.openplat.claim.StudentClaimService;
|
||||
import com.gxwebsoft.gxmu.openplat.entity.GxmuStudent;
|
||||
import com.gxwebsoft.gxmu.openplat.mapper.GxmuStudentMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 统一身份认证登录的账号映射测试(mock 掉认证平台与数据库)。
|
||||
*
|
||||
* <p>覆盖最容易出错的一环:同一个学生可能已经有微信账号、可能名册已认领、
|
||||
* 也可能名册里根本没有——三种情况必须走到同一个账号上,且不能重复建号。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-09-15
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class SsoLoginServiceImplTest {
|
||||
|
||||
private static final Integer TENANT_ID = 10049;
|
||||
private static final String STUDENT_NO = "201510051";
|
||||
private static final String TOKEN_KEY = "WLgNsWJ8rPjRtnjzX/Gx2RGS80Kwnm/ZeLbvIL+NrBs=";
|
||||
|
||||
@Mock
|
||||
private GxmuAuthProperties properties;
|
||||
|
||||
@Mock
|
||||
private LyuapAuthClient authClient;
|
||||
|
||||
@Mock
|
||||
private UserService userService;
|
||||
|
||||
@Mock
|
||||
private RoleService roleService;
|
||||
|
||||
@Mock
|
||||
private UserRoleService userRoleService;
|
||||
|
||||
@Mock
|
||||
private GxmuStudentMapper studentMapper;
|
||||
|
||||
@Mock
|
||||
private StudentClaimService studentClaimService;
|
||||
|
||||
@Mock
|
||||
private ConfigProperties configProperties;
|
||||
|
||||
@Mock
|
||||
private CacheClient cacheClient;
|
||||
|
||||
@Mock
|
||||
private LoginRecordService loginRecordService;
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
@InjectMocks
|
||||
private SsoLoginServiceImpl ssoLoginService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(properties.isEnabled()).thenReturn(true);
|
||||
when(properties.getAppId()).thenReturn("gxmu-app");
|
||||
when(properties.getAppSecret()).thenReturn("app-secret");
|
||||
when(configProperties.getTokenExpireTime()).thenReturn(3600L);
|
||||
when(configProperties.getTokenKey()).thenReturn(TOKEN_KEY);
|
||||
when(cacheClient.getSettingInfo(anyString(), anyInt())).thenReturn(null);
|
||||
// 默认:认证平台认得这个学号,且是在读学生
|
||||
when(authClient.getUserInfo(anyString())).thenReturn(userInfo("学生", "1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 名册已认领:直接复用已关联的账号(学生先用微信注册、认领过的那种)。
|
||||
*/
|
||||
@Test
|
||||
void login_reusesAccountAlreadyClaimedByRoster() {
|
||||
GxmuStudent student = student(1, 7);
|
||||
when(studentMapper.selectOne(any())).thenReturn(student);
|
||||
|
||||
User existing = new User();
|
||||
existing.setUserId(7);
|
||||
existing.setUsername("wx_abc123456789");
|
||||
existing.setStatus(0);
|
||||
when(userService.getById(7)).thenReturn(existing);
|
||||
when(userService.getByUsername("wx_abc123456789", TENANT_ID)).thenReturn(existing);
|
||||
|
||||
LoginResult result = ssoLoginService.login(STUDENT_NO, "pwd", TENANT_ID, request);
|
||||
|
||||
assertNotNull(result.getAccess_token());
|
||||
assertEquals("wx_abc123456789", result.getUser().getUsername());
|
||||
verify(userService, never()).saveUser(any());
|
||||
// 已是当前账号认领的,无需再次认领
|
||||
verify(studentClaimService, never()).claimByXh(any(), anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 名册有、未认领、账号也没有:建号 + 认领。
|
||||
*/
|
||||
@Test
|
||||
void login_createsAccountAndClaimsWhenRosterUnclaimed() {
|
||||
GxmuStudent student = student(2, null);
|
||||
when(studentMapper.selectOne(any())).thenReturn(student);
|
||||
|
||||
when(userService.getByUsername(anyString(), any())).thenReturn(null);
|
||||
when(userService.getOne(any())).thenReturn(null);
|
||||
when(userService.getByPhone(anyString())).thenReturn(null);
|
||||
Role role = new Role();
|
||||
role.setRoleId(5);
|
||||
when(roleService.getOne(any(), anyBoolean())).thenReturn(role);
|
||||
when(userService.saveUser(any())).thenAnswer(invocation -> {
|
||||
User saved = invocation.getArgument(0);
|
||||
saved.setUserId(99);
|
||||
return true;
|
||||
});
|
||||
when(studentClaimService.claimByXh(any(), eq(STUDENT_NO))).thenReturn(true);
|
||||
|
||||
LoginResult result = ssoLoginService.login(STUDENT_NO, "pwd", TENANT_ID, request);
|
||||
|
||||
assertNotNull(result.getAccess_token());
|
||||
assertEquals(STUDENT_NO, result.getUser().getUsername());
|
||||
assertEquals(99, result.getUser().getUserId());
|
||||
verify(userService).saveUser(any());
|
||||
verify(userRoleService).save(any(UserRole.class));
|
||||
verify(studentClaimService).claimByXh(any(), eq(STUDENT_NO));
|
||||
}
|
||||
|
||||
/**
|
||||
* 名册没有、本系统也没有账号、且未放开"名册外登录":拒绝,并说明原因。
|
||||
*/
|
||||
@Test
|
||||
void login_rejectsStudentMissingFromRoster() {
|
||||
when(studentMapper.selectOne(any())).thenReturn(null);
|
||||
when(userService.getByUsername(anyString(), any())).thenReturn(null);
|
||||
when(userService.getOne(any())).thenReturn(null);
|
||||
when(userService.getByPhone(anyString())).thenReturn(null);
|
||||
when(properties.isAllowUnknownUser()).thenReturn(false);
|
||||
|
||||
BusinessException error = assertThrows(BusinessException.class,
|
||||
() -> ssoLoginService.login(STUDENT_NO, "pwd", TENANT_ID, request));
|
||||
assertEquals("学号未在学生名册中,请联系团委", error.getMessage());
|
||||
verify(userService, never()).saveUser(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* 教师(非学生)没有账号时拒绝,按 ADR-0001 教师不建登录账号。
|
||||
*/
|
||||
@Test
|
||||
void login_rejectsTeacherWithoutAccount() {
|
||||
when(studentMapper.selectOne(any())).thenReturn(null);
|
||||
when(userService.getByUsername(anyString(), any())).thenReturn(null);
|
||||
when(userService.getOne(any())).thenReturn(null);
|
||||
when(userService.getByPhone(anyString())).thenReturn(null);
|
||||
when(properties.isAllowUnknownUser()).thenReturn(false);
|
||||
when(authClient.getUserInfo(STUDENT_NO)).thenReturn(userInfo("教职工", "1"));
|
||||
|
||||
BusinessException error = assertThrows(BusinessException.class,
|
||||
() -> ssoLoginService.login(STUDENT_NO, "pwd", TENANT_ID, request));
|
||||
assertEquals("该账号未在本系统开通,教师请使用 PC 后台登录", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void login_propagatesPasswordFailure() {
|
||||
doThrow(new GxmuAuthException("6", "学号或密码错误"))
|
||||
.when(authClient).checkPassword(STUDENT_NO, "bad");
|
||||
|
||||
BusinessException error = assertThrows(BusinessException.class,
|
||||
() -> ssoLoginService.login(STUDENT_NO, "bad", TENANT_ID, request));
|
||||
assertEquals("学号或密码错误", error.getMessage());
|
||||
verify(authClient, never()).getUserInfo(anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证平台 ZT=0(已毕业)**不拦登录**:毕业生也要能进系统办团组织关系转接。
|
||||
* 2026-09-18 真实学号 20205280988(2020 级,已毕业)就是 ZT=0。
|
||||
*/
|
||||
@Test
|
||||
void login_allowsGraduateWhoseUnifiedAuthStatusIsInactive() {
|
||||
doNothing().when(authClient).checkPassword(anyString(), anyString());
|
||||
when(authClient.getUserInfo(STUDENT_NO)).thenReturn(userInfo("学生", "0"));
|
||||
|
||||
GxmuStudent student = student(3, 7);
|
||||
when(studentMapper.selectOne(any())).thenReturn(student);
|
||||
User existing = new User();
|
||||
existing.setUserId(7);
|
||||
existing.setUsername("wx_graduate0001");
|
||||
existing.setStatus(0);
|
||||
when(userService.getById(7)).thenReturn(existing);
|
||||
when(userService.getByUsername("wx_graduate0001", TENANT_ID)).thenReturn(existing);
|
||||
|
||||
LoginResult result = ssoLoginService.login(STUDENT_NO, "pwd", TENANT_ID, request);
|
||||
assertNotNull(result.getAccess_token());
|
||||
assertEquals(7, result.getUser().getUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证平台放行了,但本系统把账号冻结了:仍然拒绝(status 才是本系统的闸门)。
|
||||
*/
|
||||
@Test
|
||||
void login_stillRejectsAccountFrozenLocally() {
|
||||
doNothing().when(authClient).checkPassword(anyString(), anyString());
|
||||
when(authClient.getUserInfo(STUDENT_NO)).thenReturn(userInfo("学生", "1"));
|
||||
|
||||
GxmuStudent student = student(4, 8);
|
||||
when(studentMapper.selectOne(any())).thenReturn(student);
|
||||
User frozen = new User();
|
||||
frozen.setUserId(8);
|
||||
frozen.setUsername("wx_frozen000001");
|
||||
frozen.setStatus(1);
|
||||
when(userService.getById(8)).thenReturn(frozen);
|
||||
|
||||
BusinessException error = assertThrows(BusinessException.class,
|
||||
() -> ssoLoginService.login(STUDENT_NO, "pwd", TENANT_ID, request));
|
||||
assertEquals("账号被冻结,请联系团委", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void login_rejectsBlankInput() {
|
||||
assertEquals("请输入学号", assertThrows(BusinessException.class,
|
||||
() -> ssoLoginService.login(" ", "pwd", TENANT_ID, request)).getMessage());
|
||||
assertEquals("请输入密码", assertThrows(BusinessException.class,
|
||||
() -> ssoLoginService.login(STUDENT_NO, "", TENANT_ID, request)).getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void available_requiresSwitchAndCredentials() {
|
||||
assertTrue(ssoLoginService.available());
|
||||
|
||||
when(properties.getAppId()).thenReturn("");
|
||||
assertFalse(ssoLoginService.available());
|
||||
|
||||
when(properties.getAppId()).thenReturn("gxmu-app");
|
||||
when(properties.isEnabled()).thenReturn(false);
|
||||
assertFalse(ssoLoginService.available());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private GxmuStudent student(Integer id, Integer userId) {
|
||||
GxmuStudent student = new GxmuStudent();
|
||||
student.setId(id);
|
||||
student.setXh(STUDENT_NO);
|
||||
student.setXm("张三");
|
||||
student.setUserId(userId);
|
||||
return student;
|
||||
}
|
||||
|
||||
private LyuapUser userInfo(String type, String status) {
|
||||
LyuapUser info = new LyuapUser();
|
||||
info.setYhbh(STUDENT_NO);
|
||||
info.setYhmc("张三");
|
||||
info.setYhlxmc(type);
|
||||
info.setZt(status);
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user