Merge branch 'master' of gitee.com:youlaiorg/youlai-boot
# Conflicts: # src/main/java/com/youlai/boot/config/SecurityConfig.java # src/main/java/com/youlai/boot/config/property/TencentSmsProperties.java # src/main/java/com/youlai/boot/support/sms/impl/AliyunSmsServiceImpl.java # src/main/java/com/youlai/boot/support/sms/impl/TencentSmsService.java # src/main/resources/application-dev.yml # src/main/resources/application-prod.yml
This commit is contained in:
@@ -3,7 +3,7 @@ package com.youlai.boot.app.controller;
|
||||
import com.youlai.boot.app.model.req.MobileLoginReq;
|
||||
import com.youlai.boot.app.model.req.MobileRegisterReq;
|
||||
import com.youlai.boot.app.service.AppAuthService;
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.app.model.entity.AppUser;
|
||||
import com.youlai.boot.app.model.form.AppUserForm;
|
||||
import com.youlai.boot.framework.annotation.DataPermission;
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.system.model.query.UserQuery;
|
||||
import com.youlai.boot.system.model.vo.UserExportVO;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.app.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.app.service;
|
||||
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.auth.model.resp.CaptchaInfo;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
|
||||
public interface AppAuthService {
|
||||
|
||||
@@ -4,17 +4,15 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.app.model.entity.AppUser;
|
||||
import com.youlai.boot.app.service.AppAuthService;
|
||||
import com.youlai.boot.app.service.AppUserService;
|
||||
import com.youlai.boot.auth.model.resp.CaptchaInfo;
|
||||
import com.youlai.boot.auth.service.CaptchaService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.util.CodeGeneratorUtil;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import com.youlai.boot.framework.integration.sms.service.impl.AliyunSmsService;
|
||||
import com.youlai.boot.framework.integration.sms.service.impl.TencentSmsService;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.youlai.boot.support.sms.SmsService;
|
||||
import com.youlai.boot.support.sms.SmsTypeEnum;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -44,7 +42,7 @@ public class AppAuthServiceImpl implements AppAuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
private final AliyunSmsService smsService;
|
||||
private final SmsService smsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CaptchaService captchaService;
|
||||
private final AppUserService userService;
|
||||
@@ -76,7 +74,7 @@ public class AppAuthServiceImpl implements AppAuthService {
|
||||
Map<String, String> templateParams = new HashMap<>();
|
||||
templateParams.put("code", code);
|
||||
|
||||
boolean success = smsService.sendSms(mobile, SmsTypeEnum.REGISTER, templateParams);
|
||||
boolean success = smsService.send(mobile, SmsTypeEnum.REGISTER, templateParams);
|
||||
if (success) {
|
||||
// 缓存验证码至Redis,用于注册校验
|
||||
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_REGISTER_CODE, mobile), code, 5, TimeUnit.MINUTES);
|
||||
@@ -162,7 +160,7 @@ public class AppAuthServiceImpl implements AppAuthService {
|
||||
|
||||
// 方式2: 使用腾讯云短信(需要时取消下面注释,并注释掉上面的阿里云代码)
|
||||
try {
|
||||
success = smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
|
||||
success = smsService.send(mobile, SmsTypeEnum.LOGIN, templateParams);
|
||||
log.info("腾讯云短信发送结果: {}", success ? "成功" : "失败");
|
||||
} catch (Exception e) {
|
||||
log.error("腾讯云短信发送异常", e);
|
||||
|
||||
@@ -17,13 +17,13 @@ import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SystemConstants;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.model.Option;
|
||||
import com.youlai.boot.framework.integration.mail.service.MailService;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.support.mail.EmailService;
|
||||
import com.youlai.boot.support.sms.SmsService;
|
||||
import com.youlai.boot.support.sms.SmsTypeEnum;
|
||||
import com.youlai.boot.system.converter.UserConverter;
|
||||
import com.youlai.boot.system.enums.DictCodeEnum;
|
||||
import com.youlai.boot.system.mapper.UserMapper;
|
||||
@@ -69,7 +69,7 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
|
||||
|
||||
private final SmsService smsService;
|
||||
|
||||
private final MailService mailService;
|
||||
private final EmailService mailService;
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
@@ -463,7 +463,7 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
|
||||
|
||||
Map<String, String> templateParams = new HashMap<>();
|
||||
templateParams.put("code", code);
|
||||
boolean result = smsService.sendSms(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams);
|
||||
boolean result = smsService.send(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams);
|
||||
if (result) {
|
||||
// 缓存验证码,5分钟有效,用于更换手机号校验
|
||||
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
|
||||
@@ -546,7 +546,7 @@ public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> impl
|
||||
// TODO 为了方便测试,验证码固定为 123456,实际开发中在配置了邮箱服务后,可以使用上面的随机验证码
|
||||
String code = "123456";
|
||||
|
||||
mailService.sendMail(email, "邮箱验证码", "您的验证码为:" + code + ",请在5分钟内使用");
|
||||
mailService.send(email, "邮箱验证码", "您的验证码为:" + code + ",请在5分钟内使用");
|
||||
// 缓存验证码,5分钟有效,用于更换邮箱校验
|
||||
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email);
|
||||
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
|
||||
|
||||
@@ -6,7 +6,6 @@ import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.youlai.boot.app.model.vo.AppFileInfo;
|
||||
import com.youlai.boot.app.service.AppFileService;
|
||||
import com.youlai.boot.file.service.FileService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.form.LoginForm;
|
||||
import com.youlai.boot.auth.model.req.LoginReq;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.framework.annotation.RateLimit;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.RateLimit;
|
||||
import com.youlai.boot.auth.model.resp.CaptchaInfo;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -49,32 +49,11 @@ public class AuthController {
|
||||
@PostMapping("/login")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
@RateLimit
|
||||
public Result<AuthenticationToken> login(@RequestBody @Valid LoginForm request) {
|
||||
public Result<AuthenticationToken> login(@RequestBody @Valid LoginReq request) {
|
||||
AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword());
|
||||
return Result.success(authenticationToken);
|
||||
}
|
||||
|
||||
@Operation(summary = "短信验证码登录")
|
||||
@PostMapping("/login/sms")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> loginBySms(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile,
|
||||
@Parameter(description = "验证码", example = "123456") @RequestParam String code
|
||||
) {
|
||||
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
|
||||
return Result.success(loginResult);
|
||||
}
|
||||
|
||||
@Operation(summary = "发送登录短信验证码")
|
||||
@PostMapping("/sms/code")
|
||||
@RateLimit(limit = 1, window = 60)
|
||||
public Result<Void> sendSmsCode(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
|
||||
) {
|
||||
authService.sendSmsCode(mobile);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "退出登录")
|
||||
@DeleteMapping("/logout")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGOUT)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.req.QrCodeTicketReq;
|
||||
import com.youlai.boot.auth.model.resp.QrCodeGenerateResp;
|
||||
import com.youlai.boot.auth.model.resp.QrCodeStatusResp;
|
||||
import com.youlai.boot.auth.service.QrCodeLoginService;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import com.youlai.boot.common.annotation.RateLimit;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 扫码登录认证接口
|
||||
*
|
||||
* <p>generate/status/login 不需要登录态(PC 端未登录),由 Security 配置放行;
|
||||
* scan/confirm/cancel 需要 APP 端登录态,当前用户 ID 从 Security 上下文获取。</p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
@Tag(name = "01.认证中心")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/qr-code")
|
||||
@RequiredArgsConstructor
|
||||
public class QrCodeAuthController {
|
||||
|
||||
private final QrCodeLoginService qrCodeLoginService;
|
||||
|
||||
/**
|
||||
* 生成扫码登录票据
|
||||
*/
|
||||
@Operation(summary = "[扫码]生成扫码登录票据")
|
||||
@PostMapping("/generate")
|
||||
@RateLimit(limit = 30, window = 60)
|
||||
public Result<QrCodeGenerateResp> generate(HttpServletRequest request) {
|
||||
return Result.success(qrCodeLoginService.generate(IPUtils.getIpAddr(request)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询扫码状态
|
||||
*/
|
||||
@Operation(summary = "[扫码]查询扫码状态")
|
||||
@GetMapping("/status")
|
||||
@RateLimit(limit = 60, window = 60)
|
||||
public Result<QrCodeStatusResp> status(@RequestParam String ticket) {
|
||||
return Result.success(qrCodeLoginService.status(ticket));
|
||||
}
|
||||
|
||||
/**
|
||||
* APP 标记已扫码
|
||||
*/
|
||||
@Operation(summary = "[扫码]APP 标记已扫码")
|
||||
@PostMapping("/scan")
|
||||
public Result<QrCodeStatusResp> scan(@RequestBody @Valid QrCodeTicketReq form) {
|
||||
return Result.success(qrCodeLoginService.scan(form.getTicket(), SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* APP 确认登录
|
||||
*/
|
||||
@Operation(summary = "[扫码]APP 确认登录")
|
||||
@PostMapping("/confirm")
|
||||
public Result<QrCodeStatusResp> confirm(@RequestBody @Valid QrCodeTicketReq form) {
|
||||
return Result.success(qrCodeLoginService.confirm(form.getTicket(), SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* APP 取消登录
|
||||
*/
|
||||
@Operation(summary = "[扫码]APP 取消登录")
|
||||
@PostMapping("/cancel")
|
||||
public Result<QrCodeStatusResp> cancel(@RequestBody @Valid QrCodeTicketReq form) {
|
||||
return Result.success(qrCodeLoginService.cancel(form.getTicket(), SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* PC 端用票据换取会话令牌
|
||||
*/
|
||||
@Operation(summary = "[扫码]PC 端换取会话令牌")
|
||||
@PostMapping("/login")
|
||||
public Result<AuthenticationToken> login(@RequestBody @Valid QrCodeTicketReq form) {
|
||||
return Result.success(qrCodeLoginService.login(form.getTicket()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.RateLimit;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 短信验证码认证接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
@Tag(name = "01.认证中心")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/sms")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SmsAuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Operation(summary = "[短信]发送登录短信验证码")
|
||||
@PostMapping("/code")
|
||||
@RateLimit(limit = 1, window = 60)
|
||||
public Result<Void> sendCode(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
|
||||
) {
|
||||
authService.sendSmsCode(mobile);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "[短信]短信验证码登录")
|
||||
@PostMapping("/login")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> login(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile,
|
||||
@Parameter(description = "验证码", example = "123456") @RequestParam String code
|
||||
) {
|
||||
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
|
||||
return Result.success(loginResult);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.form.WxMaBindMobileForm;
|
||||
import com.youlai.boot.auth.model.form.WxMaPhoneLoginForm;
|
||||
import com.youlai.boot.auth.model.vo.WxMaLoginVO;
|
||||
import com.youlai.boot.auth.model.req.WxMaBindMobileReq;
|
||||
import com.youlai.boot.auth.model.req.WxMaPhoneLoginReq;
|
||||
import com.youlai.boot.auth.model.resp.WxMaLoginResp;
|
||||
import com.youlai.boot.auth.service.WxMaAuthService;
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.framework.annotation.RateLimit;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.RateLimit;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
@@ -29,9 +29,9 @@ import jakarta.validation.Valid;
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Tag(name = "13.微信小程序认证")
|
||||
@Tag(name = "01.认证中心")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/wxma/auth")
|
||||
@RequestMapping("/api/v1/auth/wxma")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WxMaAuthController {
|
||||
@@ -47,15 +47,15 @@ public class WxMaAuthController {
|
||||
* <li>未绑定手机号的用户:返回 openid,需调用绑定手机号接口</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号")
|
||||
@Operation(summary = "[小程序]静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号")
|
||||
@PostMapping("/silent-login")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
@RateLimit
|
||||
public Result<WxMaLoginVO> silentLogin(
|
||||
public Result<WxMaLoginResp> silentLogin(
|
||||
@Parameter(description = "微信登录凭证(wx.login 获取)", required = true, example = "0xxx")
|
||||
@RequestParam String code
|
||||
) {
|
||||
WxMaLoginVO result = wxMaAuthService.silentLogin(code);
|
||||
WxMaLoginResp result = wxMaAuthService.silentLogin(code);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@@ -66,11 +66,11 @@ public class WxMaAuthController {
|
||||
* <p>
|
||||
* 一步完成登录,无需绑定流程,自动创建新用户
|
||||
*/
|
||||
@Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
|
||||
@Operation(summary = "[小程序]手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
|
||||
@PostMapping("/phone-login")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
@RateLimit
|
||||
public Result<AuthenticationToken> phoneLogin(@Valid @RequestBody WxMaPhoneLoginForm req) {
|
||||
public Result<AuthenticationToken> phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) {
|
||||
AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode());
|
||||
return Result.success(result);
|
||||
}
|
||||
@@ -82,11 +82,11 @@ public class WxMaAuthController {
|
||||
* <p>
|
||||
* 绑定成功后自动完成登录
|
||||
*/
|
||||
@Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
|
||||
@Operation(summary = "[小程序]绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
|
||||
@PostMapping("/bind-mobile")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
@RateLimit
|
||||
public Result<AuthenticationToken> bindMobile(@Valid @RequestBody WxMaBindMobileForm req) {
|
||||
public Result<AuthenticationToken> bindMobile(@Valid @RequestBody WxMaBindMobileReq req) {
|
||||
AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.captcha.enums;
|
||||
package com.youlai.boot.auth.enums;
|
||||
|
||||
/**
|
||||
* EasyCaptcha 验证码类型
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.youlai.boot.auth.enums;
|
||||
|
||||
/**
|
||||
* 扫码登录票据状态
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
public enum QrCodeLoginStatusEnum {
|
||||
|
||||
/** 票据已创建,等待 APP 扫码 */
|
||||
WAITING,
|
||||
|
||||
/** APP 已扫码,等待用户在手机上确认 */
|
||||
SCANNED,
|
||||
|
||||
/** 用户已在 APP 上确认登录 */
|
||||
CONFIRMED,
|
||||
|
||||
/** PC 已用票据换取会话令牌,票据作废,不可再用 */
|
||||
LOGGED_IN,
|
||||
|
||||
/** 用户在 APP 上取消登录 */
|
||||
CANCELED,
|
||||
|
||||
/** 票据超时,由 Redis TTL 自动清理 */
|
||||
EXPIRED
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.youlai.boot.framework.captcha.exception;
|
||||
package com.youlai.boot.auth.exception;
|
||||
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 验证码异常
|
||||
* 图形验证码校验失败时抛出的异常。
|
||||
* <p>
|
||||
* 携带 {@link ResultCode} 用于统一错误响应。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.1.0
|
||||
*/
|
||||
@Getter
|
||||
public class CaptchaException extends RuntimeException {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.auth.security.exception;
|
||||
package com.youlai.boot.auth.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.youlai.boot.auth.qrcode.model;
|
||||
package com.youlai.boot.auth.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 扫码登录票据上下文,序列化为 JSON 存入 Redis。
|
||||
* <p>
|
||||
* 字段说明见 docs/youlai-boot/scan-code-login.md 的 Redis 存储设计。
|
||||
* 扫码登录票据上下文,序列化为 JSON 存入 Redis
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
@Data
|
||||
public class QrCodeLoginContext {
|
||||
@@ -13,7 +14,7 @@ public class QrCodeLoginContext {
|
||||
/** 票据,UUID 无连字符 */
|
||||
private String ticket;
|
||||
|
||||
/** 状态枚举名,取 {@link QrCodeLoginStatusEnum#name()} */
|
||||
/** 状态枚举名,取 {@link com.youlai.boot.auth.enums.QrCodeLoginStatusEnum#name()} */
|
||||
private String status;
|
||||
|
||||
/** 扫码用户 ID,scan 时写入 */
|
||||
@@ -34,6 +35,6 @@ public class QrCodeLoginContext {
|
||||
/** 确认时间戳(毫秒) */
|
||||
private Long confirmedAt;
|
||||
|
||||
/** generate 时的 PC 端 IP,用于审计 */
|
||||
/** generate 时的 PC 端 IP */
|
||||
private String clientIp;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.youlai.boot.auth.model.form;
|
||||
package com.youlai.boot.auth.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录表单
|
||||
* 登录请求参数
|
||||
*/
|
||||
@Schema(description = "登录请求参数")
|
||||
@Data
|
||||
public class LoginForm {
|
||||
public class LoginReq {
|
||||
|
||||
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.youlai.boot.auth.model.req;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 扫码登录票据请求,用于 scan/confirm/cancel/login 接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
@Data
|
||||
public class QrCodeTicketReq {
|
||||
|
||||
@NotBlank(message = "票据不能为空")
|
||||
private String ticket;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.youlai.boot.auth.model.form;
|
||||
package com.youlai.boot.auth.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 微信小程序绑定手机号表单
|
||||
* 微信小程序绑定手机号请求
|
||||
*/
|
||||
@Schema(description = "微信小程序绑定手机号请求")
|
||||
@Data
|
||||
public class WxMaBindMobileForm {
|
||||
public class WxMaBindMobileReq {
|
||||
|
||||
@NotBlank(message = "openid 不能为空")
|
||||
@Schema(description = "微信用户唯一标识", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.youlai.boot.auth.model.form;
|
||||
package com.youlai.boot.auth.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号快捷登录表单
|
||||
* 微信小程序手机号快捷登录请求
|
||||
*/
|
||||
@Schema(description = "微信小程序手机号快捷登录请求")
|
||||
@Data
|
||||
public class WxMaPhoneLoginForm {
|
||||
public class WxMaPhoneLoginReq {
|
||||
|
||||
@NotBlank(message = "微信登录凭证不能为空")
|
||||
@Schema(description = "微信登录凭证(wx.login 获取)", example = "0xxx")
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.captcha.model;
|
||||
package com.youlai.boot.auth.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.youlai.boot.auth.qrcode.model.vo;
|
||||
package com.youlai.boot.auth.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* generate 接口响应。
|
||||
* generate 接口响应
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "扫码票据生成结果")
|
||||
public class QrCodeGenerateVO {
|
||||
public class QrCodeGenerateResp {
|
||||
|
||||
@Schema(description = "票据")
|
||||
private String ticket;
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.youlai.boot.auth.qrcode.model.vo;
|
||||
package com.youlai.boot.auth.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* status/scan/confirm/cancel 接口响应。
|
||||
* status/scan/confirm/cancel 接口响应
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "扫码登录状态")
|
||||
public class QrCodeStatusVO {
|
||||
public class QrCodeStatusResp {
|
||||
|
||||
@Schema(description = "票据")
|
||||
private String ticket;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.auth.model.vo;
|
||||
package com.youlai.boot.auth.model.resp;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -14,7 +14,7 @@ import lombok.NoArgsConstructor;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "微信小程序登录响应")
|
||||
public class WxMaLoginVO {
|
||||
public class WxMaLoginResp {
|
||||
|
||||
@Schema(description = "是否新用户")
|
||||
private Boolean isNewUser;
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.youlai.boot.auth.qrcode.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.qrcode.model.form.QrCodeTicketForm;
|
||||
import com.youlai.boot.auth.qrcode.model.vo.QrCodeGenerateVO;
|
||||
import com.youlai.boot.auth.qrcode.model.vo.QrCodeStatusVO;
|
||||
import com.youlai.boot.auth.qrcode.service.QrCodeLoginService;
|
||||
import com.youlai.boot.framework.annotation.RateLimit;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 扫码登录接口。
|
||||
* <p>
|
||||
* generate/status/login 不需要登录态(PC 端未登录),由 Security 配置放行;
|
||||
* scan/confirm/cancel 需要 APP 端登录态,当前用户 ID 从 Security 上下文获取。
|
||||
*/
|
||||
@Tag(name = "02.扫码登录")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/qr-code")
|
||||
@RequiredArgsConstructor
|
||||
public class QrCodeLoginController {
|
||||
|
||||
private final QrCodeLoginService qrCodeLoginService;
|
||||
|
||||
@Operation(summary = "生成扫码登录票据")
|
||||
@PostMapping("/generate")
|
||||
@RateLimit(limit = 30, window = 60)
|
||||
public Result<QrCodeGenerateVO> generate(HttpServletRequest request) {
|
||||
return Result.success(qrCodeLoginService.generate(getClientIp(request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询扫码状态")
|
||||
@GetMapping("/status")
|
||||
@RateLimit(limit = 60, window = 60)
|
||||
public Result<QrCodeStatusVO> status(@RequestParam String ticket) {
|
||||
return Result.success(qrCodeLoginService.status(ticket));
|
||||
}
|
||||
|
||||
@Operation(summary = "APP 标记已扫码")
|
||||
@PostMapping("/scan")
|
||||
public Result<QrCodeStatusVO> scan(@RequestBody @Valid QrCodeTicketForm form) {
|
||||
return Result.success(qrCodeLoginService.scan(form.getTicket(), SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "APP 确认登录")
|
||||
@PostMapping("/confirm")
|
||||
public Result<QrCodeStatusVO> confirm(@RequestBody @Valid QrCodeTicketForm form) {
|
||||
return Result.success(qrCodeLoginService.confirm(form.getTicket(), SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "APP 取消登录")
|
||||
@PostMapping("/cancel")
|
||||
public Result<QrCodeStatusVO> cancel(@RequestBody @Valid QrCodeTicketForm form) {
|
||||
return Result.success(qrCodeLoginService.cancel(form.getTicket(), SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "PC 端换取会话令牌")
|
||||
@PostMapping("/login")
|
||||
public Result<AuthenticationToken> login(@RequestBody @Valid QrCodeTicketForm form) {
|
||||
return Result.success(qrCodeLoginService.login(form.getTicket()));
|
||||
}
|
||||
|
||||
/** 从请求头或连接信息中提取客户端 IP,兼容反向代理 */
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (StrUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
int comma = ip.indexOf(',');
|
||||
return comma > 0 ? ip.substring(0, comma).trim() : ip.trim();
|
||||
}
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
if (StrUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.youlai.boot.auth.qrcode.model;
|
||||
|
||||
/**
|
||||
* 扫码登录票据状态。
|
||||
* <p>
|
||||
* WAITING 票据已创建,等待 APP 扫码
|
||||
* SCANNED APP 已扫码,等待用户在手机上确认
|
||||
* CONFIRMED 用户已在 APP 上确认登录
|
||||
* LOGGED_IN PC 已用票据换取会话令牌,票据作废,不可再用
|
||||
* CANCELED 用户在 APP 上取消登录
|
||||
* EXPIRED 票据超时,由 Redis TTL 自动清理,内存中通常不会出现该值
|
||||
*/
|
||||
public enum QrCodeLoginStatusEnum {
|
||||
|
||||
WAITING,
|
||||
SCANNED,
|
||||
CONFIRMED,
|
||||
LOGGED_IN,
|
||||
CANCELED,
|
||||
EXPIRED
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.youlai.boot.auth.qrcode.model.form;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 扫码登录票据表单,用于 scan/confirm/cancel/login 接口。
|
||||
*/
|
||||
@Data
|
||||
public class QrCodeTicketForm {
|
||||
|
||||
@NotBlank(message = "票据不能为空")
|
||||
private String ticket;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.youlai.boot.auth.qrcode.service;
|
||||
|
||||
import com.youlai.boot.auth.qrcode.model.vo.QrCodeGenerateVO;
|
||||
import com.youlai.boot.auth.qrcode.model.vo.QrCodeStatusVO;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
|
||||
/**
|
||||
* 扫码登录服务。
|
||||
*/
|
||||
public interface QrCodeLoginService {
|
||||
|
||||
/** 生成票据,未登录调用 */
|
||||
QrCodeGenerateVO generate(String clientIp);
|
||||
|
||||
/** 查询状态,未登录调用 */
|
||||
QrCodeStatusVO status(String ticket);
|
||||
|
||||
/** APP 标记已扫码,需 APP 端已登录 */
|
||||
QrCodeStatusVO scan(String ticket, Long userId);
|
||||
|
||||
/** APP 确认登录,需 APP 端已登录 */
|
||||
QrCodeStatusVO confirm(String ticket, Long userId);
|
||||
|
||||
/** APP 取消登录,需 APP 端已登录 */
|
||||
QrCodeStatusVO cancel(String ticket, Long userId);
|
||||
|
||||
/** PC 端用票据换取会话令牌,未登录调用 */
|
||||
AuthenticationToken login(String ticket);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.youlai.boot.auth.qrcode.util;
|
||||
|
||||
/**
|
||||
* 昵称脱敏:保留首尾各一个字符,中间用 * 替换。
|
||||
* <p>
|
||||
* 长度 1:原样返回
|
||||
* 长度 2:首字 + *
|
||||
* 长度 ≥3:首字 + (n-2) 个 * + 末字
|
||||
*/
|
||||
public final class QrCodeNicknameMasker {
|
||||
|
||||
private QrCodeNicknameMasker() {
|
||||
}
|
||||
|
||||
public static String mask(String nickname) {
|
||||
if (nickname == null || nickname.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
int len = nickname.length();
|
||||
if (len == 1) {
|
||||
return nickname;
|
||||
}
|
||||
if (len == 2) {
|
||||
return nickname.charAt(0) + "*";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(nickname.charAt(0));
|
||||
for (int i = 0; i < len - 2; i++) {
|
||||
sb.append('*');
|
||||
}
|
||||
sb.append(nickname.charAt(len - 1));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import cn.hutool.json.JSONUtil;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.web.util.ResponseWriter;
|
||||
import com.youlai.boot.framework.captcha.exception.CaptchaException;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import com.youlai.boot.auth.exception.CaptchaException;
|
||||
import com.youlai.boot.auth.service.CaptchaService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
|
||||
@@ -6,8 +6,8 @@ import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import com.youlai.boot.auth.security.exception.SmsCaptchaException;
|
||||
import com.youlai.boot.auth.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.auth.exception.SmsCaptchaException;
|
||||
import com.youlai.boot.auth.security.token.SmsAuthenticationToken;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
|
||||
@@ -7,8 +7,8 @@ import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import com.youlai.boot.auth.security.exception.MobileNotBoundException;
|
||||
import com.youlai.boot.auth.security.model.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.common.exception.MobileNotBoundException;
|
||||
import com.youlai.boot.auth.security.token.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.auth.security.model;
|
||||
package com.youlai.boot.auth.security.token;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.auth.security.model;
|
||||
package com.youlai.boot.auth.security.token;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
@@ -1,56 +1,143 @@
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.auth.model.resp.CaptchaInfo;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.auth.security.token.SmsAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.support.sms.SmsTypeEnum;
|
||||
import com.youlai.boot.support.sms.SmsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
* 认证服务
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public interface AuthService {
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
private final SmsService smsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
/**
|
||||
* 账号密码登录
|
||||
* 用户名密码登录
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 认证令牌
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken login(String username, String password);
|
||||
public AuthenticationToken login(String username, String password) {
|
||||
// 1. 创建用于密码认证的令牌(未认证)
|
||||
UsernamePasswordAuthenticationToken authenticationToken =
|
||||
new UsernamePasswordAuthenticationToken(username.trim(), password);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
// 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
|
||||
// 默认情况下会委托给 DaoAuthenticationProvider:
|
||||
// 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现)
|
||||
// 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
|
||||
// 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken authenticationTokenResponse =
|
||||
tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authenticationTokenResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
public void sendSmsCode(String mobile) {
|
||||
|
||||
// 随机生成4位验证码
|
||||
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
|
||||
// TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码
|
||||
String code = "1234";
|
||||
|
||||
// 发送短信验证码
|
||||
Map<String, String> templateParams = new HashMap<>();
|
||||
templateParams.put("code", code);
|
||||
try {
|
||||
smsService.send(mobile, SmsTypeEnum.LOGIN, templateParams);
|
||||
} catch (Exception e) {
|
||||
log.error("发送短信验证码失败", e);
|
||||
}
|
||||
// 缓存验证码至Redis,用于登录校验
|
||||
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param code 验证码
|
||||
* @return 认证令牌
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken loginBySms(String mobile, String code);
|
||||
public AuthenticationToken loginBySms(String mobile, String code) {
|
||||
// 1. 创建用户短信验证码认证的令牌(未认证)
|
||||
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* 注销登录
|
||||
*/
|
||||
void sendSmsCode(String mobile);
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
void logout();
|
||||
public void logout() {
|
||||
String token = SecurityUtils.getAccessToken();
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
tokenManager.invalidateToken(token);
|
||||
// 清除Security上下文
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
CaptchaInfo getCaptcha();
|
||||
public CaptchaInfo getCaptcha() {
|
||||
return captchaService.generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
* 刷新token
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 认证令牌
|
||||
* @return 新的访问令牌
|
||||
*/
|
||||
AuthenticationToken refreshToken(String refreshToken);
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
return tokenManager.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.captcha.service;
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
@@ -6,11 +6,11 @@ import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.captcha.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.auth.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.captcha.config.CaptchaProperties;
|
||||
import com.youlai.boot.framework.captcha.exception.CaptchaException;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.config.property.CaptchaProperties;
|
||||
import com.youlai.boot.auth.exception.CaptchaException;
|
||||
import com.youlai.boot.auth.model.resp.CaptchaInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -20,6 +20,9 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 验证码服务
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.1.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -1,16 +1,14 @@
|
||||
package com.youlai.boot.auth.qrcode.service.impl;
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.qrcode.model.QrCodeLoginContext;
|
||||
import com.youlai.boot.auth.qrcode.model.QrCodeLoginStatusEnum;
|
||||
import com.youlai.boot.auth.qrcode.model.vo.QrCodeGenerateVO;
|
||||
import com.youlai.boot.auth.qrcode.model.vo.QrCodeStatusVO;
|
||||
import com.youlai.boot.auth.qrcode.service.QrCodeLoginService;
|
||||
import com.youlai.boot.auth.qrcode.util.QrCodeNicknameMasker;
|
||||
import com.youlai.boot.auth.enums.QrCodeLoginStatusEnum;
|
||||
import com.youlai.boot.auth.model.dto.QrCodeLoginContext;
|
||||
import com.youlai.boot.auth.model.resp.QrCodeGenerateResp;
|
||||
import com.youlai.boot.auth.model.resp.QrCodeStatusResp;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import com.youlai.boot.common.exception.TokenInvalidException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
@@ -27,23 +25,20 @@ import tools.jackson.databind.json.JsonMapper;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 扫码登录服务实现。
|
||||
* <p>
|
||||
* 票据上下文以 JSON 形式存入 Redis,TTL 与有效期一致;状态流转的合法性全部由
|
||||
* {@link #requireStatus} / {@link #requireSameUser} 在写入前拦截,非法迁移直接抛业务异常,
|
||||
* 因此同一 ticket 被多端并发操作时不需要分布式锁——最坏情况是后到的操作在状态校验处失败。
|
||||
* PC 端换取会话时直接复用 {@link TokenManager#generateToken(Authentication)},
|
||||
* 生成的令牌与账号密码登录走的是同一套会话治理(单/多设备、登出、刷新),前端无差别处理。
|
||||
* 扫码登录服务
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.5.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class QrCodeLoginServiceImpl implements QrCodeLoginService {
|
||||
public class QrCodeLoginService {
|
||||
|
||||
/** 票据默认有效期(秒),二维码在此时间内未走完流程即由 Redis TTL 自动清理 */
|
||||
/** 票据默认有效期(秒) */
|
||||
private static final int DEFAULT_EXPIRE_SECONDS = 300;
|
||||
|
||||
/** 状态流转时若 Redis 剩余 TTL 小于该值,补足到此值,避免临界点票据在下一步操作前被清掉 */
|
||||
/** 状态流转时若 Redis 剩余 TTL 小于该值,补足到此值 */
|
||||
private static final int MIN_REMAIN_SECONDS = 30;
|
||||
|
||||
/**
|
||||
@@ -56,8 +51,10 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
|
||||
private final TokenManager tokenManager;
|
||||
private final UserSocialService userSocialService;
|
||||
|
||||
@Override
|
||||
public QrCodeGenerateVO generate(String clientIp) {
|
||||
/**
|
||||
* 生成扫码票据,写入 Redis 后返回 ticket 和有效期
|
||||
*/
|
||||
public QrCodeGenerateResp generate(String clientIp) {
|
||||
String ticket = IdUtil.fastSimpleUUID();
|
||||
QrCodeLoginContext ctx = new QrCodeLoginContext();
|
||||
ctx.setTicket(ticket);
|
||||
@@ -65,60 +62,70 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
|
||||
ctx.setCreatedAt(System.currentTimeMillis());
|
||||
ctx.setClientIp(clientIp);
|
||||
save(ctx, DEFAULT_EXPIRE_SECONDS);
|
||||
return QrCodeGenerateVO.builder()
|
||||
return QrCodeGenerateResp.builder()
|
||||
.ticket(ticket)
|
||||
.expireSeconds(DEFAULT_EXPIRE_SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public QrCodeStatusVO status(String ticket) {
|
||||
/**
|
||||
* PC 端轮询扫码状态,未登录调用
|
||||
*/
|
||||
public QrCodeStatusResp status(String ticket) {
|
||||
QrCodeLoginContext ctx = loadOrThrow(ticket);
|
||||
return toVO(ctx, remainingSeconds(ticket));
|
||||
return toResp(ctx, remainingSeconds(ticket));
|
||||
}
|
||||
|
||||
@Override
|
||||
public QrCodeStatusVO scan(String ticket, Long userId) {
|
||||
/**
|
||||
* APP 端标记已扫码,需已登录。WAITING → SCANNED
|
||||
*/
|
||||
public QrCodeStatusResp scan(String ticket, Long userId) {
|
||||
QrCodeLoginContext ctx = loadOrThrow(ticket);
|
||||
requireStatus(ctx, QrCodeLoginStatusEnum.WAITING);
|
||||
fillUserInfo(ctx, userId);
|
||||
ctx.setStatus(QrCodeLoginStatusEnum.SCANNED.name());
|
||||
ctx.setScannedAt(System.currentTimeMillis());
|
||||
save(ctx, refreshTtl(ticket));
|
||||
return toVO(ctx, remainingSeconds(ticket));
|
||||
return toResp(ctx, remainingSeconds(ticket));
|
||||
}
|
||||
|
||||
@Override
|
||||
public QrCodeStatusVO confirm(String ticket, Long userId) {
|
||||
/**
|
||||
* APP 端确认登录,需扫码本人操作。SCANNED → CONFIRMED
|
||||
*/
|
||||
public QrCodeStatusResp confirm(String ticket, Long userId) {
|
||||
QrCodeLoginContext ctx = loadOrThrow(ticket);
|
||||
requireStatus(ctx, QrCodeLoginStatusEnum.SCANNED);
|
||||
requireSameUser(ctx, userId);
|
||||
ctx.setStatus(QrCodeLoginStatusEnum.CONFIRMED.name());
|
||||
ctx.setConfirmedAt(System.currentTimeMillis());
|
||||
save(ctx, refreshTtl(ticket));
|
||||
return toVO(ctx, remainingSeconds(ticket));
|
||||
return toResp(ctx, remainingSeconds(ticket));
|
||||
}
|
||||
|
||||
@Override
|
||||
public QrCodeStatusVO cancel(String ticket, Long userId) {
|
||||
/**
|
||||
* APP 端取消登录,仅流程中的票据可取消
|
||||
*/
|
||||
public QrCodeStatusResp cancel(String ticket, Long userId) {
|
||||
QrCodeLoginContext ctx = loadOrThrow(ticket);
|
||||
QrCodeLoginStatusEnum current = QrCodeLoginStatusEnum.valueOf(ctx.getStatus());
|
||||
// 只有还在流程中的票据(等待扫码 / 已扫码 / 已确认)允许取消,已登录或已取消的重复操作直接拒绝
|
||||
// 只有还在流程中的票据(等待扫码 / 已扫码 / 已确认)允许取消
|
||||
if (current != QrCodeLoginStatusEnum.WAITING
|
||||
&& current != QrCodeLoginStatusEnum.SCANNED
|
||||
&& current != QrCodeLoginStatusEnum.CONFIRMED) {
|
||||
throw new TokenInvalidException(ResultCode.QR_CODE_STATUS_ILLEGAL);
|
||||
}
|
||||
// 一旦有人扫过码,取消权就归扫码本人,防止他人替扫码用户取消
|
||||
// 一旦有人扫过码,取消权就归扫码本人
|
||||
if (current != QrCodeLoginStatusEnum.WAITING && ctx.getUserId() != null) {
|
||||
requireSameUser(ctx, userId);
|
||||
}
|
||||
ctx.setStatus(QrCodeLoginStatusEnum.CANCELED.name());
|
||||
save(ctx, refreshTtl(ticket));
|
||||
return toVO(ctx, remainingSeconds(ticket));
|
||||
return toResp(ctx, remainingSeconds(ticket));
|
||||
}
|
||||
|
||||
@Override
|
||||
/**
|
||||
* PC 端用票据换取会话令牌,CONFIRMED → LOGGED_IN(一次性)
|
||||
*/
|
||||
public AuthenticationToken login(String ticket) {
|
||||
QrCodeLoginContext ctx = loadOrThrow(ticket);
|
||||
requireStatus(ctx, QrCodeLoginStatusEnum.CONFIRMED);
|
||||
@@ -130,15 +137,15 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
details, null, details.getAuthorities());
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
// 换取令牌成功后立即把票据置为已使用(一次性),再次 login 会在 requireStatus(CONFIRMED) 处被拒,杜绝重放
|
||||
// 换取令牌成功后立即把票据置为已使用(一次性),再次 login 会在 requireStatus(CONFIRMED) 处被拒
|
||||
ctx.setStatus(QrCodeLoginStatusEnum.LOGGED_IN.name());
|
||||
save(ctx, Math.max(remainingSeconds(ticket), MIN_REMAIN_SECONDS));
|
||||
return token;
|
||||
}
|
||||
|
||||
// ======================== private ========================
|
||||
|
||||
/** 读取票据上下文,票据为空、不存在或已过期都视为 QR_CODE_NOT_FOUND */
|
||||
/**
|
||||
* 读取票据上下文,票据为空、不存在或已过期都视为 QR_CODE_NOT_FOUND
|
||||
*/
|
||||
private QrCodeLoginContext loadOrThrow(String ticket) {
|
||||
if (StrUtil.isBlank(ticket)) {
|
||||
throw new TokenInvalidException(ResultCode.QR_CODE_NOT_FOUND);
|
||||
@@ -159,21 +166,27 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前状态必须等于期望状态,否则说明流程被跳步或重复操作 */
|
||||
/**
|
||||
* 当前状态必须等于期望状态,否则说明流程被跳步或重复操作
|
||||
*/
|
||||
private void requireStatus(QrCodeLoginContext ctx, QrCodeLoginStatusEnum expected) {
|
||||
if (!expected.name().equals(ctx.getStatus())) {
|
||||
throw new TokenInvalidException(ResultCode.QR_CODE_STATUS_ILLEGAL);
|
||||
}
|
||||
}
|
||||
|
||||
/** 操作者必须是当初扫码的那个用户,防止 A 扫码 B 确认 */
|
||||
/**
|
||||
* 操作者必须是当初扫码的那个用户,防止 A 扫码 B 确认
|
||||
*/
|
||||
private void requireSameUser(QrCodeLoginContext ctx, Long userId) {
|
||||
if (ctx.getUserId() == null || !ctx.getUserId().equals(userId)) {
|
||||
throw new TokenInvalidException(ResultCode.QR_CODE_USER_MISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
/** 扫码时把当前 APP 用户的昵称、头像写进上下文,供 PC 端 status 展示 */
|
||||
/**
|
||||
* 扫码时把当前 APP 用户的昵称、头像写进上下文,供 PC 端 status 展示
|
||||
*/
|
||||
private void fillUserInfo(QrCodeLoginContext ctx, Long userId) {
|
||||
SecurityUser info = userSocialService.getAuthInfoByUserId(userId);
|
||||
if (info == null) {
|
||||
@@ -184,35 +197,43 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
|
||||
ctx.setAvatar(info.getAvatar());
|
||||
}
|
||||
|
||||
/** 重写整个上下文并刷新 TTL */
|
||||
/**
|
||||
* 重写整个上下文并刷新 TTL
|
||||
*/
|
||||
private void save(QrCodeLoginContext ctx, int ttl) {
|
||||
redisTemplate.opsForValue().set(key(ctx.getTicket()), ctx, ttl, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/** 票据在 Redis 中的剩余秒数,取不到或已过期返回 0 */
|
||||
/**
|
||||
* 票据在 Redis 中的剩余秒数,取不到或已过期返回 0
|
||||
*/
|
||||
private int remainingSeconds(String ticket) {
|
||||
Long ttl = redisTemplate.getExpire(key(ticket), TimeUnit.SECONDS);
|
||||
return ttl == null ? 0 : Math.max(ttl.intValue(), 0);
|
||||
}
|
||||
|
||||
/** 状态流转时写回的 TTL:维持剩余时间,不足 MIN_REMAIN_SECONDS 则补足 */
|
||||
/**
|
||||
* 状态流转时写回的 TTL:维持剩余时间,不足 MIN_REMAIN_SECONDS 则补足
|
||||
*/
|
||||
private int refreshTtl(String ticket) {
|
||||
int remain = remainingSeconds(ticket);
|
||||
return remain < MIN_REMAIN_SECONDS ? MIN_REMAIN_SECONDS : remain;
|
||||
}
|
||||
|
||||
/** 上下文转前端 VO,昵称脱敏、用户信息仅在扫码后暴露 */
|
||||
private QrCodeStatusVO toVO(QrCodeLoginContext ctx, int expireSeconds) {
|
||||
/**
|
||||
* 上下文转前端 VO,昵称脱敏、用户信息仅在扫码后暴露
|
||||
*/
|
||||
private QrCodeStatusResp toResp(QrCodeLoginContext ctx, int expireSeconds) {
|
||||
QrCodeLoginStatusEnum status = QrCodeLoginStatusEnum.valueOf(ctx.getStatus());
|
||||
String nickname = null;
|
||||
String avatar = null;
|
||||
// WAITING 阶段谁都能查状态,此时不能泄露用户信息;扫码/确认后才回传脱敏昵称与头像
|
||||
if (status == QrCodeLoginStatusEnum.SCANNED
|
||||
|| status == QrCodeLoginStatusEnum.CONFIRMED) {
|
||||
nickname = QrCodeNicknameMasker.mask(ctx.getNickname());
|
||||
nickname = maskNickname(ctx.getNickname());
|
||||
avatar = ctx.getAvatar();
|
||||
}
|
||||
return QrCodeStatusVO.builder()
|
||||
return QrCodeStatusResp.builder()
|
||||
.ticket(ctx.getTicket())
|
||||
.status(status.name())
|
||||
.nickname(nickname)
|
||||
@@ -221,7 +242,23 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/** 拼接票据在 Redis 中的完整 Key */
|
||||
/**
|
||||
* 昵称脱敏:保留首尾各一个字符,中间用 * 替换
|
||||
*/
|
||||
private String maskNickname(String nickname) {
|
||||
if (StrUtil.isBlank(nickname)) {
|
||||
return "";
|
||||
}
|
||||
int len = nickname.length();
|
||||
if (len == 1) {
|
||||
return nickname;
|
||||
}
|
||||
return StrUtil.hide(nickname, 1, len - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接票据在 Redis 中的完整 Key
|
||||
*/
|
||||
private String key(String ticket) {
|
||||
return StrUtil.format(RedisConstants.Auth.QR_CODE_LOGIN, ticket);
|
||||
}
|
||||
@@ -1,15 +1,53 @@
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import com.youlai.boot.auth.model.vo.WxMaLoginVO;
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.resp.WxMaLoginResp;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.common.exception.MobileNotBoundException;
|
||||
import com.youlai.boot.auth.security.token.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.SysUser;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import com.youlai.boot.system.service.UserRoleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 微信小程序认证服务接口
|
||||
* 微信小程序认证服务
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public interface WxMaAuthService {
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WxMaAuthService {
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
private final UserService userService;
|
||||
private final UserSocialService userSocialService;
|
||||
private final UserRoleService userRoleService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 静默登录
|
||||
@@ -21,21 +59,61 @@ public interface WxMaAuthService {
|
||||
* @param code 微信登录凭证(wx.login 获取)
|
||||
* @return 登录结果(成功返回 token,需绑定返回 openid)
|
||||
*/
|
||||
WxMaLoginVO silentLogin(String code);
|
||||
public WxMaLoginResp silentLogin(String code) {
|
||||
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code);
|
||||
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(token);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return WxMaLoginResp.builder()
|
||||
.isNewUser(false)
|
||||
.needBindMobile(false)
|
||||
.accessToken(authToken.getAccessToken())
|
||||
.refreshToken(authToken.getRefreshToken())
|
||||
.tokenType(authToken.getTokenType())
|
||||
.expiresIn(authToken.getExpiresIn())
|
||||
.build();
|
||||
} catch (MobileNotBoundException e) {
|
||||
return WxMaLoginResp.builder()
|
||||
.isNewUser(true)
|
||||
.needBindMobile(true)
|
||||
.openid(e.getOpenid())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号快捷登录
|
||||
* <p>
|
||||
* 同时使用微信登录凭证和手机号授权凭证,
|
||||
* 一步完成用户注册/登录,无需额外绑定流程。
|
||||
* 适用于企业认证的小程序(已开通手机号快捷登录权限)。
|
||||
* </p>
|
||||
*
|
||||
* @param loginCode 微信登录凭证(wx.login 获取)
|
||||
* @param phoneCode 手机号授权凭证(getPhoneNumber 事件获取)
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken phoneLogin(String loginCode, String phoneCode);
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken phoneLogin(String loginCode, String phoneCode) {
|
||||
// 1. 解析微信登录凭证,获取会话信息
|
||||
WxMaJscode2SessionResult session = resolveSession(loginCode);
|
||||
String openid = session.getOpenid();
|
||||
|
||||
// 2. 解析手机号授权凭证,获取手机号
|
||||
String mobile = resolvePhoneNumber(phoneCode);
|
||||
|
||||
log.info("微信小程序手机号快捷登录:openid={}, mobile={}", openid, mobile);
|
||||
|
||||
// 3. 查询或创建用户
|
||||
SysUser user = findOrCreateUser(mobile);
|
||||
|
||||
// 4. 绑定微信 openid
|
||||
bindWechatOpenid(user, session);
|
||||
|
||||
// 5. 生成认证令牌
|
||||
return generateAuthToken(mobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
@@ -49,5 +127,139 @@ public interface WxMaAuthService {
|
||||
* @param smsCode 短信验证码
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken bindMobile(String openid, String mobile, String smsCode);
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken bindMobile(String openid, String mobile, String smsCode) {
|
||||
// 1. 验证短信验证码
|
||||
validateSmsCode(mobile, smsCode);
|
||||
|
||||
// 2. 查询或创建用户
|
||||
SysUser user = findOrCreateUser(mobile);
|
||||
|
||||
// 3. 绑定微信 openid
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
openid,
|
||||
null, null, null, null
|
||||
);
|
||||
|
||||
log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid);
|
||||
|
||||
// 4. 生成认证令牌
|
||||
return generateAuthToken(mobile);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 解析微信登录凭证,获取会话信息
|
||||
*/
|
||||
private WxMaJscode2SessionResult resolveSession(String loginCode) {
|
||||
try {
|
||||
return wxMaService.jsCode2SessionInfo(loginCode);
|
||||
} catch (Exception e) {
|
||||
log.error("获取微信会话信息失败,loginCode={}", loginCode, e);
|
||||
throw new IllegalArgumentException("微信登录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析手机号授权凭证,获取手机号
|
||||
*/
|
||||
private String resolvePhoneNumber(String phoneCode) {
|
||||
try {
|
||||
WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode);
|
||||
return phoneInfo.getPhoneNumber();
|
||||
} catch (Exception e) {
|
||||
log.error("获取微信手机号失败,phoneCode={}", phoneCode, e);
|
||||
throw new IllegalArgumentException("获取手机号失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询或创建用户
|
||||
*/
|
||||
private SysUser findOrCreateUser(String mobile) {
|
||||
SysUser user = userService.lambdaQuery()
|
||||
.eq(SysUser::getMobile, mobile)
|
||||
.one();
|
||||
|
||||
if (user == null) {
|
||||
user = createNewUser(mobile);
|
||||
log.info("微信小程序登录:创建新用户,mobile={}, userId={}", mobile, user.getId());
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
* <p>
|
||||
* 新用户默认分配 GUEST(访问游客)角色
|
||||
* </p>
|
||||
*/
|
||||
private SysUser createNewUser(String mobile) {
|
||||
SysUser user = new SysUser();
|
||||
user.setMobile(mobile);
|
||||
user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8));
|
||||
user.setNickname("微信用户");
|
||||
user.setStatus(1);
|
||||
user.setIsDeleted(0);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
userService.save(user);
|
||||
|
||||
// 分配 GUEST 角色(角色ID=3)
|
||||
userRoleService.saveUserRoles(user.getId(), Collections.singletonList(3L));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定微信 openid
|
||||
*/
|
||||
private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) {
|
||||
try {
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
session.getOpenid(),
|
||||
session.getUnionid(),
|
||||
user.getNickname(),
|
||||
user.getAvatar(),
|
||||
session.getSessionKey()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// 绑定失败不影响登录
|
||||
log.warn("绑定微信 openid 失败,userId={}, openid={}", user.getId(), session.getOpenid(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*/
|
||||
private void validateSmsCode(String mobile, String smsCode) {
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
|
||||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
if (!StrUtil.equals(smsCode, cachedCode)) {
|
||||
throw new IllegalArgumentException("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码
|
||||
redisTemplate.delete(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成认证令牌
|
||||
*/
|
||||
private AuthenticationToken generateAuthToken(String mobile) {
|
||||
SecurityUserDetails userDetails = new SecurityUserDetails(userService.getAuthInfoByMobile(mobile));
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities()
|
||||
);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authToken;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package com.youlai.boot.auth.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.auth.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证服务实现类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
private final SmsService smsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
/**
|
||||
* 用户名密码登录
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken login(String username, String password) {
|
||||
// 1. 创建用于密码认证的令牌(未认证)
|
||||
UsernamePasswordAuthenticationToken authenticationToken =
|
||||
new UsernamePasswordAuthenticationToken(username.trim(), password);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
// 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
|
||||
// 默认情况下会委托给 DaoAuthenticationProvider:
|
||||
// 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现)
|
||||
// 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
|
||||
// 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken authenticationTokenResponse =
|
||||
tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authenticationTokenResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
@Override
|
||||
public void sendSmsCode(String mobile) {
|
||||
|
||||
// 随机生成4位验证码
|
||||
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
|
||||
// TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码
|
||||
String code = "1234";
|
||||
|
||||
// 发送短信验证码
|
||||
Map<String, String> templateParams = new HashMap<>();
|
||||
templateParams.put("code", code);
|
||||
try {
|
||||
smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
|
||||
} catch (Exception e) {
|
||||
log.error("发送短信验证码失败", e);
|
||||
}
|
||||
// 缓存验证码至Redis,用于登录校验
|
||||
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param code 验证码
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken loginBySms(String mobile, String code) {
|
||||
// 1. 创建用户短信验证码认证的令牌(未认证)
|
||||
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销登录
|
||||
*/
|
||||
@Override
|
||||
public void logout() {
|
||||
String token = SecurityUtils.getAccessToken();
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
tokenManager.invalidateToken(token);
|
||||
// 清除Security上下文
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
@Override
|
||||
public CaptchaInfo getCaptcha() {
|
||||
return captchaService.generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 新的访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
return tokenManager.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
package com.youlai.boot.auth.service.impl;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.vo.WxMaLoginVO;
|
||||
import com.youlai.boot.auth.service.WxMaAuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.auth.security.exception.MobileNotBoundException;
|
||||
import com.youlai.boot.auth.security.model.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.common.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.SysUser;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import com.youlai.boot.system.service.UserRoleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 微信小程序认证服务实现
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WxMaAuthServiceImpl implements WxMaAuthService {
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
private final UserService userService;
|
||||
private final UserSocialService userSocialService;
|
||||
private final UserRoleService userRoleService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 静默登录
|
||||
*/
|
||||
@Override
|
||||
public WxMaLoginVO silentLogin(String code) {
|
||||
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code);
|
||||
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(token);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return WxMaLoginVO.builder()
|
||||
.isNewUser(false)
|
||||
.needBindMobile(false)
|
||||
.accessToken(authToken.getAccessToken())
|
||||
.refreshToken(authToken.getRefreshToken())
|
||||
.tokenType(authToken.getTokenType())
|
||||
.expiresIn(authToken.getExpiresIn())
|
||||
.build();
|
||||
} catch (MobileNotBoundException e) {
|
||||
return WxMaLoginVO.builder()
|
||||
.isNewUser(true)
|
||||
.needBindMobile(true)
|
||||
.openid(e.getOpenid())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号快捷登录
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken phoneLogin(String loginCode, String phoneCode) {
|
||||
// 1. 解析微信登录凭证,获取会话信息
|
||||
WxMaJscode2SessionResult session = resolveSession(loginCode);
|
||||
String openid = session.getOpenid();
|
||||
|
||||
// 2. 解析手机号授权凭证,获取手机号
|
||||
String mobile = resolvePhoneNumber(phoneCode);
|
||||
|
||||
log.info("微信小程序手机号快捷登录:openid={}, mobile={}", openid, mobile);
|
||||
|
||||
// 3. 查询或创建用户
|
||||
SysUser user = findOrCreateUser(mobile);
|
||||
|
||||
// 4. 绑定微信 openid
|
||||
bindWechatOpenid(user, session);
|
||||
|
||||
// 5. 生成认证令牌
|
||||
return generateAuthToken(mobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken bindMobile(String openid, String mobile, String smsCode) {
|
||||
// 1. 验证短信验证码
|
||||
validateSmsCode(mobile, smsCode);
|
||||
|
||||
// 2. 查询或创建用户
|
||||
SysUser user = findOrCreateUser(mobile);
|
||||
|
||||
// 3. 绑定微信 openid
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
openid,
|
||||
null, null, null, null
|
||||
);
|
||||
|
||||
log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid);
|
||||
|
||||
// 4. 生成认证令牌
|
||||
return generateAuthToken(mobile);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 解析微信登录凭证,获取会话信息
|
||||
*/
|
||||
private WxMaJscode2SessionResult resolveSession(String loginCode) {
|
||||
try {
|
||||
return wxMaService.jsCode2SessionInfo(loginCode);
|
||||
} catch (Exception e) {
|
||||
log.error("获取微信会话信息失败,loginCode={}", loginCode, e);
|
||||
throw new IllegalArgumentException("微信登录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析手机号授权凭证,获取手机号
|
||||
*/
|
||||
private String resolvePhoneNumber(String phoneCode) {
|
||||
try {
|
||||
WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode);
|
||||
return phoneInfo.getPhoneNumber();
|
||||
} catch (Exception e) {
|
||||
log.error("获取微信手机号失败,phoneCode={}", phoneCode, e);
|
||||
throw new IllegalArgumentException("获取手机号失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询或创建用户
|
||||
*/
|
||||
private SysUser findOrCreateUser(String mobile) {
|
||||
SysUser user = userService.lambdaQuery()
|
||||
.eq(SysUser::getMobile, mobile)
|
||||
.one();
|
||||
|
||||
if (user == null) {
|
||||
user = createNewUser(mobile);
|
||||
log.info("微信小程序登录:创建新用户,mobile={}, userId={}", mobile, user.getId());
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
* <p>
|
||||
* 新用户默认分配 GUEST(访问游客)角色
|
||||
* </p>
|
||||
*/
|
||||
private SysUser createNewUser(String mobile) {
|
||||
SysUser user = new SysUser();
|
||||
user.setMobile(mobile);
|
||||
user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8));
|
||||
user.setNickname("微信用户");
|
||||
user.setStatus(1);
|
||||
user.setIsDeleted(0);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
userService.save(user);
|
||||
|
||||
// 分配 GUEST 角色(角色ID=3)
|
||||
userRoleService.saveUserRoles(user.getId(), Collections.singletonList(3L));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定微信 openid
|
||||
*/
|
||||
private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) {
|
||||
try {
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
session.getOpenid(),
|
||||
session.getUnionid(),
|
||||
user.getNickname(),
|
||||
user.getAvatar(),
|
||||
session.getSessionKey()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// 绑定失败不影响登录
|
||||
log.warn("绑定微信 openid 失败,userId={}, openid={}", user.getId(), session.getOpenid(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*/
|
||||
private void validateSmsCode(String mobile, String smsCode) {
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
|
||||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
if (!StrUtil.equals(smsCode, cachedCode)) {
|
||||
throw new IllegalArgumentException("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码
|
||||
redisTemplate.delete(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成认证令牌
|
||||
*/
|
||||
private AuthenticationToken generateAuthToken(String mobile) {
|
||||
SecurityUserDetails userDetails = new SecurityUserDetails(userService.getAuthInfoByMobile(mobile));
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities()
|
||||
);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authToken;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import com.youlai.boot.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.codegen.service.GenTableService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.codegen.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import com.youlai.boot.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.codegen.enums.QueryTypeEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.annotation;
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.annotation;
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.annotation;
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.annotation;
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
|
||||
import java.lang.annotation.*;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.annotation;
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
import com.youlai.boot.common.validator.FieldValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.youlai.boot.framework.web.aspect;
|
||||
package com.youlai.boot.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
@@ -37,6 +37,8 @@ import java.util.concurrent.Executor;
|
||||
@Slf4j
|
||||
public class LogAspect {
|
||||
|
||||
private static final int MAX_ERROR_MSG_LENGTH = 1000;
|
||||
|
||||
private final LogService logService;
|
||||
private final Executor operationLogExecutor;
|
||||
|
||||
@@ -66,10 +68,6 @@ public class LogAspect {
|
||||
throw e;
|
||||
} finally {
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
if (userId == null) {
|
||||
userId = SecurityUtils.getUserId();
|
||||
username = SecurityUtils.getUsername();
|
||||
}
|
||||
try {
|
||||
SysLog entity = buildEntity(logAnnotation, elapsed, failure, userId, username);
|
||||
if (entity != null) {
|
||||
@@ -94,7 +92,6 @@ public class LogAspect {
|
||||
HttpServletRequest req = attrs.getRequest();
|
||||
UserAgent ua = UserAgentUtil.parse(req.getHeader("User-Agent"));
|
||||
String ip = IPUtils.getIpAddr(req);
|
||||
IpRegion region = parseRegion(IPUtils.getRegion(ip));
|
||||
|
||||
LogModuleEnum module = ann.module();
|
||||
ActionTypeEnum action = ann.value();
|
||||
@@ -109,25 +106,34 @@ public class LogAspect {
|
||||
entity.setRequestUri(req.getRequestURI());
|
||||
entity.setRequestMethod(req.getMethod());
|
||||
entity.setIp(ip);
|
||||
entity.setProvince(region.province);
|
||||
entity.setCity(region.city);
|
||||
entity.setDevice(getDevice(ua));
|
||||
entity.setOs(getOs(ua));
|
||||
entity.setBrowser(getBrowser(ua));
|
||||
entity.setStatus(failure == null ? 1 : 0);
|
||||
entity.setErrorMsg(failure == null ? null : failure.getMessage());
|
||||
entity.setErrorMsg(failure == null ? null : truncate(failure.getMessage(), MAX_ERROR_MSG_LENGTH));
|
||||
entity.setExecutionTime((int) Math.min(elapsed, Integer.MAX_VALUE));
|
||||
entity.setCreateTime(LocalDateTime.now());
|
||||
|
||||
IpRegion region = parseRegion(ip);
|
||||
entity.setProvince(region.province);
|
||||
entity.setCity(region.city);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private IpRegion parseRegion(String region) {
|
||||
private IpRegion parseRegion(String ip) {
|
||||
String region = IPUtils.getRegion(ip);
|
||||
if (StrUtil.isBlank(region)) return IpRegion.EMPTY;
|
||||
String[] parts = region.split("\\|");
|
||||
if (parts.length < 4) return IpRegion.EMPTY;
|
||||
return new IpRegion(StrUtil.blankToDefault(parts[2], null), StrUtil.blankToDefault(parts[3], null));
|
||||
}
|
||||
|
||||
private String truncate(String msg, int maxLen) {
|
||||
if (msg == null) return null;
|
||||
return msg.length() <= maxLen ? msg : msg.substring(0, maxLen);
|
||||
}
|
||||
|
||||
private String getOs(UserAgent ua) {
|
||||
return Optional.ofNullable(ua).map(UserAgent::getOs).map(os -> os.getName()).orElse(null);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.youlai.boot.framework.web.aspect;
|
||||
package com.youlai.boot.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.youlai.boot.framework.annotation.RateLimit;
|
||||
import com.youlai.boot.common.annotation.RateLimit;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import com.youlai.boot.framework.web.config.RateLimitProperties;
|
||||
import com.youlai.boot.framework.web.exception.RateLimitException;
|
||||
import com.youlai.boot.config.property.RateLimitProperties;
|
||||
import com.youlai.boot.common.exception.RateLimitException;
|
||||
import com.youlai.boot.framework.web.ratelimit.SlidingWindowScript;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.web.aspect;
|
||||
package com.youlai.boot.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
@@ -7,7 +7,7 @@ import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.framework.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.base;
|
||||
package com.youlai.boot.common.base;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.common.base;
|
||||
|
||||
import com.youlai.boot.framework.annotation.ValidField;
|
||||
import com.youlai.boot.common.annotation.ValidField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@ import com.youlai.boot.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 第三方登录平台类型枚举
|
||||
* 第三方登录平台类型枚举。
|
||||
* <p>
|
||||
* value 作为 MyBatis-Plus 枚举值持久化到数据库。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.17.0
|
||||
*/
|
||||
@Getter
|
||||
public enum SocialPlatformEnum implements IBaseEnum<String> {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package com.youlai.boot.auth.security.exception;
|
||||
package com.youlai.boot.common.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* 需要绑定手机号异常(微信小程序登录未绑定手机号时抛出)。
|
||||
* 微信小程序登录时手机号未绑定异常。
|
||||
* <p>
|
||||
* 携带 openid 和 sessionKey 供前端引导用户完成手机号绑定。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public class MobileNotBoundException extends AuthenticationException {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.web.exception;
|
||||
package com.youlai.boot.common.exception;
|
||||
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.exception;
|
||||
package com.youlai.boot.common.exception;
|
||||
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.common.validator;
|
||||
|
||||
import com.youlai.boot.framework.annotation.ValidField;
|
||||
import com.youlai.boot.common.annotation.ValidField;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.cache;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.youlai.boot.framework.captcha.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import com.youlai.boot.config.property.CaptchaProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.web.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.web.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.apidoc;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.youlai.boot.framework.integration.mail.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import com.youlai.boot.config.property.MailProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.mybatis.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.framework.apidoc;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.web.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.cache;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.cache.autoconfigure.CacheProperties;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.cache;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.youlai.boot.auth.security.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.framework.security.filter.MobileApiSignatureFilter;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.auth.service.CaptchaService;
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
|
||||
import com.youlai.boot.framework.security.service.SecurityUserDetailsService;
|
||||
@@ -23,7 +23,6 @@ import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
@@ -35,6 +34,8 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
|
||||
/**
|
||||
* Spring Security 配置类。
|
||||
* <p>
|
||||
@@ -61,7 +62,7 @@ public class SecurityConfig {
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(requestMatcherRegistry -> {
|
||||
// ASYNC 调度请求(如 SSE emitter.send())不经过外部 HTTP 请求,没有 Authorization 头,直接放行
|
||||
// ASYNC 分发(SSE 超时/完成后的异步 dispatch)无 Authorization 头,直接放行
|
||||
requestMatcherRegistry.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll();
|
||||
String[] ignoreUrls = securityProperties.getIgnoreUrls();
|
||||
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
|
||||
@@ -69,8 +70,8 @@ public class SecurityConfig {
|
||||
}
|
||||
|
||||
// 移动设备专用接口路径(需要设备签名验证,但不需要用户登录)
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/sn/**").permitAll();
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/app/**").permitAll();
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/sn/**").permitAll();
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/app/**").permitAll();
|
||||
// 其他所有请求需登录后访问
|
||||
requestMatcherRegistry.anyRequest().authenticated();
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.youlai.boot.framework.integration.wxma.config;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
|
||||
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
|
||||
import com.youlai.boot.config.property.WxMaProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.job;
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.integration.sms.config;
|
||||
package com.youlai.boot.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.captcha.config;
|
||||
package com.youlai.boot.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.file.config;
|
||||
package com.youlai.boot.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.integration.mail.config;
|
||||
package com.youlai.boot.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.web.config;
|
||||
package com.youlai.boot.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.security.config;
|
||||
package com.youlai.boot.config.property;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.integration.wxma.config;
|
||||
package com.youlai.boot.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.device.controller;
|
||||
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.youlai.boot.device.model.query.DeveloperQuery;
|
||||
import com.youlai.boot.device.model.vo.DeveloperVO;
|
||||
import com.youlai.boot.device.service.DeveloperService;
|
||||
import com.youlai.boot.device.service.DeviceService;
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.device.controller;
|
||||
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.PageResult;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.device.controller;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.youlai.boot.framework.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.config.FilePath;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.framework.annotation.DataPermission;
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceInfo;
|
||||
import com.youlai.boot.device.model.query.DeviceQuery;
|
||||
import com.youlai.boot.device.model.vo.DevicePageVO;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.boot.device.model.entity;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.framework.base.BaseEntity;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.youlai.boot.framework.integration.mail.service;
|
||||
|
||||
import com.youlai.boot.framework.integration.mail.config.MailProperties;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 邮件服务
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/8/17
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MailService {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
/**
|
||||
* 发送简单文本邮件
|
||||
*
|
||||
* @param to 收件人地址
|
||||
* @param subject 邮件主题
|
||||
* @param text 邮件内容
|
||||
*/
|
||||
public void sendMail(String to, String subject, String text) {
|
||||
try {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setFrom(mailProperties.getFrom());
|
||||
message.setTo(to);
|
||||
message.setSubject(subject);
|
||||
message.setText(text);
|
||||
mailSender.send(message);
|
||||
} catch (Exception e) {
|
||||
log.error("发送邮件失败{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送带附件的邮件
|
||||
*
|
||||
* @param to 收件人地址
|
||||
* @param subject 邮件主题
|
||||
* @param text 邮件内容
|
||||
* @param filePath 附件路径
|
||||
*/
|
||||
public void sendMailWithAttachment(String to, String subject, String text, String filePath) {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
try {
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
helper.setFrom(mailProperties.getFrom());
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(text, true); // true 表示支持HTML内容
|
||||
|
||||
FileSystemResource file = new FileSystemResource(new File(filePath));
|
||||
helper.addAttachment(file.getFilename(), file);
|
||||
|
||||
mailSender.send(message);
|
||||
} catch (MessagingException e) {
|
||||
log.error("发送带附件的邮件失败{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.youlai.boot.framework.integration.sms.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "sms.tencent")
|
||||
@Data
|
||||
public class TencentSmsProperties {
|
||||
|
||||
private String secretId;
|
||||
|
||||
private String secretKey;
|
||||
|
||||
private String regionId;
|
||||
|
||||
private String sdkAppId;
|
||||
|
||||
private String signName;
|
||||
|
||||
private Map<String, String> templates;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.youlai.boot.framework.integration.sms.service;
|
||||
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信服务接口层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/8/17
|
||||
*/
|
||||
public interface SmsService {
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*
|
||||
* @param mobile 手机号 13388886666
|
||||
* @param smsType 短信模板 SMS_194640010,模板内容:您的验证码为:${code},请在5分钟内使用
|
||||
* @param templateParams 模板参数 [{"code":"123456"}] ,用于替换短信模板中的变量
|
||||
* @return boolean 是否发送成功
|
||||
*/
|
||||
boolean sendSms(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.youlai.boot.framework.integration.sms.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||
import com.youlai.boot.framework.integration.sms.config.TencentSmsProperties;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Primary
|
||||
public class TencentSmsService implements SmsService {
|
||||
|
||||
private final TencentSmsProperties tencentSmsProperties;
|
||||
|
||||
@Override
|
||||
public boolean sendSms(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams) {
|
||||
try {
|
||||
String templateCode = tencentSmsProperties.getTemplates().get(smsType.getValue());
|
||||
|
||||
Credential cred = new Credential(
|
||||
tencentSmsProperties.getSecretId(),
|
||||
tencentSmsProperties.getSecretKey()
|
||||
);
|
||||
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
httpProfile.setEndpoint("sms.tencentcloudapi.com");
|
||||
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
|
||||
SmsClient client = new SmsClient(cred, tencentSmsProperties.getRegionId(), clientProfile);
|
||||
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
req.setSmsSdkAppId(tencentSmsProperties.getSdkAppId());
|
||||
req.setSignName(tencentSmsProperties.getSignName());
|
||||
req.setTemplateId(templateCode);
|
||||
|
||||
String[] phoneNumberSet = {"+86" + mobile};
|
||||
req.setPhoneNumberSet(phoneNumberSet);
|
||||
|
||||
String[] templateParamSet = templateParams.values().toArray(new String[0]);
|
||||
req.setTemplateParamSet(templateParamSet);
|
||||
|
||||
SendSmsResponse resp = client.SendSms(req);
|
||||
|
||||
log.info("腾讯云短信发送响应: {}", JSONUtil.toJsonStr(resp));
|
||||
|
||||
return "Ok".equals(resp.getSendStatusSet()[0].getCode());
|
||||
|
||||
} catch (TencentCloudSDKException e) {
|
||||
log.error("腾讯云短信发送失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
|
||||
import com.youlai.boot.framework.annotation.DataPermission;
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.common.enums.DataScopeEnum;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import com.youlai.boot.framework.security.model.SecurityUserDetails;
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.Set;
|
||||
* 由 system 模块提供适配器实现,framework 层通过此接口获取角色权限集合,
|
||||
* 不直接依赖 system 模块的 {@code RoleMenuService}。
|
||||
*
|
||||
* @see com.youlai.boot.system.security.adapter.PermissionAdapter
|
||||
* @see com.youlai.boot.system.security.port.PermissionAdapter
|
||||
*/
|
||||
public interface PermissionPort {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.youlai.boot.framework.security.model.SecurityUser;
|
||||
* 由 system 模块提供适配器实现,framework 层通过此接口获取认证数据,
|
||||
* 不直接依赖 system 模块的 {@code UserService} / {@code UserSocialService}。
|
||||
*
|
||||
* @see com.youlai.boot.system.security.adapter.UserAuthenticationAdapter
|
||||
* @see com.youlai.boot.system.security.port.UserAuthenticationAdapter
|
||||
*/
|
||||
public interface UserAuthenticationPort {
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user