SysLoginService.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. package org.dromara.web.service;
  2. import cn.dev33.satoken.exception.NotLoginException;
  3. import cn.dev33.satoken.secure.BCrypt;
  4. import cn.dev33.satoken.stp.StpUtil;
  5. import cn.hutool.core.bean.BeanUtil;
  6. import cn.hutool.core.util.ObjectUtil;
  7. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  8. import jakarta.servlet.http.HttpServletRequest;
  9. import lombok.RequiredArgsConstructor;
  10. import lombok.extern.slf4j.Slf4j;
  11. import me.zhyd.oauth.model.AuthResponse;
  12. import me.zhyd.oauth.model.AuthUser;
  13. import org.dromara.common.core.constant.Constants;
  14. import org.dromara.common.core.constant.GlobalConstants;
  15. import org.dromara.common.core.constant.TenantConstants;
  16. import org.dromara.common.core.domain.R;
  17. import org.dromara.common.core.domain.dto.RoleDTO;
  18. import org.dromara.common.core.domain.model.LoginUser;
  19. import org.dromara.common.core.domain.model.XcxLoginUser;
  20. import org.dromara.common.core.enums.DeviceType;
  21. import org.dromara.common.core.enums.LoginType;
  22. import org.dromara.common.core.enums.TenantStatus;
  23. import org.dromara.common.core.enums.UserStatus;
  24. import org.dromara.common.core.exception.user.CaptchaException;
  25. import org.dromara.common.core.exception.user.CaptchaExpireException;
  26. import org.dromara.common.core.exception.user.UserException;
  27. import org.dromara.common.core.utils.*;
  28. import org.dromara.common.log.event.LogininforEvent;
  29. import org.dromara.common.redis.utils.RedisUtils;
  30. import org.dromara.common.satoken.utils.LoginHelper;
  31. import org.dromara.common.tenant.exception.TenantException;
  32. import org.dromara.common.tenant.helper.TenantHelper;
  33. import org.dromara.common.web.config.properties.CaptchaProperties;
  34. import org.dromara.system.domain.SysAuthUser;
  35. import org.dromara.system.domain.SysUser;
  36. import org.dromara.system.domain.vo.SysTenantVo;
  37. import org.dromara.system.domain.vo.SysUserVo;
  38. import org.dromara.system.mapper.SysUserMapper;
  39. import org.dromara.system.service.ISysPermissionService;
  40. import org.dromara.system.service.ISysTenantService;
  41. import org.springframework.beans.factory.annotation.Value;
  42. import org.springframework.stereotype.Service;
  43. import java.io.IOException;
  44. import java.time.Duration;
  45. import java.util.Date;
  46. import java.util.List;
  47. import java.util.function.Supplier;
  48. /**
  49. * 登录校验方法
  50. *
  51. * @author Lion Li
  52. */
  53. @RequiredArgsConstructor
  54. @Slf4j
  55. @Service
  56. public class SysLoginService {
  57. private final SysUserMapper userMapper;
  58. private final CaptchaProperties captchaProperties;
  59. private final ISysPermissionService permissionService;
  60. private final ISysTenantService tenantService;
  61. @Value("${user.password.maxRetryCount}")
  62. private Integer maxRetryCount;
  63. @Value("${user.password.lockTime}")
  64. private Integer lockTime;
  65. /**
  66. * 登录验证
  67. *
  68. * @param username 用户名
  69. * @param password 密码
  70. * @param code 验证码
  71. * @param uuid 唯一标识
  72. * @return 结果
  73. */
  74. public String login(String tenantId, String username, String password, String code, String uuid) {
  75. boolean captchaEnabled = captchaProperties.getEnable();
  76. // 验证码开关
  77. if (captchaEnabled) {
  78. validateCaptcha(tenantId, username, code, uuid);
  79. }
  80. // 校验租户
  81. checkTenant(tenantId);
  82. // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
  83. SysUserVo user = loadUserByUsername(tenantId, username);
  84. checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
  85. // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
  86. LoginUser loginUser = buildLoginUser(user);
  87. // 生成token
  88. LoginHelper.loginByDevice(loginUser, DeviceType.PC);
  89. recordLogininfor(loginUser.getTenantId(), username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
  90. recordLoginInfo(user.getUserId());
  91. return StpUtil.getTokenValue();
  92. }
  93. public String smsLogin(String tenantId, String phonenumber, String smsCode) {
  94. // 校验租户
  95. checkTenant(tenantId);
  96. // 通过手机号查找用户
  97. SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
  98. checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
  99. // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
  100. LoginUser loginUser = buildLoginUser(user);
  101. // 生成token
  102. LoginHelper.loginByDevice(loginUser, DeviceType.APP);
  103. recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
  104. recordLoginInfo(user.getUserId());
  105. return StpUtil.getTokenValue();
  106. }
  107. public String emailLogin(String tenantId, String email, String emailCode) {
  108. // 校验租户
  109. checkTenant(tenantId);
  110. // 通过邮箱查找用户
  111. SysUserVo user = loadUserByEmail(tenantId, email);
  112. checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
  113. // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
  114. LoginUser loginUser = buildLoginUser(user);
  115. // 生成token
  116. LoginHelper.loginByDevice(loginUser, DeviceType.APP);
  117. recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
  118. recordLoginInfo(user.getUserId());
  119. return StpUtil.getTokenValue();
  120. }
  121. public String xcxLogin(String xcxCode) {
  122. // xcxCode 为 小程序调用 wx.login 授权后获取
  123. // todo 以下自行实现
  124. // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
  125. String openid = "";
  126. // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
  127. SysUserVo user = loadUserByOpenid(openid);
  128. // 校验租户
  129. checkTenant(user.getTenantId());
  130. // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
  131. XcxLoginUser loginUser = new XcxLoginUser();
  132. loginUser.setTenantId(user.getTenantId());
  133. loginUser.setUserId(user.getUserId());
  134. loginUser.setUsername(user.getUserName());
  135. loginUser.setUserType(user.getUserType());
  136. loginUser.setOpenid(openid);
  137. // 生成token
  138. LoginHelper.loginByDevice(loginUser, DeviceType.XCX);
  139. recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
  140. recordLoginInfo(user.getUserId());
  141. return StpUtil.getTokenValue();
  142. }
  143. /**
  144. * 认证授权登录
  145. * @param source
  146. * @throws IOException
  147. */
  148. /**
  149. * 社交登录
  150. * @param source 登录来源
  151. * @param authUser 授权响应实体
  152. * @param request Http请求对象
  153. * @return 统一响应实体
  154. * @throws IOException
  155. */
  156. public R<String> socialLogin(String source, AuthResponse<AuthUser> authUser, HttpServletRequest request) throws IOException {
  157. // 判断授权响应是否成功
  158. if (!authUser.ok()) {
  159. return R.fail("对不起,授权信息验证不通过,请联系管理员");
  160. }
  161. AuthUser authUserData = authUser.getData();
  162. // 判断数据库中是否已存在该用户
  163. SysUserVo user = userMapper.selectAuthUserByUuid(source + authUserData.getUuid());
  164. if (ObjectUtil.isNotNull(user)) {
  165. checkTenant(user.getTenantId());
  166. SysUserVo dbUser = loadUserByUsername(user.getTenantId(), user.getUserName());
  167. // 登录
  168. LoginHelper.loginByDevice(buildLoginUser(dbUser), DeviceType.auth);
  169. recordLogininfor(dbUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
  170. recordLoginInfo(user.getUserId());
  171. return R.ok(StpUtil.getTokenValue());
  172. } else {
  173. if (LoginHelper.getUserId() == null) {
  174. return R.fail("授权失败,请先登录再绑定");
  175. }
  176. // 组装授权用户信息
  177. SysAuthUser sysAuthUser = new SysAuthUser();
  178. sysAuthUser.setAvatar(authUserData.getAvatar());
  179. sysAuthUser.setUuid(source + authUserData.getUuid());
  180. sysAuthUser.setUserId(LoginHelper.getUserId());
  181. sysAuthUser.setUserName(authUserData.getUsername());
  182. sysAuthUser.setNickName(authUserData.getNickname());
  183. sysAuthUser.setEmail(authUserData.getEmail());
  184. sysAuthUser.setSource(source);
  185. sysAuthUser.setCreateTime(new Date().toString());
  186. // 新用户,绑定第三方账号
  187. userMapper.insertAuthUser(sysAuthUser);
  188. SysUserVo lodingData = loadUserByUsername(LoginHelper.getTenantId(), LoginHelper.getUsername());
  189. checkTenant(lodingData.getTenantId());
  190. LoginHelper.loginByDevice(buildLoginUser(lodingData), DeviceType.auth);
  191. recordLogininfor(lodingData.getTenantId(), sysAuthUser.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
  192. recordLoginInfo(sysAuthUser.getUserId());
  193. return R.ok(StpUtil.getTokenValue());
  194. }
  195. }
  196. /**
  197. * 退出登录
  198. */
  199. public void logout() {
  200. try {
  201. LoginUser loginUser = LoginHelper.getLoginUser();
  202. if (TenantHelper.isEnable() && LoginHelper.isSuperAdmin()) {
  203. // 超级管理员 登出清除动态租户
  204. TenantHelper.clearDynamic();
  205. }
  206. StpUtil.logout();
  207. recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
  208. } catch (NotLoginException ignored) {
  209. }
  210. }
  211. /**
  212. * 记录登录信息
  213. *
  214. * @param tenantId 租户ID
  215. * @param username 用户名
  216. * @param status 状态
  217. * @param message 消息内容
  218. */
  219. private void recordLogininfor(String tenantId, String username, String status, String message) {
  220. LogininforEvent logininforEvent = new LogininforEvent();
  221. logininforEvent.setTenantId(tenantId);
  222. logininforEvent.setUsername(username);
  223. logininforEvent.setStatus(status);
  224. logininforEvent.setMessage(message);
  225. logininforEvent.setRequest(ServletUtils.getRequest());
  226. SpringUtils.context().publishEvent(logininforEvent);
  227. }
  228. /**
  229. * 校验短信验证码
  230. */
  231. private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
  232. String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
  233. if (StringUtils.isBlank(code)) {
  234. recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
  235. throw new CaptchaExpireException();
  236. }
  237. return code.equals(smsCode);
  238. }
  239. /**
  240. * 校验邮箱验证码
  241. */
  242. private boolean validateEmailCode(String tenantId, String email, String emailCode) {
  243. String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
  244. if (StringUtils.isBlank(code)) {
  245. recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
  246. throw new CaptchaExpireException();
  247. }
  248. return code.equals(emailCode);
  249. }
  250. /**
  251. * 校验验证码
  252. *
  253. * @param username 用户名
  254. * @param code 验证码
  255. * @param uuid 唯一标识
  256. */
  257. public void validateCaptcha(String tenantId, String username, String code, String uuid) {
  258. String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
  259. String captcha = RedisUtils.getCacheObject(verifyKey);
  260. RedisUtils.deleteObject(verifyKey);
  261. if (captcha == null) {
  262. recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
  263. throw new CaptchaExpireException();
  264. }
  265. if (!code.equalsIgnoreCase(captcha)) {
  266. recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
  267. throw new CaptchaException();
  268. }
  269. }
  270. private SysUserVo loadUserByUsername(String tenantId, String username) {
  271. SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
  272. .select(SysUser::getUserName, SysUser::getStatus)
  273. .eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
  274. .eq(SysUser::getUserName, username));
  275. if (ObjectUtil.isNull(user)) {
  276. log.info("登录用户:{} 不存在.", username);
  277. throw new UserException("user.not.exists", username);
  278. } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
  279. log.info("登录用户:{} 已被停用.", username);
  280. throw new UserException("user.blocked", username);
  281. }
  282. if (TenantHelper.isEnable()) {
  283. return userMapper.selectTenantUserByUserName(username, tenantId);
  284. }
  285. return userMapper.selectUserByUserName(username);
  286. }
  287. private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
  288. SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
  289. .select(SysUser::getPhonenumber, SysUser::getStatus)
  290. .eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
  291. .eq(SysUser::getPhonenumber, phonenumber));
  292. if (ObjectUtil.isNull(user)) {
  293. log.info("登录用户:{} 不存在.", phonenumber);
  294. throw new UserException("user.not.exists", phonenumber);
  295. } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
  296. log.info("登录用户:{} 已被停用.", phonenumber);
  297. throw new UserException("user.blocked", phonenumber);
  298. }
  299. if (TenantHelper.isEnable()) {
  300. return userMapper.selectTenantUserByPhonenumber(phonenumber, tenantId);
  301. }
  302. return userMapper.selectUserByPhonenumber(phonenumber);
  303. }
  304. private SysUserVo loadUserByEmail(String tenantId, String email) {
  305. SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
  306. .select(SysUser::getPhonenumber, SysUser::getStatus)
  307. .eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
  308. .eq(SysUser::getEmail, email));
  309. if (ObjectUtil.isNull(user)) {
  310. log.info("登录用户:{} 不存在.", email);
  311. throw new UserException("user.not.exists", email);
  312. } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
  313. log.info("登录用户:{} 已被停用.", email);
  314. throw new UserException("user.blocked", email);
  315. }
  316. if (TenantHelper.isEnable()) {
  317. return userMapper.selectTenantUserByEmail(email, tenantId);
  318. }
  319. return userMapper.selectUserByEmail(email);
  320. }
  321. private SysUserVo loadUserByOpenid(String openid) {
  322. // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
  323. // todo 自行实现 userService.selectUserByOpenid(openid);
  324. SysUserVo user = new SysUserVo();
  325. if (ObjectUtil.isNull(user)) {
  326. log.info("登录用户:{} 不存在.", openid);
  327. // todo 用户不存在 业务逻辑自行实现
  328. } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
  329. log.info("登录用户:{} 已被停用.", openid);
  330. // todo 用户已被停用 业务逻辑自行实现
  331. }
  332. return user;
  333. }
  334. /**
  335. * 构建登录用户
  336. */
  337. private LoginUser buildLoginUser(SysUserVo user) {
  338. LoginUser loginUser = new LoginUser();
  339. loginUser.setTenantId(user.getTenantId());
  340. loginUser.setUserId(user.getUserId());
  341. loginUser.setDeptId(user.getDeptId());
  342. loginUser.setUsername(user.getUserName());
  343. loginUser.setUserType(user.getUserType());
  344. loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
  345. loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
  346. loginUser.setDeptName(ObjectUtil.isNull(user.getDept()) ? "" : user.getDept().getDeptName());
  347. List<RoleDTO> roles = BeanUtil.copyToList(user.getRoles(), RoleDTO.class);
  348. loginUser.setRoles(roles);
  349. return loginUser;
  350. }
  351. /**
  352. * 记录登录信息
  353. *
  354. * @param userId 用户ID
  355. */
  356. public void recordLoginInfo(Long userId) {
  357. SysUser sysUser = new SysUser();
  358. sysUser.setUserId(userId);
  359. sysUser.setLoginIp(ServletUtils.getClientIP());
  360. sysUser.setLoginDate(DateUtils.getNowDate());
  361. sysUser.setUpdateBy(userId);
  362. userMapper.updateById(sysUser);
  363. }
  364. /**
  365. * 登录校验
  366. */
  367. private void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
  368. String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
  369. String loginFail = Constants.LOGIN_FAIL;
  370. // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
  371. Integer errorNumber = RedisUtils.getCacheObject(errorKey);
  372. // 锁定时间内登录 则踢出
  373. if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
  374. recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
  375. throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
  376. }
  377. if (supplier.get()) {
  378. // 是否第一次
  379. errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
  380. // 达到规定错误次数 则锁定登录
  381. if (errorNumber.equals(maxRetryCount)) {
  382. RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
  383. recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
  384. throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
  385. } else {
  386. // 未达到规定错误次数 则递增
  387. RedisUtils.setCacheObject(errorKey, errorNumber);
  388. recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
  389. throw new UserException(loginType.getRetryLimitCount(), errorNumber);
  390. }
  391. }
  392. // 登录成功 清空错误次数
  393. RedisUtils.deleteObject(errorKey);
  394. }
  395. private void checkTenant(String tenantId) {
  396. if (!TenantHelper.isEnable()) {
  397. return;
  398. }
  399. if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
  400. return;
  401. }
  402. SysTenantVo tenant = tenantService.queryByTenantId(tenantId);
  403. if (ObjectUtil.isNull(tenant)) {
  404. log.info("登录租户:{} 不存在.", tenantId);
  405. throw new TenantException("tenant.not.exists");
  406. } else if (TenantStatus.DISABLE.getCode().equals(tenant.getStatus())) {
  407. log.info("登录租户:{} 已被停用.", tenantId);
  408. throw new TenantException("tenant.blocked");
  409. } else if (ObjectUtil.isNotNull(tenant.getExpireTime())
  410. && new Date().after(tenant.getExpireTime())) {
  411. log.info("登录租户:{} 已超过有效期.", tenantId);
  412. throw new TenantException("tenant.expired");
  413. }
  414. }
  415. }