@Data
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "成功", data);
}
}
@Data
@AllArgsConstructor
public class WechatQrcodeVO {
private String sceneId;
private String qrcodeUrl;
private Integer expireTime;
}
@Data
@AllArgsConstructor
public class ScanStatusVO {
private Integer status;
private String token;
private Object userInfo;
}
@Data
public class SendCodeRequest {
private String account;
private String type;
}
@Data
public class VerifyCodeRequest {
private String account;
private String code;
private String bizType;
private String sceneId;
}
@Service
@Slf4j
public class AliyunSmsService {
@Value("${aliyun.sms.access-key-id}")
private String accessKeyId;
@Value("${aliyun.sms.access-key-secret}")
private String accessKeySecret;
@Value("${aliyun.sms.region-id}")
private String regionId;
@Value("${aliyun.sms.sign-name}")
private String signName;
@Value("${aliyun.sms.template-code}")
private String templateCode;
public void sendSmsCode(String phone, String code) {
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phone);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\"" + code + "\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
if (!"OK".equals(response.getCode())) {
throw new RuntimeException("短信发送失败:" + response.getMessage());
}
} catch (ClientException e) {
log.error("调用阿里云短信 API 异常", e);
throw new RuntimeException("服务暂时不可用");
} finally {
client.shutdown();
}
}
}
@Service
@Slf4j
public class EmailService {
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String fromEmail;
public void sendEmailCode(String to, String code) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromEmail);
message.setTo(to);
message.setSubject("【YourApp】邮箱验证码");
message.setText("您的验证码是:" + code + ",5 分钟内有效。如非本人操作,请忽略此邮件。");
try {
mailSender.send(message);
} catch (Exception e) {
log.error("发送邮件失败", e);
throw new RuntimeException("邮件发送失败");
}
}
}
@RestController
@RequestMapping("/admin/auth")
@Slf4j
public class UnifiedLoginController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private UserService userService;
@Autowired
private JwtService jwtService;
@Autowired
private AliyunSmsService aliyunSmsService;
@Autowired
private EmailService emailService;
@Value("${wechat.open.app-id}")
private String appId;
@Value("${wechat.open.app-secret}")
private String appSecret;
@Value("${wechat.open.redirect-uri}")
private String redirectUri;
@PostMapping("/wechat/qrcode/generate")
public Result<WechatQrcodeVO> generateQrcode() {
String sceneId = "LOGIN_" + System.currentTimeMillis() + "_" + RandomUtil.randomString(16);
String qrcodeUrl = "https://open.weixin.qq.com/connect/qrconnect?"
+ "appid=" + appId + "&redirect_uri=" + UrlUtil.encode(redirectUri)
+ "&response_type=code&scope=snsapi_login"
+ "&state=" + sceneId + "#wechat_redirect";
String redisKey = "login:scene:" + sceneId;
redisTemplate.opsForHash().put(redisKey, "status", "0");
redisTemplate.expire(redisKey, 180, TimeUnit.SECONDS);
return Result.success(new WechatQrcodeVO(sceneId, qrcodeUrl, 180));
}
@GetMapping("/qrcode/status")
public Result<ScanStatusVO> checkStatus(@RequestParam String sceneId) {
String redisKey = "login:scene:" + sceneId;
String status = (String) redisTemplate.opsForHash().get(redisKey, "status");
if (status == null) {
return Result.success(new ScanStatusVO(4, null, null));
}
if ("3".equals(status)) {
String token = (String) redisTemplate.opsForHash().get(redisKey, "token");
Long userId = Long.valueOf((String) redisTemplate.opsForHash().get(redisKey, "userId"));
redisTemplate.delete(redisKey);
return Result.success(new ScanStatusVO(3, token, userService.getUserInfo(userId)));
}
return Result.success(new ScanStatusVO(Integer.parseInt(status), null, null));
}
@GetMapping("/wechat/callback")
public String callback(@RequestParam String code, @RequestParam String state) {
String sceneId = state;
String redisKey = "login:scene:" + sceneId;
if (!redisTemplate.hasKey(redisKey)) {
return "<script>alert('二维码已过期');window.close();</script>";
}
try {
String accessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?"
+ "appid=" + appId + "&secret=" + appSecret + "&code=" + code + "&grant_type=authorization_code";
String resp = HttpUtil.get(accessTokenUrl);
JSONObject json = JSONUtil.parseObj(resp);
String openId = json.getStr("openid");
User user = userService.findByWechatOpenId(openId);
if (user != null) {
handleLoginSuccess(redisKey, user);
return "<script>alert('扫码成功,请在电脑端查看');window.close();</script>";
} else {
redisTemplate.opsForHash().put(redisKey, "status", "5");
redisTemplate.opsForHash().put(redisKey, "openId", openId);
redisTemplate.expire(redisKey, 180, TimeUnit.SECONDS);
return "<script>alert('请在电脑端绑定手机号或邮箱');window.close();</script>";
}
} catch (Exception e) {
log.error("微信回调处理失败", e);
return "<script>alert('系统错误');</script>";
}
}
@PostMapping("/code/send")
public Result<?> sendCode(@RequestBody SendCodeRequest request) {
String account = request.getAccount();
String type = request.getType();
String rateLimitKey = "code:rate_limit:" + account;
if (Boolean.TRUE.equals(redisTemplate.hasKey(rateLimitKey))) {
throw new RuntimeException("操作过于频繁,请 60 秒后再试");
}
String code = RandomUtil.randomNumbers(6);
String codeKey = "code:verify:" + account;
if ("PHONE".equals(type)) {
if (!Validator.isMobile(account)) throw new IllegalArgumentException("手机号格式错误");
aliyunSmsService.sendSmsCode(account, code);
} else if ("EMAIL".equals(type)) {
if (!Validator.isEmail(account)) throw new IllegalArgumentException("邮箱格式错误");
emailService.sendEmailCode(account, code);
} else {
throw new IllegalArgumentException("不支持的账号类型");
}
redisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES);
redisTemplate.opsForValue().set(rateLimitKey, "1", 60, TimeUnit.SECONDS);
return Result.success("验证码发送成功");
}
@PostMapping("/code/verify")
public Result<?> verifyCode(@RequestBody VerifyCodeRequest request) {
String account = request.getAccount();
String inputCode = request.getCode();
String bizType = request.getBizType();
String codeKey = "code:verify:" + account;
String correctCode = redisTemplate.opsForValue().get(codeKey);
if (correctCode == null || !correctCode.equals(inputCode)) {
throw new RuntimeException("验证码错误或已过期");
}
redisTemplate.delete(codeKey);
if ("WECHAT_BIND".equals(bizType)) {
return handleWechatBind(account, request.getSceneId());
} else if ("PHONE_LOGIN".equals(bizType) || "EMAIL_LOGIN".equals(bizType)) {
return handleAccountLogin(account);
} else {
throw new IllegalArgumentException("不支持的业务类型");
}
}
private void handleLoginSuccess(String redisKey, User user) {
String token = jwtService.generateToken(user);
redisTemplate.opsForHash().put(redisKey, "status", "3");
redisTemplate.opsForHash().put(redisKey, "token", token);
redisTemplate.opsForHash().put(redisKey, "userId", String.valueOf(user.getId()));
redisTemplate.expire(redisKey, 180, TimeUnit.SECONDS);
}
private Result<?> handleWechatBind(String account, String sceneId) {
String redisKey = "login:scene:" + sceneId;
String openId = (String) redisTemplate.opsForHash().get(redisKey, "openId");
if (openId == null) throw new RuntimeException("无效的绑定会话");
User user = userService.bindWechatWithAccount(openId, account);
handleLoginSuccess(redisKey, user);
return Result.success("绑定成功");
}
private Result<ScanStatusVO> handleAccountLogin(String account) {
User user = userService.findByAccount(account);
if (user == null) {
user = userService.registerByAccount(account);
}
String token = jwtService.generateToken(user);
return Result.success(new ScanStatusVO(3, token, userService.getUserInfo(user.getId())));
}
}