feat: 新增微信小程序登录功能及第三方账号绑定表
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.vo.WechatLoginResult;
|
||||
import com.youlai.boot.auth.model.dto.LoginRequest;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
@@ -67,6 +68,39 @@ public class AuthController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "微信小程序登录(个人小程序)")
|
||||
@PostMapping("/wechat-miniapp/login")
|
||||
@Log(value = "微信小程序登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<WechatLoginResult> loginByWechatMini(
|
||||
@Parameter(description = "微信登录code", example = "xxx") @RequestParam String code
|
||||
) {
|
||||
WechatLoginResult result = authService.loginByWechatMini(code);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "微信小程序一键登录(企业小程序)")
|
||||
@PostMapping("/wechat-miniapp/phone-login")
|
||||
@Log(value = "微信小程序一键登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<AuthenticationToken> loginByWechatMiniWithPhone(
|
||||
@Parameter(description = "微信登录code", example = "xxx") @RequestParam String loginCode,
|
||||
@Parameter(description = "手机号授权code", example = "xxx") @RequestParam String phoneCode
|
||||
) {
|
||||
AuthenticationToken result = authService.wechatMiniLoginWithPhone(loginCode, phoneCode);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "微信小程序绑定手机号")
|
||||
@PostMapping("/wechat-miniapp/bind-mobile")
|
||||
@Log(value = "微信小程序绑定手机号", module = LogModuleEnum.LOGIN)
|
||||
public Result<AuthenticationToken> bindMobileForWechatMini(
|
||||
@Parameter(description = "微信openid") @RequestParam String openid,
|
||||
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile,
|
||||
@Parameter(description = "短信验证码", example = "1234") @RequestParam String code
|
||||
) {
|
||||
AuthenticationToken result = authService.bindMobileForWechatMini(openid, mobile, code);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "退出登录")
|
||||
@DeleteMapping("/logout")
|
||||
@Log(value = "退出登录", module = LogModuleEnum.LOGIN)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.youlai.boot.auth.model.vo;
|
||||
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 微信小程序登录结果
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "微信小程序登录结果")
|
||||
public class WechatLoginResult {
|
||||
|
||||
@Schema(description = "是否新用户")
|
||||
private Boolean isNewUser;
|
||||
|
||||
@Schema(description = "是否需要绑定手机号")
|
||||
private Boolean needBindMobile;
|
||||
|
||||
@Schema(description = "微信openid(绑定手机号时需要)")
|
||||
private String openid;
|
||||
|
||||
@Schema(description = "访问令牌")
|
||||
private String accessToken;
|
||||
|
||||
@Schema(description = "刷新令牌")
|
||||
private String refreshToken;
|
||||
|
||||
@Schema(description = "令牌类型")
|
||||
private String tokenType;
|
||||
|
||||
@Schema(description = "过期时间(秒)")
|
||||
private Long expiresIn;
|
||||
|
||||
/**
|
||||
* 创建需要绑定手机号的结果
|
||||
*/
|
||||
public static WechatLoginResult needBindMobile(String openid) {
|
||||
WechatLoginResult result = new WechatLoginResult();
|
||||
result.setIsNewUser(true);
|
||||
result.setNeedBindMobile(true);
|
||||
result.setOpenid(openid);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建登录成功的结果
|
||||
*/
|
||||
public static WechatLoginResult success(AuthenticationToken token) {
|
||||
WechatLoginResult result = new WechatLoginResult();
|
||||
result.setIsNewUser(false);
|
||||
result.setNeedBindMobile(false);
|
||||
result.setAccessToken(token.getAccessToken());
|
||||
result.setRefreshToken(token.getRefreshToken());
|
||||
result.setTokenType(token.getTokenType());
|
||||
result.setExpiresIn(token.getExpiresIn());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.vo.WechatLoginResult;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
|
||||
/**
|
||||
@@ -55,4 +56,31 @@ public interface AuthService {
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthenticationToken loginBySms(String mobile, String code);
|
||||
|
||||
/**
|
||||
* 微信小程序登录(个人小程序)
|
||||
*
|
||||
* @param code 微信登录code
|
||||
* @return 登录结果
|
||||
*/
|
||||
WechatLoginResult loginByWechatMini(String code);
|
||||
|
||||
/**
|
||||
* 微信小程序一键登录(企业小程序)
|
||||
*
|
||||
* @param loginCode 微信登录code
|
||||
* @param phoneCode 手机号授权code
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthenticationToken wechatMiniLoginWithPhone(String loginCode, String phoneCode);
|
||||
|
||||
/**
|
||||
* 微信小程序绑定手机号
|
||||
*
|
||||
* @param openid 微信openid
|
||||
* @param mobile 手机号
|
||||
* @param smsCode 短信验证码
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthenticationToken bindMobileForWechatMini(String openid, String mobile, String smsCode);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
package com.youlai.boot.auth.service.impl;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.vo.WechatLoginResult;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.config.property.CaptchaProperties;
|
||||
import com.youlai.boot.support.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.support.sms.service.SmsService;
|
||||
import com.youlai.boot.security.exception.NeedBindMobileException;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.model.WechatMiniAuthenticationToken;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.support.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.support.sms.service.SmsService;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.User;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -24,8 +33,10 @@ 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 org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.awt.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -50,6 +61,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final SmsService smsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final UserSocialService userSocialService;
|
||||
private final UserService userService;
|
||||
private final WxMaService wxMaService;
|
||||
|
||||
/**
|
||||
* 用户名密码登录
|
||||
@@ -198,4 +212,150 @@ public class AuthServiceImpl implements AuthService {
|
||||
return tokenManager.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序登录(个人小程序)
|
||||
*/
|
||||
@Override
|
||||
public WechatLoginResult loginByWechatMini(String code) {
|
||||
WechatMiniAuthenticationToken token = new WechatMiniAuthenticationToken(code);
|
||||
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(token);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return WechatLoginResult.success(authToken);
|
||||
} catch (NeedBindMobileException e) {
|
||||
return WechatLoginResult.needBindMobile(e.getOpenid());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序一键登录(企业小程序)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken wechatMiniLoginWithPhone(String loginCode, String phoneCode) {
|
||||
// 1. 用 loginCode 换取 openid
|
||||
cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult session;
|
||||
try {
|
||||
session = wxMaService.jsCode2SessionInfo(loginCode);
|
||||
} catch (Exception e) {
|
||||
log.error("微信小程序一键登录失败:获取openid异常,loginCode={}", loginCode, e);
|
||||
throw new IllegalArgumentException("微信登录失败:" + e.getMessage());
|
||||
}
|
||||
String openid = session.getOpenid();
|
||||
|
||||
// 2. 用 phoneCode 换取手机号
|
||||
cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo phoneInfo;
|
||||
try {
|
||||
phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode);
|
||||
} catch (Exception e) {
|
||||
log.error("微信小程序一键登录失败:获取手机号异常,phoneCode={}", phoneCode, e);
|
||||
throw new IllegalArgumentException("获取手机号失败:" + e.getMessage());
|
||||
}
|
||||
String mobile = phoneInfo.getPhoneNumber();
|
||||
|
||||
log.info("微信小程序一键登录:openid={}, mobile={}", openid, mobile);
|
||||
|
||||
// 3. 查询或创建用户
|
||||
User user = userService.lambdaQuery()
|
||||
.eq(User::getMobile, mobile)
|
||||
.one();
|
||||
|
||||
if (user == null) {
|
||||
user = new User();
|
||||
user.setMobile(mobile);
|
||||
user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8));
|
||||
user.setNickname(phoneInfo.getNickName() != null ? phoneInfo.getNickName() : "微信用户");
|
||||
user.setAvatar(phoneInfo.getAvatarUrl());
|
||||
user.setStatus(1);
|
||||
user.setIsDeleted(0);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
userService.save(user);
|
||||
log.info("微信小程序一键登录:创建新用户,mobile={}, userId={}", mobile, user.getId());
|
||||
}
|
||||
|
||||
// 4. 绑定 openid
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
openid,
|
||||
session.getUnionid(),
|
||||
user.getNickname(),
|
||||
user.getAvatar(),
|
||||
session.getSessionKey()
|
||||
);
|
||||
|
||||
// 5. 生成 token
|
||||
SysUserDetails userDetails = new SysUserDetails(userService.getAuthInfoByMobile(mobile));
|
||||
AuthenticationToken authToken = tokenManager.generateToken(userDetails);
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
|
||||
);
|
||||
|
||||
log.info("微信小程序一键登录成功:mobile={}, openid={}", mobile, openid);
|
||||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序绑定手机号
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken bindMobileForWechatMini(String openid, String mobile, String smsCode) {
|
||||
// 1. 验证短信验证码
|
||||
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);
|
||||
|
||||
// 2. 查询或创建用户
|
||||
User user = userService.lambdaQuery()
|
||||
.eq(User::getMobile, mobile)
|
||||
.one();
|
||||
|
||||
if (user == null) {
|
||||
// 创建新用户
|
||||
user = new User();
|
||||
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);
|
||||
log.info("微信小程序绑定手机号:创建新用户,mobile={}, userId={}", mobile, user.getId());
|
||||
}
|
||||
|
||||
// 3. 绑定第三方账号
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
openid,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// 4. 生成token
|
||||
SysUserDetails userDetails = new SysUserDetails(userService.getAuthInfoByMobile(mobile));
|
||||
AuthenticationToken authToken = tokenManager.generateToken(userDetails);
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
|
||||
);
|
||||
|
||||
log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid);
|
||||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
@@ -9,9 +10,11 @@ import com.youlai.boot.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.security.handler.MyAccessDeniedHandler;
|
||||
import com.youlai.boot.security.handler.MyAuthenticationEntryPoint;
|
||||
import com.youlai.boot.security.provider.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.security.provider.WechatMiniAuthenticationProvider;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.system.service.ConfigService;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -54,6 +57,9 @@ public class SecurityConfig {
|
||||
private final ConfigService configService;
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
private final UserSocialService userSocialService;
|
||||
|
||||
/**
|
||||
* 配置安全过滤链 SecurityFilterChain
|
||||
*/
|
||||
@@ -128,17 +134,27 @@ public class SecurityConfig {
|
||||
return new SmsAuthenticationProvider(userService, redisTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序认证 Provider
|
||||
*/
|
||||
@Bean
|
||||
public WechatMiniAuthenticationProvider wechatMiniAuthenticationProvider() {
|
||||
return new WechatMiniAuthenticationProvider(wxMaService, userSocialService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
DaoAuthenticationProvider daoAuthenticationProvider,
|
||||
SmsAuthenticationProvider smsAuthenticationProvider
|
||||
SmsAuthenticationProvider smsAuthenticationProvider,
|
||||
WechatMiniAuthenticationProvider wechatMiniAuthenticationProvider
|
||||
) {
|
||||
return new ProviderManager(
|
||||
daoAuthenticationProvider,
|
||||
smsAuthenticationProvider
|
||||
smsAuthenticationProvider,
|
||||
wechatMiniAuthenticationProvider
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
31
src/main/java/com/youlai/boot/config/WxMaConfiguration.java
Normal file
31
src/main/java/com/youlai/boot/config/WxMaConfiguration.java
Normal file
@@ -0,0 +1,31 @@
|
||||
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 org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 微信小程序配置
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(WxMaProperties.class)
|
||||
public class WxMaConfiguration {
|
||||
|
||||
@Bean
|
||||
public WxMaService wxMaService(WxMaProperties properties) {
|
||||
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
|
||||
config.setAppid(properties.getAppid());
|
||||
config.setSecret(properties.getSecret());
|
||||
config.setToken(properties.getToken());
|
||||
config.setAesKey(properties.getAesKey());
|
||||
config.setMsgDataFormat(properties.getMsgDataFormat());
|
||||
|
||||
WxMaService service = new WxMaServiceImpl();
|
||||
service.setWxMaConfig(config);
|
||||
return service;
|
||||
}
|
||||
|
||||
}
|
||||
38
src/main/java/com/youlai/boot/config/WxMaProperties.java
Normal file
38
src/main/java/com/youlai/boot/config/WxMaProperties.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 微信小程序配置属性
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "wx.miniapp")
|
||||
public class WxMaProperties {
|
||||
|
||||
/**
|
||||
* 小程序appid
|
||||
*/
|
||||
private String appid;
|
||||
|
||||
/**
|
||||
* 小程序Secret
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* 小程序token
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 小程序EncodingAESKey
|
||||
*/
|
||||
private String aesKey;
|
||||
|
||||
/**
|
||||
* 消息格式
|
||||
*/
|
||||
private String msgDataFormat;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.youlai.boot.security.exception;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* 需要绑定手机号异常
|
||||
*/
|
||||
public class NeedBindMobileException extends AuthenticationException {
|
||||
|
||||
private final String openid;
|
||||
|
||||
private final String sessionKey;
|
||||
|
||||
public NeedBindMobileException(String openid, String sessionKey) {
|
||||
super("需要绑定手机号");
|
||||
this.openid = openid;
|
||||
this.sessionKey = sessionKey;
|
||||
}
|
||||
|
||||
public String getOpenid() {
|
||||
return openid;
|
||||
}
|
||||
|
||||
public String getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.youlai.boot.security.model;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 微信小程序认证 Token
|
||||
*/
|
||||
public class WechatMiniAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 622L;
|
||||
|
||||
/**
|
||||
* 认证信息
|
||||
* 未认证时:微信code
|
||||
* 已认证时:SysUserDetails 用户详情
|
||||
*/
|
||||
private final Object principal;
|
||||
|
||||
/**
|
||||
* 凭证信息
|
||||
* 未认证时:null
|
||||
* 已认证时:null
|
||||
*/
|
||||
private final Object credentials;
|
||||
|
||||
/**
|
||||
* 创建未认证的 Token
|
||||
*
|
||||
* @param code 微信小程序code
|
||||
*/
|
||||
public WechatMiniAuthenticationToken(String code) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.principal = code;
|
||||
this.credentials = null;
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已认证的 Token
|
||||
*
|
||||
* @param principal 用户详情(SysUserDetails)
|
||||
* @param authorities 授权信息
|
||||
*/
|
||||
public WechatMiniAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal;
|
||||
this.credentials = null;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已认证的 Token(静态工厂方法)
|
||||
*/
|
||||
public static WechatMiniAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
|
||||
return new WechatMiniAuthenticationToken(principal, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.youlai.boot.security.provider;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.youlai.boot.security.exception.NeedBindMobileException;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.security.model.WechatMiniAuthenticationToken;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
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
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class WechatMiniAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
private final UserSocialService userSocialService;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String code = (String) authentication.getPrincipal();
|
||||
|
||||
if (code == null || code.isEmpty()) {
|
||||
log.warn("微信小程序登录失败:code为空");
|
||||
throw new IllegalArgumentException("code不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 用 code 换取 openid
|
||||
WxMaJscode2SessionResult session = wxMaService.jsCode2SessionInfo(code);
|
||||
String openid = session.getOpenid();
|
||||
String sessionKey = session.getSessionKey();
|
||||
|
||||
log.info("微信小程序登录:openid={}", openid);
|
||||
|
||||
// 2. 根据 openid 查询绑定信息
|
||||
UserSocial userSocial = userSocialService.getByPlatformAndOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
|
||||
|
||||
if (userSocial == null) {
|
||||
// 未绑定,抛出异常提示需要绑定手机号
|
||||
log.info("微信小程序登录:用户未绑定手机号,openid={}", openid);
|
||||
throw new NeedBindMobileException(openid, sessionKey);
|
||||
}
|
||||
|
||||
// 3. 获取用户认证信息
|
||||
UserAuthInfo userAuthInfo = userSocialService.getAuthInfoByOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
|
||||
|
||||
if (userAuthInfo == null) {
|
||||
log.warn("微信小程序登录失败:用户不存在,openid={}", openid);
|
||||
throw new UsernameNotFoundException("用户不存在");
|
||||
}
|
||||
|
||||
// 4. 检查用户状态
|
||||
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
|
||||
log.warn("微信小程序登录失败:用户已禁用,username={}", userAuthInfo.getUsername());
|
||||
throw new DisabledException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 5. 更新 session_key
|
||||
userSocialService.updateSessionKey(userSocial.getId(), sessionKey);
|
||||
|
||||
// 6. 构建已认证 Token
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
|
||||
log.info("微信小程序登录成功:username={}, openid={}", userAuthInfo.getUsername(), openid);
|
||||
|
||||
return WechatMiniAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities());
|
||||
|
||||
} catch (WxErrorException e) {
|
||||
log.error("微信小程序登录失败:调用微信接口异常,code={}", code, e);
|
||||
throw new IllegalArgumentException("微信登录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return WechatMiniAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.youlai.boot.system.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.youlai.boot.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 第三方登录平台类型枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum SocialPlatformEnum implements IBaseEnum<String> {
|
||||
|
||||
WECHAT_MINI("WECHAT_MINI", "微信小程序"),
|
||||
WECHAT_MP("WECHAT_MP", "微信公众号"),
|
||||
WECHAT_OPEN("WECHAT_OPEN", "微信开放平台"),
|
||||
ALIPAY("ALIPAY", "支付宝"),
|
||||
QQ("QQ", "QQ"),
|
||||
APPLE("APPLE", "Apple ID");
|
||||
|
||||
@EnumValue
|
||||
private final String value;
|
||||
|
||||
private final String label;
|
||||
|
||||
SocialPlatformEnum(String value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.youlai.boot.system.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户第三方账号绑定持久层
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserSocialMapper extends BaseMapper<UserSocial> {
|
||||
|
||||
/**
|
||||
* 根据用户ID获取认证信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 认证信息
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByUserId(Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.youlai.boot.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户第三方账号绑定实体
|
||||
*/
|
||||
@TableName("sys_user_social")
|
||||
@Getter
|
||||
@Setter
|
||||
public class UserSocial {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 平台类型
|
||||
*/
|
||||
private SocialPlatformEnum platform;
|
||||
|
||||
/**
|
||||
* 平台openid
|
||||
*/
|
||||
private String openid;
|
||||
|
||||
/**
|
||||
* 微信unionid
|
||||
*/
|
||||
private String unionid;
|
||||
|
||||
/**
|
||||
* 第三方昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 第三方头像URL
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 微信session_key
|
||||
*/
|
||||
private String sessionKey;
|
||||
|
||||
/**
|
||||
* 是否已验证(1-已验证 0-未验证)
|
||||
*/
|
||||
private Integer verified;
|
||||
|
||||
/**
|
||||
* 绑定时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.youlai.boot.system.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
|
||||
/**
|
||||
* 用户第三方账号绑定业务接口
|
||||
*/
|
||||
public interface UserSocialService extends IService<UserSocial> {
|
||||
|
||||
/**
|
||||
* 根据平台和openid查询绑定信息
|
||||
*
|
||||
* @param platform 平台类型
|
||||
* @param openid openid
|
||||
* @return 绑定信息
|
||||
*/
|
||||
UserSocial getByPlatformAndOpenid(SocialPlatformEnum platform, String openid);
|
||||
|
||||
/**
|
||||
* 根据unionid查询绑定信息
|
||||
*
|
||||
* @param unionid unionid
|
||||
* @return 绑定信息
|
||||
*/
|
||||
UserSocial getByUnionid(String unionid);
|
||||
|
||||
/**
|
||||
* 绑定或更新第三方账号
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param platform 平台类型
|
||||
* @param openid openid
|
||||
* @param unionid unionid
|
||||
* @param nickname 昵称
|
||||
* @param avatar 头像
|
||||
* @param sessionKey session_key
|
||||
* @return 绑定信息
|
||||
*/
|
||||
UserSocial bindOrUpdate(Long userId, SocialPlatformEnum platform, String openid, String unionid, String nickname, String avatar, String sessionKey);
|
||||
|
||||
/**
|
||||
* 解绑第三方账号
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param platform 平台类型
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean unbind(Long userId, SocialPlatformEnum platform);
|
||||
|
||||
/**
|
||||
* 根据openid获取用户认证信息
|
||||
*
|
||||
* @param platform 平台类型
|
||||
* @param openid openid
|
||||
* @return 用户认证信息
|
||||
*/
|
||||
UserAuthInfo getAuthInfoByOpenid(SocialPlatformEnum platform, String openid);
|
||||
|
||||
/**
|
||||
* 更新session_key
|
||||
*
|
||||
* @param id 绑定记录ID
|
||||
* @param sessionKey session_key
|
||||
*/
|
||||
void updateSessionKey(Long id, String sessionKey);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.youlai.boot.system.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.security.model.UserAuthInfo;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.mapper.UserSocialMapper;
|
||||
import com.youlai.boot.system.model.entity.UserSocial;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户第三方账号绑定业务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserSocialServiceImpl extends ServiceImpl<UserSocialMapper, UserSocial> implements UserSocialService {
|
||||
|
||||
@Override
|
||||
public UserSocial getByPlatformAndOpenid(SocialPlatformEnum platform, String openid) {
|
||||
return getOne(new LambdaQueryWrapper<UserSocial>()
|
||||
.eq(UserSocial::getPlatform, platform)
|
||||
.eq(UserSocial::getOpenid, openid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSocial getByUnionid(String unionid) {
|
||||
if (StrUtil.isBlank(unionid)) {
|
||||
return null;
|
||||
}
|
||||
return getOne(new LambdaQueryWrapper<UserSocial>()
|
||||
.eq(UserSocial::getUnionid, unionid));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public UserSocial bindOrUpdate(Long userId, SocialPlatformEnum platform, String openid, String unionid, String nickname, String avatar, String sessionKey) {
|
||||
UserSocial userSocial = getByPlatformAndOpenid(platform, openid);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (userSocial == null) {
|
||||
userSocial = new UserSocial();
|
||||
userSocial.setUserId(userId);
|
||||
userSocial.setPlatform(platform);
|
||||
userSocial.setOpenid(openid);
|
||||
userSocial.setUnionid(unionid);
|
||||
userSocial.setNickname(nickname);
|
||||
userSocial.setAvatar(avatar);
|
||||
userSocial.setSessionKey(sessionKey);
|
||||
userSocial.setVerified(1);
|
||||
userSocial.setCreateTime(now);
|
||||
userSocial.setUpdateTime(now);
|
||||
save(userSocial);
|
||||
log.info("第三方账号绑定成功:userId={}, platform={}, openid={}", userId, platform, openid);
|
||||
} else {
|
||||
userSocial.setUserId(userId);
|
||||
userSocial.setUnionid(unionid);
|
||||
userSocial.setNickname(nickname);
|
||||
userSocial.setAvatar(avatar);
|
||||
userSocial.setSessionKey(sessionKey);
|
||||
userSocial.setUpdateTime(now);
|
||||
updateById(userSocial);
|
||||
log.info("第三方账号更新成功:userId={}, platform={}, openid={}", userId, platform, openid);
|
||||
}
|
||||
|
||||
return userSocial;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean unbind(Long userId, SocialPlatformEnum platform) {
|
||||
boolean removed = remove(new LambdaQueryWrapper<UserSocial>()
|
||||
.eq(UserSocial::getUserId, userId)
|
||||
.eq(UserSocial::getPlatform, platform));
|
||||
if (removed) {
|
||||
log.info("第三方账号解绑成功:userId={}, platform={}", userId, platform);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuthInfo getAuthInfoByOpenid(SocialPlatformEnum platform, String openid) {
|
||||
UserSocial userSocial = getByPlatformAndOpenid(platform, openid);
|
||||
if (userSocial == null) {
|
||||
return null;
|
||||
}
|
||||
return baseMapper.getAuthInfoByUserId(userSocial.getUserId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSessionKey(Long id, String sessionKey) {
|
||||
UserSocial userSocial = getById(id);
|
||||
if (userSocial != null) {
|
||||
userSocial.setSessionKey(sessionKey);
|
||||
userSocial.setUpdateTime(LocalDateTime.now());
|
||||
updateById(userSocial);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -85,13 +85,10 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
return templateConfig.getTemplatePath();
|
||||
}
|
||||
if ("API".equals(templateName)) {
|
||||
return "codegen/api.js.vm";
|
||||
return "codegen/frontend/js/api.js.vm";
|
||||
}
|
||||
if ("VIEW".equals(templateName)) {
|
||||
return "codegen/index.js.vue.vm";
|
||||
}
|
||||
if ("API_TYPES".equals(templateName)) {
|
||||
return "codegen/api-types.js.vm";
|
||||
return "codegen/frontend/js/index.js.vue.vm";
|
||||
}
|
||||
return templateConfig.getTemplatePath();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user