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.LoginCodeEnum;
|
||||||
import com.yfd.platform.config.bean.LoginProperties;
|
import com.yfd.platform.config.bean.LoginProperties;
|
||||||
import com.yfd.platform.constant.Constant;
|
import com.yfd.platform.constant.Constant;
|
||||||
import com.yfd.platform.system.domain.LoginUser;
|
import com.yfd.platform.system.domain.*;
|
||||||
import com.yfd.platform.system.domain.SysLog;
|
import com.yfd.platform.system.mapper.SysMenuMapper;
|
||||||
import com.yfd.platform.system.domain.SysUser;
|
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.ISysLogService;
|
||||||
import com.yfd.platform.system.service.IUserService;
|
import com.yfd.platform.system.service.IUserService;
|
||||||
import com.yfd.platform.utils.RequestHolder;
|
import com.yfd.platform.utils.RequestHolder;
|
||||||
@ -35,6 +38,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -66,10 +70,27 @@ public class LoginController {
|
|||||||
@Resource
|
@Resource
|
||||||
private LoginProperties loginProperties;
|
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")
|
@PostMapping("/login")
|
||||||
@Operation(summary = "登录用户")
|
@Operation(summary = "登录用户")
|
||||||
@ResponseBody
|
@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,
|
String password = RsaUtils.decryptByPrivateKey(privateKey,
|
||||||
user.getPassword());
|
user.getPassword());
|
||||||
@ -105,7 +126,7 @@ public class LoginController {
|
|||||||
if (!loginUser.isSuperAdmin() && StrUtil.isNotBlank(tenantId)) {
|
if (!loginUser.isSuperAdmin() && StrUtil.isNotBlank(tenantId)) {
|
||||||
List<String> accessibleIds = loginUser.getAccessibleTenantIds();
|
List<String> accessibleIds = loginUser.getAccessibleTenantIds();
|
||||||
if (accessibleIds == null || !accessibleIds.contains(tenantId)) {
|
if (accessibleIds == null || !accessibleIds.contains(tenantId)) {
|
||||||
return ResponseResult.error("账号不存在或密码错误");
|
return ResponseResult.error("用户无权限访问该平台");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +142,24 @@ public class LoginController {
|
|||||||
return ResponseResult.error("账号已停用");
|
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();
|
HttpServletRequest request = RequestHolder.getHttpServletRequest();
|
||||||
@ -162,6 +201,8 @@ public class LoginController {
|
|||||||
String jsonStr = JSONUtil.toJsonStr(loginUser);
|
String jsonStr = JSONUtil.toJsonStr(loginUser);
|
||||||
webConfig.loginuserCache().put("login:" + userId, jsonStr);
|
webConfig.loginuserCache().put("login:" + userId, jsonStr);
|
||||||
webConfig.loginuserCache().put("expire_time:" + userId, map.get("expire_time").toString());
|
webConfig.loginuserCache().put("expire_time:" + userId, map.get("expire_time").toString());
|
||||||
|
// 记录当前会话的客户端类型,用于多端登录控制
|
||||||
|
// webConfig.loginuserCache().put("login:clientType:" + loginUser.getUsername(), clientType);
|
||||||
return ResponseResult.successData(map);
|
return ResponseResult.successData(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,6 +242,8 @@ public class LoginController {
|
|||||||
String userId = loginuser.getUser().getId();
|
String userId = loginuser.getUser().getId();
|
||||||
//删除redis中的登陆用户信息
|
//删除redis中的登陆用户信息
|
||||||
webConfig.loginuserCache().remove("login:" + userId);
|
webConfig.loginuserCache().remove("login:" + userId);
|
||||||
|
// 清除客户端类型记录
|
||||||
|
// webConfig.loginuserCache().remove("login:clientType:" + loginuser.getUsername());
|
||||||
//记录退出日志
|
//记录退出日志
|
||||||
HttpServletRequest request = RequestHolder.getHttpServletRequest();
|
HttpServletRequest request = RequestHolder.getHttpServletRequest();
|
||||||
SysLog sysLog = new SysLog();
|
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