fix: 增加一个统一登录接口,支持短信和密码登录,并且支持多web限制

This commit is contained in:
tangwei 2026-08-11 16:55:22 +08:00
parent bcf8279d3d
commit 42ea915d6c
2 changed files with 322 additions and 7 deletions

View File

@ -14,9 +14,12 @@ import com.yfd.platform.config.WebConfig;
import com.yfd.platform.config.bean.LoginCodeEnum;
import com.yfd.platform.config.bean.LoginProperties;
import com.yfd.platform.constant.Constant;
import com.yfd.platform.system.domain.LoginUser;
import com.yfd.platform.system.domain.SysLog;
import com.yfd.platform.system.domain.SysUser;
import com.yfd.platform.system.domain.*;
import com.yfd.platform.system.mapper.SysMenuMapper;
import com.yfd.platform.system.mapper.SysUserMapper;
import com.yfd.platform.system.mapper.SysUserTenantMapper;
import com.yfd.platform.system.service.ISmsVerifyCodeService;
import com.yfd.platform.system.service.ISysAcctPasswordPolicyService;
import com.yfd.platform.system.service.ISysLogService;
import com.yfd.platform.system.service.IUserService;
import com.yfd.platform.utils.RequestHolder;
@ -35,6 +38,7 @@ import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
@ -66,10 +70,27 @@ public class LoginController {
@Resource
private LoginProperties loginProperties;
@Resource
private ISysAcctPasswordPolicyService policyService;
@Resource
private SysUserTenantMapper sysUserTenantMapper;
@Resource
private SysMenuMapper sysMenuMapper;
@Resource
private SysUserMapper sysUserMapper;
@Resource
private ISmsVerifyCodeService smsVerifyCodeService;
@PostMapping("/login")
@Operation(summary = "登录用户")
@ResponseBody
public ResponseResult login(SysUser user, @RequestHeader(value = "Tenant_id", required = false) String tenantId) throws Exception {
public ResponseResult login(SysUser user,
@RequestHeader(value = "Tenant_id", required = false) String tenantId,
@RequestParam(value = "clientType", required = false, defaultValue = "web") String clientType) throws Exception {
// 密码解密
String password = RsaUtils.decryptByPrivateKey(privateKey,
user.getPassword());
@ -105,15 +126,15 @@ public class LoginController {
if (!loginUser.isSuperAdmin() && StrUtil.isNotBlank(tenantId)) {
List<String> accessibleIds = loginUser.getAccessibleTenantIds();
if (accessibleIds == null || !accessibleIds.contains(tenantId)) {
return ResponseResult.error("账号不存在或密码错误");
return ResponseResult.error("用户无权限访问该平台");
}
}
if (StrUtil.isNotBlank(regStatus)&&"REJECTED".equals(regStatus)) {
if (StrUtil.isNotBlank(regStatus) && "REJECTED".equals(regStatus)) {
return ResponseResult.error("账号审核未通过");
}
if (StrUtil.isNotBlank(regStatus)&&"PENDING".equals(regStatus)) {
if (StrUtil.isNotBlank(regStatus) && "PENDING".equals(regStatus)) {
return ResponseResult.error("账号待审核,请联系管理员");
}
@ -121,6 +142,24 @@ public class LoginController {
return ResponseResult.error("账号已停用");
}
// 多端登录控制检查密码策略是否允许当前客户端类型多端登录
// SysAcctPasswordPolicy policy = policyService.getCurrentPolicy();
// if (policy != null) {
// boolean multiLoginEnabled;
// if ("app".equals(clientType)) {
// multiLoginEnabled = policy.getEnableAppMultipleLogin() != null && policy.getEnableAppMultipleLogin() == 1;
// } else {
// multiLoginEnabled = policy.getEnableWebMultipleLogin() != null && policy.getEnableWebMultipleLogin() == 1;
// }
// if (!multiLoginEnabled) {
// // 多端登录未开启检查该用户是否已有同类型的活跃会话
// String existingClientType =
// webConfig.loginuserCache().get("login:clientType:" + loginUser.getUsername());
// if (StrUtil.isNotBlank(existingClientType) && existingClientType.equals(clientType)) {
// return ResponseResult.error("该账号已在其他设备登录,不允许同时登录");
// }
// }
// }
HttpServletRequest request = RequestHolder.getHttpServletRequest();
@ -162,6 +201,8 @@ public class LoginController {
String jsonStr = JSONUtil.toJsonStr(loginUser);
webConfig.loginuserCache().put("login:" + userId, jsonStr);
webConfig.loginuserCache().put("expire_time:" + userId, map.get("expire_time").toString());
// 记录当前会话的客户端类型用于多端登录控制
// webConfig.loginuserCache().put("login:clientType:" + loginUser.getUsername(), clientType);
return ResponseResult.successData(map);
}
@ -201,6 +242,8 @@ public class LoginController {
String userId = loginuser.getUser().getId();
//删除redis中的登陆用户信息
webConfig.loginuserCache().remove("login:" + userId);
// 清除客户端类型记录
// webConfig.loginuserCache().remove("login:clientType:" + loginuser.getUsername());
//记录退出日志
HttpServletRequest request = RequestHolder.getHttpServletRequest();
SysLog sysLog = new SysLog();
@ -266,4 +309,220 @@ public class LoginController {
}
// ==================== 统一登录 ====================
/**
* 统一登录入口支持两种方式
* <ul>
* <li><b>密码登录</b>loginType=password用户名 + 密码 + 图形验证码</li>
* <li><b>短信登录</b>loginType=sms手机号 + 短信验证码</li>
* </ul>
* <p>均受密码策略中的多端登录控制enableWebMultipleLogin / enableAppMultipleLogin</p>
*/
@PostMapping("/unifiedLogin")
@Operation(summary = "统一登录(支持密码/短信验证码)")
@ResponseBody
public ResponseResult unifiedLogin(@RequestHeader(value = "Tenant_id", required = false) String tenantId,
@RequestBody UnifiedLoginRequest req) throws Exception {
String clientType = StrUtil.isBlank(req.getClientType()) ? "web" : req.getClientType();
LoginUser loginUser;
String loginDesc;
// ========== 1. 密码登录 ==========
if ("password".equals(req.getLoginType())) {
if (StrUtil.isBlank(req.getUsername()) || StrUtil.isBlank(req.getPassword())) {
return ResponseResult.error("用户名和密码不能为空");
}
// 1.1 图形验证码校验
if (StrUtil.isBlank(req.getCaptchaUuid()) || StrUtil.isBlank(req.getCaptchaCode())) {
return ResponseResult.error("验证码不能为空");
}
String cachedCode = webConfig.loginuserCache().get(req.getCaptchaUuid());
webConfig.loginuserCache().remove(req.getCaptchaUuid());
if (StrUtil.isBlank(cachedCode)) {
return ResponseResult.error("验证码不存在或已过期");
}
if (!req.getCaptchaCode().equalsIgnoreCase(cachedCode)) {
return ResponseResult.error("验证码错误");
}
// 1.2 密码解密 + Spring Security 认证
String password = RsaUtils.decryptByPrivateKey(privateKey, req.getPassword());
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(req.getUsername(), password);
Authentication authenticate = authenticationManager.authenticate(authToken);
if (ObjectUtil.isNull(authenticate)) {
return ResponseResult.unlogin();
}
loginUser = (LoginUser) authenticate.getPrincipal();
loginDesc = loginUser.getUser().getNickname() + "登录系统!";
} else if ("sms".equals(req.getLoginType())) {
// ========== 2. 短信验证码登录 ==========
if (StrUtil.isBlank(req.getPhone()) || StrUtil.isBlank(req.getSmsCode())) {
return ResponseResult.error("手机号和验证码不能为空");
}
// 2.1 验证短信验证码
boolean verified = smsVerifyCodeService.verifyCode(req.getPhone(), req.getSmsCode(), SmsVerifyCode.TYPE_LOGIN, tenantId);
if (!verified) {
return ResponseResult.error("验证码错误或已过期");
}
// 2.2 通过手机号查找用户
SysUser user = userService.getUserByPhone(req.getPhone(), null);
if (user == null) {
return ResponseResult.error("该手机号未注册");
}
// 2.3 状态校验
String statusError = checkUserStatus(user);
if (statusError != null) {
return ResponseResult.error(statusError);
}
// 2.4 判断超级管理员
boolean isSuperAdmin = (checkIsSuperAdmin(user.getId()) || user.getUsertype() == 0);
// 2.5 多门户权限校验
List<String> accessibleTenantIds = sysUserTenantMapper.getTenantIdsByUserId(user.getId());
if (!isSuperAdmin) {
if (StrUtil.isBlank(tenantId)) {
return ResponseResult.error("用户无权限访问该平台");
}
if (accessibleTenantIds == null || !accessibleTenantIds.contains(tenantId)) {
return ResponseResult.error("用户无权限访问该平台");
}
}
// 2.6 加载权限
List<String> permissions;
if (isSuperAdmin) {
permissions = sysMenuMapper.selectPermsByUserId(user.getId(), null);
} else {
permissions = sysMenuMapper.selectPermsByUserId(user.getId(), tenantId);
}
loginUser = new LoginUser(user, permissions, tenantId, accessibleTenantIds, isSuperAdmin);
loginDesc = user.getNickname() + "使用短信验证码登录系统!";
// 短信登录手动设置 SecurityContext
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()));
} else {
return ResponseResult.error("登录类型错误,仅支持 password / sms");
}
// ========== 3. 密码登录的额外状态校验 ==========
if ("password".equals(req.getLoginType())) {
String statusError = checkUserStatus(loginUser.getUser());
if (statusError != null) {
return ResponseResult.error(statusError);
}
// 多门户校验
if (!loginUser.isSuperAdmin() && StrUtil.isNotBlank(tenantId)) {
List<String> accessibleIds = loginUser.getAccessibleTenantIds();
if (accessibleIds == null || !accessibleIds.contains(tenantId)) {
return ResponseResult.error("用户无权限访问该平台");
}
}
}
// ========== 4. 多端登录控制 ==========
String multiLoginError = checkMultiLogin(loginUser.getUsername(), clientType);
if (multiLoginError != null) {
return ResponseResult.error(multiLoginError);
}
// ========== 5. 生成 Token + 缓存会话 ==========
return buildLoginResult(loginUser, tenantId, clientType, loginDesc);
}
/**
* 校验用户状态返回错误信息null 表示通过
*/
private String checkUserStatus(SysUser user) {
if (user.getStatus() != null && user.getStatus() == 0) {
return "账号已停用";
}
if (user.getRegStatus() != null && "PENDING".equals(user.getRegStatus())) {
return "账号待审核,请联系管理员";
}
if (user.getRegStatus() != null && "REJECTED".equals(user.getRegStatus())) {
return "账号审核未通过";
}
return null;
}
/**
* 多端登录控制检查
*/
private String checkMultiLogin(String username, String clientType) {
SysAcctPasswordPolicy policy = policyService.getCurrentPolicy();
if (policy == null) {
return null;
}
boolean multiLoginEnabled = "app".equals(clientType)
? (policy.getEnableAppMultipleLogin() != null && policy.getEnableAppMultipleLogin() == 1)
: (policy.getEnableWebMultipleLogin() != null && policy.getEnableWebMultipleLogin() == 1);
if (!multiLoginEnabled) {
String existingClientType = webConfig.loginuserCache().get("login:clientType:" + username);
if (StrUtil.isNotBlank(existingClientType) && existingClientType.equals(clientType)) {
return "该账号已在其他设备登录,不允许同时登录";
}
}
return null;
}
/**
* 判断是否为超级管理员
*/
private boolean checkIsSuperAdmin(String userId) {
String maxLevel = sysUserMapper.getMaxLevel(userId);
return "1".equals(maxLevel);
}
/**
* 生成 JWT Token记录日志缓存会话信息
*/
private ResponseResult buildLoginResult(LoginUser loginUser, String tenantId,
String clientType, String loginDesc) {
// 记录登录日志
HttpServletRequest request = RequestHolder.getHttpServletRequest();
SysLog sysLog = new SysLog();
sysLog.setUsercode(loginUser.getUsername());
sysLog.setUsername(loginUser.getUser().getNickname());
sysLog.setRequestip(StringUtils.getIp(request));
sysLog.setBrowser(StringUtils.getBrowser(request));
sysLog.setOpttype("登录(login)");
sysLog.setModule("统一登录");
sysLog.setMethod(this.getClass().getName() + ".unifiedLogin()");
sysLog.setDescription(loginDesc);
sysLog.setLogtime(new Timestamp(System.currentTimeMillis()));
sysLogService.save(sysLog);
// 生成 JWT Token
String userId = loginUser.getUser().getId();
Map<String, Object> map = new HashMap<>(10) {{
put("userid", userId);
put("username", loginUser.getUsername());
long expireTime = System.currentTimeMillis() + (30L * 24L * 60L * 60L * 1000L);
put("expire_time", expireTime);
}};
String token = JWTUtil.createToken(map, "12345678".getBytes());
map.put("token", token);
map.put("accessibleTenantIds", loginUser.getAccessibleTenantIds());
map.put("superAdmin", loginUser.isSuperAdmin());
map.put("currentTenantId", tenantId);
// 缓存登录会话
String jsonStr = JSONUtil.toJsonStr(loginUser);
webConfig.loginuserCache().put("login:" + userId, jsonStr);
webConfig.loginuserCache().put("expire_time:" + userId, map.get("expire_time").toString());
webConfig.loginuserCache().put("login:clientType:" + loginUser.getUsername(), clientType);
return ResponseResult.successData(map);
}
}

View File

@ -0,0 +1,56 @@
package com.yfd.platform.system.domain;
import lombok.Data;
/**
* <p>
* 统一登录请求体支持密码登录和短信验证码登录
* </p>
*
* <pre>
* 密码登录示例
* {
* "loginType": "password",
* "username": "admin",
* "password": "rsa_encrypted_password",
* "captchaUuid": "code_xxx",
* "captchaCode": "abcd",
* "clientType": "web"
* }
*
* 短信登录示例
* {
* "loginType": "sms",
* "phone": "13800138000",
* "smsCode": "123456",
* "clientType": "app"
* }
* </pre>
*/
@Data
public class UnifiedLoginRequest {
/** 登录类型: "password"(用户名密码) / "sms"(短信验证码) */
private String loginType;
/** 用户名(密码登录必填) */
private String username;
/** 密码密码登录必填RSA公钥加密 */
private String password;
/** 手机号(短信登录必填) */
private String phone;
/** 短信验证码(短信登录必填) */
private String smsCode;
/** 图形验证码UUID密码登录时用于从缓存中比对验证码 */
private String captchaUuid;
/** 图形验证码(密码登录时用户输入的验证码) */
private String captchaCode;
/** 客户端类型: "web"(浏览器)/ "app"(移动端),默认 "web" */
private String clientType;
}