登录实现 和 添加配说明文档

This commit is contained in:
zk
2026-03-11 17:44:29 +08:00
parent 7248248b89
commit 33f00d9f68
8 changed files with 345 additions and 10 deletions
@@ -0,0 +1,38 @@
package org.jiayunet.controller;
import lombok.AllArgsConstructor;
import org.jiayunet.pojo.dto.SmsLoginDto;
import org.jiayunet.pojo.vo.LoginVo;
import org.jiayunet.server.LoginServer;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.Pattern;
/**
* 登录控制类
*
* @author zk
*/
@RestController
@RequestMapping("/public")
@AllArgsConstructor
@Validated
public class LoginController {
private LoginServer loginServer;
@PostMapping("/sms/sendCode")
public boolean sendCode(@RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") String mobileNumber) {
return loginServer.sendCode(mobileNumber);
}
@PostMapping("/login/smsLogin")
public LoginVo smsLogin(@Validated @RequestBody SmsLoginDto dto,
HttpServletRequest request,
HttpServletResponse response) {
return loginServer.smsLogin(dto.getMobileNumber(), dto.getCode(), request, response);
}
}
@@ -0,0 +1,20 @@
package org.jiayunet.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 短信验证码登录入参
*
* @author zk
*/
@Data
public class SmsLoginDto {
@NotBlank(message = "手机号不能为空")
private String mobileNumber;
@NotBlank(message = "验证码不能为空")
private String code;
}
@@ -0,0 +1,20 @@
package org.jiayunet.pojo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 登录返回
*
* @author zk
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginVo {
private Long userId;
private String nick;
}
@@ -0,0 +1,164 @@
package org.jiayunet.server;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.constant.PreRedisKeyName;
import org.jiayunet.constant.SmsTemplateEnum;
import org.jiayunet.constant.sms.UniversalSmsVariable;
import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.mapper.UserMapper;
import org.jiayunet.pojo.login.RedisLoginTokenInfo;
import org.jiayunet.pojo.po.User;
import org.jiayunet.pojo.vo.LoginVo;
import org.jiayunet.tool.HttpIpTool;
import org.jiayunet.tool.server.RedisServerTool;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 登录服务
*
* @author zk
*/
@Service
@Slf4j
public class LoginServer {
@Value("${app.secret.token:youweiqingnian123}")
private String secret;
@Value("${app.login.token.exceed_time:43200}")
private int tokenExceedTime;
@Value("${app.login.device_online_quantity:5}")
private int deviceOnlineQuantity;
@Autowired
private SmsServer smsServer;
@Autowired
private UserMapper userMapper;
@Autowired
private RedisServerTool redisServerTool;
/**
* 发送短信验证码
*/
public boolean sendCode(String mobileNumber) {
Assert.hasText(mobileNumber, "手机号不能为空");
UniversalSmsVariable variable = new UniversalSmsVariable();
String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));
variable.setCode(code);
return smsServer.send(mobileNumber, SmsTemplateEnum.UNIVERSAL, variable);
}
/**
* 短信验证码登录
*/
@Transactional(rollbackFor = Exception.class)
public LoginVo smsLogin(String mobileNumber, String code, HttpServletRequest request, HttpServletResponse response) {
Assert.hasText(mobileNumber, "手机号不能为空");
Assert.hasText(code, "验证码不能为空");
// 校验验证码
boolean verified = smsServer.verify(mobileNumber, SmsTemplateEnum.UNIVERSAL, code);
if (!verified) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "验证码错误或已过期");
}
// 查询用户
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getMobileNumber, mobileNumber));
// 用户不存在则自动注册
if (user == null) {
user = new User();
user.setMobileNumber(mobileNumber);
user.setNick("用户" + mobileNumber.substring(mobileNumber.length() - 4));
user.setStatus(0);
user.setIsDelete(0L);
user.setCreateTime(Instant.now());
userMapper.insert(user);
}
// 检查用户状态
if (user.getStatus() != null && user.getStatus() == 1) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用");
}
// 生成JWT
String uuId = UUID.randomUUID().toString().replace("-", "");
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withClaim("userId", user.getId())
.withClaim("uuId", uuId)
.sign(algorithm);
// 构建Redis登录信息
String redisKey = PreRedisKeyName.LOGIN_TOKEN + user.getId();
RedisLoginTokenInfo info = redisServerTool.get(redisKey, RedisLoginTokenInfo.class);
if (info == null) {
info = new RedisLoginTokenInfo();
info.setUserId(user.getId());
info.setAuthority(new ArrayList<>());
info.setRole(new ArrayList<>());
}
// 过滤过期设备
List<RedisLoginTokenInfo.LoginDevice> devices = info.getLoginDevices();
if (devices == null) {
devices = new ArrayList<>();
}
long expireMillis = System.currentTimeMillis() - tokenExceedTime * 1000L;
devices = devices.stream()
.filter(d -> d.getLastLoginTime().isAfter(Instant.ofEpochMilli(expireMillis)))
.collect(Collectors.toList());
// 超过设备上限,移除最早的
while (devices.size() >= deviceOnlineQuantity) {
devices.stream().min(Comparator.comparing(RedisLoginTokenInfo.LoginDevice::getLastLoginTime))
.ifPresent(devices::remove);
}
// 添加当前设备
RedisLoginTokenInfo.LoginDevice device = new RedisLoginTokenInfo.LoginDevice();
device.setUuId(uuId);
device.setLastLoginTime(Instant.now());
device.setLoginIp(HttpIpTool.gteRealIP(request));
devices.add(device);
info.setLoginDevices(devices);
redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS);
// 清除验证码
smsServer.clear(mobileNumber, SmsTemplateEnum.UNIVERSAL);
// 设置Cookie
Cookie cookie = new Cookie("Token", token);
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(tokenExceedTime);
response.addCookie(cookie);
return new LoginVo(user.getId(), user.getNick());
}
}
@@ -17,8 +17,8 @@ spring:
# 电子邮箱
email:
status: close
account: ${EMAIL_ACCOUNT:sim18502043706@163.com}
authorization: ${EMAIL_AUTHORIZATION:CHBCVPYGFZCSUNCP}
account: ${EMAIL_ACCOUNT:xxx@163.com}
authorization: ${EMAIL_AUTHORIZATION:123456}
# 微信支付
wx_pay:
@@ -44,20 +44,20 @@ app:
token:
exceed_time: 129600
#设备在线数量
device_online_quantity: 10
device_online_quantity: 2
# 短信
sms:
service_provider: aliyun
aliyun:
access_key_id: LTAI5tGRFuJmt6nrxRqZYC7z
access_key_secret: xLC3kNSmCvEtsc9j4O8g3rOs70QjZQ
access_key_id: LTAI5tJBVUUJhB7yp14UDzVf
access_key_secret: Opf0iO5FKNrdwI63DPhXazW7utAGTj
oss:
service_provider: aliyun
aliyun:
access_key_id: LTAI5tGRFuJmt6nrxRqZYC7z
access_key_secret: xLC3kNSmCvEtsc9j4O8g3rOs70QjZQ
access_key_id: LTAI5tEdLKKQUKhTyUpfH5Mk
access_key_secret: RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
# 防刷配置 20秒 20次
prevent_replay: