refactor: 微信小程序授权登录重构
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<? extends GrantedAuthority> authorities) {
|
||||
public WxMiniAppCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> 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<? extends GrantedAuthority> authorities) {
|
||||
return new WechatAuthenticationToken(principal, authorities);
|
||||
public static WxMiniAppCodeAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new WxMiniAppCodeAuthenticationToken(principal, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,4 +66,4 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken {
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证通过
|
||||
*
|
||||
* @param principal 用户信息
|
||||
* @param authorities 授权信息
|
||||
* @return 认证通过的Token
|
||||
*/
|
||||
public static WxMiniAppPhoneAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<AuthenticationToken> loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) {
|
||||
AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO);
|
||||
return Result.success(token);
|
||||
}
|
||||
|
||||
@Operation(summary = "微信小程序手机号登录")
|
||||
@PostMapping("/wx/miniapp/phone-login")
|
||||
public Result<AuthenticationToken> loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) {
|
||||
AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDTO);
|
||||
return Result.success(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -165,18 +165,18 @@ public interface UserService extends IService<User> {
|
||||
/**
|
||||
* 根据 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<User> {
|
||||
*/
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<UserMapper, User> implements UserService {
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
@@ -207,14 +210,17 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> 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<String> roles = userAuthCredentials.getRoles();
|
||||
// 获取最大范围的数据权限
|
||||
@@ -225,13 +231,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> 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<String> roles = userAuthCredentials.getRoles();
|
||||
@@ -242,34 +251,135 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return userAuthCredentials;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据微信 OpenID 注册或绑定用户
|
||||
* <p>
|
||||
* TODO 根据手机号绑定用户
|
||||
* 注册或绑定微信用户
|
||||
*
|
||||
* @param openId 微信 OpenID
|
||||
* @param openId 微信OpenID
|
||||
* @return 是否成功
|
||||
*/
|
||||
@Override
|
||||
public void registerOrBindWechatUser(String openId) {
|
||||
User user = this.getOne(
|
||||
new LambdaQueryWrapper<User>().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<User>()
|
||||
.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<User>()
|
||||
.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<User>()
|
||||
.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<User>()
|
||||
.eq(User::getId, userId)
|
||||
.set(User::getOpenid, openId)
|
||||
.set(User::getUpdateTime, LocalDateTime.now())
|
||||
);
|
||||
return updated ;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user