fix: 增加一个统一登录接口,支持短信和密码登录,并且支持多web限制
This commit is contained in:
parent
bcf8279d3d
commit
42ea915d6c
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user