diff --git a/src/main/java/com/youlai/boot/config/SecurityConfig.java b/src/main/java/com/youlai/boot/config/SecurityConfig.java index fef74b6f..6790d4d5 100644 --- a/src/main/java/com/youlai/boot/config/SecurityConfig.java +++ b/src/main/java/com/youlai/boot/config/SecurityConfig.java @@ -8,7 +8,8 @@ import com.youlai.boot.core.filter.RateLimiterFilter; import com.youlai.boot.core.security.exception.MyAccessDeniedHandler; import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint; import com.youlai.boot.core.security.extension.sms.SmsAuthenticationProvider; -import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationProvider; +import com.youlai.boot.core.security.extension.wx.WxMiniAppCodeAuthenticationProvider; +import com.youlai.boot.core.security.extension.wx.WxMiniAppPhoneAuthenticationProvider; import com.youlai.boot.core.security.filter.CaptchaValidationFilter; import com.youlai.boot.core.security.filter.TokenAuthenticationFilter; import com.youlai.boot.core.security.token.TokenManager; @@ -125,13 +126,20 @@ public class SecurityConfig { } /** - * 微信认证 Provider + * 微信小程序Code认证Provider */ @Bean - public WechatAuthenticationProvider weChatAuthenticationProvider() { - return new WechatAuthenticationProvider(userService, wxMaService); + public WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider() { + return new WxMiniAppCodeAuthenticationProvider(userService, wxMaService); } + /** + * 微信小程序手机号认证Provider + */ + @Bean + public WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider() { + return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService); + } /** * 短信验证码认证 Provider @@ -147,12 +155,14 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager( DaoAuthenticationProvider daoAuthenticationProvider, - WechatAuthenticationProvider weChatAuthenticationProvider, + WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider, + WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider, SmsAuthenticationProvider smsAuthenticationProvider ) { return new ProviderManager( daoAuthenticationProvider, - weChatAuthenticationProvider, + wxMiniAppCodeAuthenticationProvider, + wxMiniAppPhoneAuthenticationProvider, smsAuthenticationProvider ); } diff --git a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationProvider.java b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppCodeAuthenticationProvider.java similarity index 81% rename from src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationProvider.java rename to src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppCodeAuthenticationProvider.java index 4c8b1c61..94a53874 100644 --- a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppCodeAuthenticationProvider.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.extension.wechat; +package com.youlai.boot.core.security.extension.wx; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; @@ -18,20 +18,19 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; /** - * 微信认证 Provider + * 微信小程序Code认证Provider * - * @author Ray.Hao - * @since 2.17.0 + * @author 有来技术团队 + * @since 2.0.0 */ @Slf4j -public class WechatAuthenticationProvider implements AuthenticationProvider { +public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvider { private final UserService userService; - private final WxMaService wxMaService; - public WechatAuthenticationProvider(UserService userService, WxMaService wxMaService) { + public WxMiniAppCodeAuthenticationProvider(UserService userService, WxMaService wxMaService) { this.userService = userService; this.wxMaService = wxMaService; } @@ -66,7 +65,7 @@ public class WechatAuthenticationProvider implements AuthenticationProvider { UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByOpenId(openId); if (userAuthCredentials == null) { - // TODO: 用户不存在则注册,这里需要获取用户手机号并与现有用户绑定 + // 用户不存在则注册 userService.registerOrBindWechatUser(openId); // 再次查询用户信息,确保用户注册成功 @@ -80,13 +79,12 @@ public class WechatAuthenticationProvider implements AuthenticationProvider { if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) { throw new DisabledException("用户已被禁用"); } - // 这里因为已经根据 code 从微信小程序获取到 openid 不需要再经过系统认证,所以直接生成 // 构建认证后的用户详情信息 SysUserDetails userDetails = new SysUserDetails(userAuthCredentials); - // 创建已认证的 WeChatAuthenticationToken - return WechatAuthenticationToken.authenticated( + // 创建已认证的Token + return WxMiniAppCodeAuthenticationToken.authenticated( userDetails, userDetails.getAuthorities() ); @@ -94,6 +92,6 @@ public class WechatAuthenticationProvider implements AuthenticationProvider { @Override public boolean supports(Class authentication) { - return WechatAuthenticationToken.class.isAssignableFrom(authentication); + return WxMiniAppCodeAuthenticationToken.class.isAssignableFrom(authentication); } -} +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationToken.java b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppCodeAuthenticationToken.java similarity index 58% rename from src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationToken.java rename to src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppCodeAuthenticationToken.java index fc8bb2be..6ae8dce1 100644 --- a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationToken.java +++ b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppCodeAuthenticationToken.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.extension.wechat; +package com.youlai.boot.core.security.extension.wx; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -7,22 +7,22 @@ import java.io.Serial; import java.util.Collection; /** - * 微信认证 Token + * 微信小程序Code认证Token * - * @author Ray.Hao - * @since 2024/12/2 + * @author 有来技术团队 + * @since 2.0.0 */ -public class WechatAuthenticationToken extends AbstractAuthenticationToken { +public class WxMiniAppCodeAuthenticationToken extends AbstractAuthenticationToken { @Serial private static final long serialVersionUID = 621L; private final Object principal; /** - * 微信认证 Token (未认证) + * 微信小程序Code认证Token (未认证) * - * @param principal 微信用户信息 + * @param principal 微信code */ - public WechatAuthenticationToken(Object principal) { + public WxMiniAppCodeAuthenticationToken(Object principal) { // 没有授权信息时,设置为 null super(null); this.principal = principal; @@ -32,12 +32,12 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken { /** - * 微信认证 Token (已认证) + * 微信小程序Code认证Token (已认证) * * @param principal 微信用户信息 * @param authorities 授权信息 */ - public WechatAuthenticationToken(Object principal, Collection authorities) { + public WxMiniAppCodeAuthenticationToken(Object principal, Collection authorities) { super(authorities); this.principal = principal; // 认证通过 @@ -50,10 +50,10 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken { * * @param principal 微信用户信息 * @param authorities 授权信息 - * @return + * @return 已认证的Token */ - public static WechatAuthenticationToken authenticated(Object principal, Collection authorities) { - return new WechatAuthenticationToken(principal, authorities); + public static WxMiniAppCodeAuthenticationToken authenticated(Object principal, Collection authorities) { + return new WxMiniAppCodeAuthenticationToken(principal, authorities); } @Override @@ -66,4 +66,4 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken { public Object getPrincipal() { return this.principal; } -} +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppPhoneAuthenticationProvider.java b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppPhoneAuthenticationProvider.java new file mode 100644 index 00000000..c38f1e3f --- /dev/null +++ b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppPhoneAuthenticationProvider.java @@ -0,0 +1,114 @@ +package com.youlai.boot.core.security.extension.wx; + +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.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.youlai.boot.core.security.model.SysUserDetails; +import com.youlai.boot.core.security.model.UserAuthCredentials; +import com.youlai.boot.system.service.UserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/** + * 微信小程序手机号认证Provider + * + * @author 有来技术团队 + * @since 2.0.0 + */ +@Slf4j +public class WxMiniAppPhoneAuthenticationProvider implements AuthenticationProvider { + + private final UserService userService; + private final WxMaService wxMaService; + + public WxMiniAppPhoneAuthenticationProvider(UserService userService, WxMaService wxMaService) { + this.userService = userService; + this.wxMaService = wxMaService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + WxMiniAppPhoneAuthenticationToken authenticationToken = (WxMiniAppPhoneAuthenticationToken) authentication; + String code = (String) authenticationToken.getPrincipal(); + String encryptedData = authenticationToken.getEncryptedData(); + String iv = authenticationToken.getIv(); + + // 1. 通过code获取session_key + WxMaJscode2SessionResult sessionInfo; + try { + sessionInfo = wxMaService.getUserService().getSessionInfo(code); + } catch (WxErrorException e) { + log.error("获取微信session_key失败", e); + throw new CredentialsExpiredException("微信登录code无效或已过期"); + } + + String sessionKey = sessionInfo.getSessionKey(); + String openId = sessionInfo.getOpenid(); + + if (StrUtil.isBlank(sessionKey) || StrUtil.isBlank(openId)) { + throw new CredentialsExpiredException("获取微信会话信息失败"); + } + + // 2. 解密手机号信息 + WxMaPhoneNumberInfo phoneNumberInfo; + try { + if (StrUtil.isNotBlank(encryptedData) && StrUtil.isNotBlank(iv)) { + phoneNumberInfo = wxMaService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv); + } else { + throw new IllegalArgumentException("缺少手机号加密数据"); + } + } catch (Exception e) { + log.error("解密微信手机号失败", e); + throw new CredentialsExpiredException("解密手机号信息失败"); + } + + if (phoneNumberInfo == null || StrUtil.isBlank(phoneNumberInfo.getPhoneNumber())) { + throw new CredentialsExpiredException("获取手机号失败"); + } + + String phoneNumber = phoneNumberInfo.getPhoneNumber(); + + // 3. 根据手机号查询用户,不存在则创建新用户 + UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber); + + if (userAuthCredentials == null) { + // 用户不存在,注册新用户 + boolean registered = userService.registerUserByMobileAndOpenId(phoneNumber, openId); + if (!registered) { + throw new UsernameNotFoundException("用户注册失败"); + } + // 重新获取用户信息 + userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber); + } else { + // 用户存在,绑定openId(如果未绑定) + userService.bindUserOpenId(userAuthCredentials.getUserId(), openId); + } + + // 4. 检查用户状态 + if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) { + throw new DisabledException("用户已被禁用"); + } + + // 5. 构建认证后的用户详情 + SysUserDetails userDetails = new SysUserDetails(userAuthCredentials); + + // 6. 创建已认证的Token + return WxMiniAppPhoneAuthenticationToken.authenticated( + userDetails, + userDetails.getAuthorities() + ); + } + + @Override + public boolean supports(Class authentication) { + return WxMiniAppPhoneAuthenticationToken.class.isAssignableFrom(authentication); + } +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppPhoneAuthenticationToken.java b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppPhoneAuthenticationToken.java new file mode 100644 index 00000000..5df01228 --- /dev/null +++ b/src/main/java/com/youlai/boot/core/security/extension/wx/WxMiniAppPhoneAuthenticationToken.java @@ -0,0 +1,89 @@ +package com.youlai.boot.core.security.extension.wx; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.io.Serial; +import java.util.Collection; + +/** + * 微信小程序手机号认证Token + * + * @author 有来技术团队 + * @since 2.0.0 + */ +public class WxMiniAppPhoneAuthenticationToken extends AbstractAuthenticationToken { + @Serial + private static final long serialVersionUID = 622L; + + private final Object principal; // code + private String encryptedData; + private String iv; + + /** + * 微信小程序手机号认证Token (未认证) + * + * @param code 微信登录code + * @param encryptedData 加密数据 + * @param iv 初始向量 + */ + public WxMiniAppPhoneAuthenticationToken(String code, String encryptedData, String iv) { + super(null); + this.principal = code; + this.encryptedData = encryptedData; + this.iv = iv; + this.setAuthenticated(false); + } + + /** + * 微信小程序手机号认证Token (已认证) + * + * @param principal 用户信息 + * @param authorities 授权信息 + */ + public WxMiniAppPhoneAuthenticationToken(Object principal, Collection authorities) { + super(authorities); + this.principal = principal; + super.setAuthenticated(true); + } + + /** + * 认证通过 + * + * @param principal 用户信息 + * @param authorities 授权信息 + * @return 认证通过的Token + */ + public static WxMiniAppPhoneAuthenticationToken authenticated(Object principal, Collection authorities) { + return new WxMiniAppPhoneAuthenticationToken(principal, authorities); + } + + @Override + public Object getCredentials() { + // 微信小程序手机号认证不需要密码 + return null; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + /** + * 获取加密数据 + * + * @return 加密数据 + */ + public String getEncryptedData() { + return encryptedData; + } + + /** + * 获取初始向量 + * + * @return 初始向量 + */ + public String getIv() { + return iv; + } +} diff --git a/src/main/java/com/youlai/boot/shared/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/shared/auth/controller/AuthController.java index c306bdee..327cb8b9 100644 --- a/src/main/java/com/youlai/boot/shared/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/shared/auth/controller/AuthController.java @@ -4,15 +4,19 @@ import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; import com.youlai.boot.shared.auth.service.AuthService; import com.youlai.boot.shared.auth.model.CaptchaInfo; +import com.youlai.boot.shared.auth.model.dto.WxMiniAppCodeLoginDTO; +import com.youlai.boot.shared.auth.model.dto.WxMiniAppPhoneLoginDTO; import com.youlai.boot.core.security.model.AuthenticationToken; 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; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; + /** * 认证控制层 * @@ -92,4 +96,18 @@ public class AuthController { AuthenticationToken loginResult = authService.loginBySms(mobile, code); return Result.success(loginResult); } + + @Operation(summary = "微信小程序Code登录") + @PostMapping("/wx/miniapp/code-login") + public Result loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) { + AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO); + return Result.success(token); + } + + @Operation(summary = "微信小程序手机号登录") + @PostMapping("/wx/miniapp/phone-login") + public Result loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) { + AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDTO); + return Result.success(token); + } } diff --git a/src/main/java/com/youlai/boot/shared/auth/model/dto/WxMiniAppCodeLoginDTO.java b/src/main/java/com/youlai/boot/shared/auth/model/dto/WxMiniAppCodeLoginDTO.java new file mode 100644 index 00000000..dcb516e4 --- /dev/null +++ b/src/main/java/com/youlai/boot/shared/auth/model/dto/WxMiniAppCodeLoginDTO.java @@ -0,0 +1,22 @@ +package com.youlai.boot.shared.auth.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 微信小程序Code登录请求参数 + * + * @author 有来技术团队 + * @since 2.0.0 + */ +@Schema(description = "微信小程序Code登录请求参数") +@Data +public class WxMiniAppCodeLoginDTO { + + @Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "code不能为空") + private String code; + +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/shared/auth/model/dto/WxMiniAppPhoneLoginDTO.java b/src/main/java/com/youlai/boot/shared/auth/model/dto/WxMiniAppPhoneLoginDTO.java new file mode 100644 index 00000000..8b8d052e --- /dev/null +++ b/src/main/java/com/youlai/boot/shared/auth/model/dto/WxMiniAppPhoneLoginDTO.java @@ -0,0 +1,28 @@ +package com.youlai.boot.shared.auth.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 微信小程序手机号登录请求参数 + * + * @author 有来技术团队 + * @since 2.0.0 + */ +@Schema(description = "微信小程序手机号登录请求参数") +@Data +public class WxMiniAppPhoneLoginDTO { + + @Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "code不能为空") + private String code; + + @Schema(description = "包括敏感数据在内的完整用户信息的加密数据") + private String encryptedData; + + @Schema(description = "加密算法的初始向量") + private String iv; + +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/shared/auth/service/AuthService.java b/src/main/java/com/youlai/boot/shared/auth/service/AuthService.java index 1243403f..2d183079 100644 --- a/src/main/java/com/youlai/boot/shared/auth/service/AuthService.java +++ b/src/main/java/com/youlai/boot/shared/auth/service/AuthService.java @@ -2,6 +2,8 @@ package com.youlai.boot.shared.auth.service; import com.youlai.boot.shared.auth.model.CaptchaInfo; import com.youlai.boot.core.security.model.AuthenticationToken; +import com.youlai.boot.shared.auth.model.dto.WxMiniAppCodeLoginDTO; +import com.youlai.boot.shared.auth.model.dto.WxMiniAppPhoneLoginDTO; /** * 认证服务接口 @@ -48,6 +50,22 @@ public interface AuthService { */ AuthenticationToken loginByWechat(String code); + /** + * 微信小程序Code登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO); + + /** + * 微信小程序手机号登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO); + /** * 发送短信验证码 * diff --git a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java index d44b0c18..3db473d9 100644 --- a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java @@ -11,11 +11,12 @@ import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.config.property.CaptchaProperties; import com.youlai.boot.core.security.extension.sms.SmsAuthenticationToken; -import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationToken; import com.youlai.boot.core.security.util.SecurityUtils; import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum; import com.youlai.boot.core.security.model.AuthenticationToken; import com.youlai.boot.shared.auth.model.CaptchaInfo; +import com.youlai.boot.shared.auth.model.dto.WxMiniAppCodeLoginDTO; +import com.youlai.boot.shared.auth.model.dto.WxMiniAppPhoneLoginDTO; import com.youlai.boot.shared.auth.service.AuthService; import com.youlai.boot.core.security.token.TokenManager; import com.youlai.boot.shared.sms.enums.SmsTypeEnum; @@ -28,6 +29,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import com.youlai.boot.core.security.extension.wx.WxMiniAppCodeAuthenticationToken; +import com.youlai.boot.core.security.extension.wx.WxMiniAppPhoneAuthenticationToken; import java.awt.*; import java.util.HashMap; @@ -87,16 +90,16 @@ public class AuthServiceImpl implements AuthService { @Override public AuthenticationToken loginByWechat(String code) { // 1. 创建用户微信认证的令牌(未认证) - WechatAuthenticationToken wechatAuthenticationToken = new WechatAuthenticationToken(code); + WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(code); // 2. 执行认证(认证中) - Authentication authentication = authenticationManager.authenticate(wechatAuthenticationToken); + Authentication authentication = authenticationManager.authenticate(authenticationToken); // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) - AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); + AuthenticationToken token = tokenManager.generateToken(authentication); SecurityContextHolder.getContext().setAuthentication(authentication); - return authenticationToken; + return token; } /** @@ -227,5 +230,50 @@ public class AuthServiceImpl implements AuthService { return tokenManager.refreshToken(refreshToken); } + /** + * 微信小程序Code登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + @Override + public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO) { + // 1. 创建微信小程序认证令牌(未认证) + WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDTO.getCode()); + + // 2. 执行认证(认证中) + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) + AuthenticationToken token = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + + return token; + } + + /** + * 微信小程序手机号登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + @Override + public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO) { + // 创建微信小程序手机号认证Token + WxMiniAppPhoneAuthenticationToken authenticationToken = new WxMiniAppPhoneAuthenticationToken( + loginDTO.getCode(), + loginDTO.getEncryptedData(), + loginDTO.getIv() + ); + + // 执行认证 + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 认证成功后生成JWT令牌,并存入Security上下文 + AuthenticationToken token = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + + return token; + } } diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index 1ddd2c12..804569e3 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -165,18 +165,18 @@ public interface UserService extends IService { /** * 根据 openid 获取用户认证信息 * - * @param username 用户名 + * @param openId 用户名 * @return {@link UserAuthCredentials} */ - UserAuthCredentials getAuthCredentialsByOpenId(String username); + UserAuthCredentials getAuthCredentialsByOpenId(String openId); /** * 根据微信 OpenID 注册或绑定用户 * * @param openId 微信 OpenID */ - void registerOrBindWechatUser(String openId); + boolean registerOrBindWechatUser(String openId); /** * 根据手机号获取用户认证信息 @@ -186,5 +186,22 @@ public interface UserService extends IService { */ UserAuthCredentials getAuthCredentialsByMobile(String mobile); + /** + * 根据手机号和OpenID注册用户 + * + * @param mobile 手机号 + * @param openId 微信OpenID + * @return 是否成功 + */ + boolean registerUserByMobileAndOpenId(String mobile, String openId); + + /** + * 绑定用户微信OpenID + * + * @param userId 用户ID + * @param openId 微信OpenID + * @return 是否成功 + */ + boolean bindUserOpenId(Long userId, String openId); } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index d950ffa9..3bec2db1 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -34,11 +34,13 @@ import com.youlai.boot.system.model.vo.UserPageVO; import com.youlai.boot.system.model.vo.UserProfileVO; import com.youlai.boot.system.service.*; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -51,6 +53,7 @@ import java.util.stream.Collectors; */ @Service @RequiredArgsConstructor +@Slf4j public class UserServiceImpl extends ServiceImpl implements UserService { private final PasswordEncoder passwordEncoder; @@ -207,14 +210,17 @@ public class UserServiceImpl extends ServiceImpl implements Us } /** - * 根据 openid 获取用户认证信息 + * 根据OpenID获取用户认证信息 * - * @param openid 微信 OpenId - * @return {@link UserAuthCredentials} + * @param openId 微信OpenID + * @return 用户认证信息 */ @Override - public UserAuthCredentials getAuthCredentialsByOpenId(String openid) { - UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByOpenId(openid); + public UserAuthCredentials getAuthCredentialsByOpenId(String openId) { + if (StrUtil.isBlank(openId)) { + return null; + } + UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByOpenId(openId); if (userAuthCredentials != null) { Set roles = userAuthCredentials.getRoles(); // 获取最大范围的数据权限 @@ -225,13 +231,16 @@ public class UserServiceImpl extends ServiceImpl implements Us } /** - * 根据手机号获取用户认证凭证信息 + * 根据手机号获取用户认证信息 * * @param mobile 手机号 - * @return {@link UserAuthCredentials} + * @return 用户认证信息 */ @Override public UserAuthCredentials getAuthCredentialsByMobile(String mobile) { + if (StrUtil.isBlank(mobile)) { + return null; + } UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByMobile(mobile); if (userAuthCredentials != null) { Set roles = userAuthCredentials.getRoles(); @@ -242,34 +251,135 @@ public class UserServiceImpl extends ServiceImpl implements Us return userAuthCredentials; } - /** - * 根据微信 OpenID 注册或绑定用户 - *

- * TODO 根据手机号绑定用户 + * 注册或绑定微信用户 * - * @param openId 微信 OpenID + * @param openId 微信OpenID + * @return 是否成功 */ @Override - public void registerOrBindWechatUser(String openId) { - User user = this.getOne( - new LambdaQueryWrapper().eq(User::getOpenid, openId) - ); - if (user == null) { - user = new User(); - user.setNickname("微信用户"); // 默认昵称 - user.setUsername(openId); // TODO 后续替换为手机号 - user.setOpenid(openId); - user.setGender(0); // 保密 - user.setUpdateBy(SecurityUtils.getUserId()); - user.setPassword(SystemConstants.DEFAULT_PASSWORD); - this.save(user); - // 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色 - UserRole userRole = new UserRole(); - userRole.setUserId(user.getId()); - userRole.setRoleId(1L); // TODO 系统管理员 - userRoleService.save(userRole); + @Transactional(rollbackFor = Exception.class) + public boolean registerOrBindWechatUser(String openId) { + if (StrUtil.isBlank(openId)) { + return false; } + + // 查询是否已存在该openId的用户 + User existUser = this.getOne( + new LambdaQueryWrapper() + .eq(User::getOpenid, openId) + ); + + if (existUser != null) { + // 用户已存在,不需要注册 + return true; + } + + // 创建新用户 + User newUser = new User(); + newUser.setNickname("微信用户"); // 默认昵称 + newUser.setUsername(openId); // TODO 后续替换为手机号 + newUser.setOpenid(openId); + newUser.setGender(0); // 保密 + newUser.setUpdateBy(SecurityUtils.getUserId()); + newUser.setPassword(SystemConstants.DEFAULT_PASSWORD); + newUser.setCreateTime(LocalDateTime.now()); + newUser.setUpdateTime(LocalDateTime.now()); + this.save(newUser); + // 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色 + UserRole userRole = new UserRole(); + userRole.setUserId(newUser.getId()); + userRole.setRoleId(1L); // TODO 系统管理员 + userRoleService.save(userRole); + return true; + } + + /** + * 根据手机号和OpenID注册用户 + * + * @param mobile 手机号 + * @param openId 微信OpenID + * @return 是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean registerUserByMobileAndOpenId(String mobile, String openId) { + if (StrUtil.isBlank(mobile) || StrUtil.isBlank(openId)) { + return false; + } + + // 先查询是否已存在手机号对应的用户 + User existingUser = this.getOne( + new LambdaQueryWrapper() + .eq(User::getMobile, mobile) + ); + + if (existingUser != null) { + // 如果存在用户但没绑定openId,则绑定openId + if (StrUtil.isBlank(existingUser.getOpenid())) { + return bindUserOpenId(existingUser.getId(), openId); + } + // 如果已经绑定了其他openId,则判断是否需要更新 + else if (!openId.equals(existingUser.getOpenid())) { + return bindUserOpenId(existingUser.getId(), openId); + } + // 如果已经绑定了相同的openId,则不需要任何操作 + return true; + } + + // 不存在用户,创建新用户 + User newUser = new User(); + newUser.setMobile(mobile); + newUser.setOpenid(openId); + newUser.setUsername(mobile); // 使用手机号作为用户名 + newUser.setNickname("微信用户_" + mobile.substring(mobile.length() - 4)); // 使用手机号后4位作为昵称 + newUser.setPassword(SystemConstants.DEFAULT_PASSWORD); // 使用加密的openId作为初始密码 + newUser.setGender(0); // 保密 + newUser.setCreateTime(LocalDateTime.now()); + newUser.setUpdateTime(LocalDateTime.now()); + this.save(newUser); + // 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色 + UserRole userRole = new UserRole(); + userRole.setUserId(newUser.getId()); + userRole.setRoleId(1L); // TODO 系统管理员 + userRoleService.save(userRole); + return true; + } + + /** + * 绑定用户微信OpenID + * + * @param userId 用户ID + * @param openId 微信OpenID + * @return 是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean bindUserOpenId(Long userId, String openId) { + if (userId == null || StrUtil.isBlank(openId)) { + return false; + } + + // 检查是否已有其他用户绑定了此openId + User existingUser = this.getOne( + new LambdaQueryWrapper() + .eq(User::getOpenid, openId) + .ne(User::getId, userId) + ); + + if (existingUser != null) { + log.warn("OpenID {} 已被用户 {} 绑定,无法为用户 {} 绑定", openId, existingUser.getId(), userId); + return false; + } + + // 更新用户openId + boolean updated = this.update( + new LambdaUpdateWrapper() + .eq(User::getId, userId) + .set(User::getOpenid, openId) + .set(User::getUpdateTime, LocalDateTime.now()) + ); + return updated ; } /**