后台登录微信二次认证

This commit is contained in:
zjf committed 2026-06-22 17:32:13 +08:00
1 parent 10d6900777
commit 39878e0aa5
21 files changed
+701 -5

No files matched your search

@@ -0,0 +1,120 @@
package com.bsd.pjcloud.auth.support;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.RetOps;
import com.bsd.pjcloud.common.data.resolver.ParamResolver;
import com.bsd.pjcloud.common.security.exception.WechatBindRequiredException;
import com.bsd.pjcloud.common.security.exception.WechatConfirmRequiredException;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* PC 后台(pig 客户端)密码登录:可选绑定 / 扫码确认 PC 微信
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PcWechatPasswordLoginGate {
private static final String PC_PASSWORD_CLIENT_ID = "pig";
private static final String PARAM_BIND_REQUIRED = "PC_WECHAT_BIND_REQUIRED";
private static final String PARAM_CONFIRM_REQUIRED = "PC_WECHAT_CONFIRM_REQUIRED";
private static final long TICKET_TTL_MINUTES = 15;
private final RemoteUserService remoteUserService;
private final StringRedisTemplate redisTemplate;
private final PcWechatSecurityProperties pcWechatSecurityProperties;
public void check(RegisteredClient registeredClient, Authentication authentication) {
if (registeredClient == null || !PC_PASSWORD_CLIENT_ID.equals(registeredClient.getClientId())) {
return;
}
if (!isBindRequired() && !isConfirmRequired()) {
return;
}
Object principal = authentication.getPrincipal();
if (!(principal instanceof PjcloudUser pjcloudUser)) {
return;
}
R<UserInfo> userResult = remoteUserService.getInnerByUserId(pjcloudUser.getId());
UserInfo userInfo = RetOps.of(userResult).getData().orElse(null);
if (userInfo == null || userInfo.getSysUser() == null) {
return;
}
String wxPcOpenid = userInfo.getSysUser().getWxPcOpenid();
String ticket = IdUtil.simpleUUID();
if (StrUtil.isBlank(wxPcOpenid)) {
if (!isBindRequired() && !isConfirmRequired()) {
return;
}
// 未绑定:只要开启了绑定或扫码确认,均须先扫码绑定
redisTemplate.opsForValue().set(pwdVerifiedKey(ticket), String.valueOf(pjcloudUser.getId()), TICKET_TTL_MINUTES,
TimeUnit.MINUTES);
throw new WechatBindRequiredException(ticket);
}
if (!isConfirmRequired()) {
return;
}
redisTemplate.opsForValue().set(loginConfirmKey(ticket), String.valueOf(pjcloudUser.getId()), TICKET_TTL_MINUTES,
TimeUnit.MINUTES);
throw new WechatConfirmRequiredException(ticket);
}
private boolean isBindRequired() {
return resolveFlag(PARAM_BIND_REQUIRED, pcWechatSecurityProperties.isBindRequired());
}
private boolean isConfirmRequired() {
return resolveFlag(PARAM_CONFIRM_REQUIRED, pcWechatSecurityProperties.isConfirmRequired());
}
private boolean resolveFlag(String paramKey, boolean defaultValue) {
try {
String value = ParamResolver.getStr(paramKey);
if (StrUtil.isNotBlank(value)) {
return parseBool(value);
}
}
catch (Exception e) {
log.debug("读取公共参数 {} 失败,使用配置文件默认值", paramKey);
}
return defaultValue;
}
private boolean parseBool(String value) {
if ("1".equals(value) || "Y".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value)) {
return true;
}
if ("0".equals(value) || "N".equalsIgnoreCase(value) || "no".equalsIgnoreCase(value)) {
return false;
}
return BooleanUtil.toBoolean(value);
}
private String pwdVerifiedKey(String ticket) {
return CacheConstants.PC_PWD_VERIFIED + ticket;
}
private String loginConfirmKey(String ticket) {
return CacheConstants.PC_LOGIN_CONFIRM + ticket;
}
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.auth.support;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* PC 后台密码登录微信校验(开发/生产可关)
* <p>
* 配置示例(pjcloud-auth application.yml 或 Nacos):
* <pre>
* security:
* pc-wechat:
* bind-required: false
* confirm-required: false
* </pre>
* 也可在「参数管理」覆盖:PC_WECHAT_BIND_REQUIRED、PC_WECHAT_CONFIRM_REQUIRED(true/false 或 1/0)
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties("security.pc-wechat")
public class PcWechatSecurityProperties {
/**
* 密码登录后未绑定 PC 微信时是否拦截(须先扫码绑定)
*/
private boolean bindRequired = false;
/**
* 已绑定 PC 微信时,密码登录是否须微信扫码确认
*/
private boolean confirmRequired = false;
}
@@ -2,7 +2,10 @@ package com.bsd.pjcloud.auth.support.base;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.bsd.pjcloud.auth.support.PcWechatPasswordLoginGate;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.security.exception.WechatBindRequiredException;
import com.bsd.pjcloud.common.security.exception.WechatConfirmRequiredException;
import com.bsd.pjcloud.common.security.service.PjcloudRedisOAuth2AuthorizationService;
import com.bsd.pjcloud.common.security.util.OAuth2ErrorCodesExpand;
import com.bsd.pjcloud.common.security.util.ScopeException;
@@ -148,6 +151,10 @@ public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider<T extends OA
Authentication usernamePasswordAuthentication = authenticationManager
.authenticate(usernamePasswordAuthenticationToken);
if (AuthorizationGrantType.PASSWORD.equals(resouceOwnerBaseAuthentication.getAuthorizationGrantType())) {
SpringUtil.getBean(PcWechatPasswordLoginGate.class).check(registeredClient, usernamePasswordAuthentication);
}
Object onlineQuantity = registeredClient.getClientSettings()
.getSettings()
.get(CommonConstants.ONLINE_QUANTITY);
@@ -303,6 +310,14 @@ public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider<T extends OA
"User credentials have expired"),
""));
}
if (authenticationException instanceof WechatBindRequiredException bindEx) {
return new OAuth2AuthenticationException(
new OAuth2Error(OAuth2ErrorCodesExpand.WECHAT_BIND_REQUIRED, bindEx.getTicket(), ""));
}
if (authenticationException instanceof WechatConfirmRequiredException confirmEx) {
return new OAuth2AuthenticationException(
new OAuth2Error(OAuth2ErrorCodesExpand.WECHAT_CONFIRM_REQUIRED, confirmEx.getTicket(), ""));
}
if (authenticationException instanceof ScopeException) {
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "invalid_scope"), ""));
@@ -0,0 +1,6 @@
security:
pc-wechat:
# PC 后台密码登录:未绑定 PC 微信时是否强制先绑定
bind-required: false
# PC 后台密码登录:已绑定后是否须微信扫码确认
confirm-required: false
@@ -16,3 +16,4 @@ spring:
import:
- optional:nacos:application-@profiles.active@.yml
- optional:nacos:${spring.application.name}-@profiles.active@.yml
- optional:classpath:application-pc-wechat.yml
@@ -132,6 +132,21 @@ public interface CacheConstants {
*/
String WX_SOCIAL_TICKET = GLOBALLY + "wx:social:ticket:";
/**
* PC 微信社交登录一次性 ticket
*/
String WX_PC_SOCIAL_TICKET = GLOBALLY + "wx:pc:social:ticket:";
/**
* PC 密码已校验待绑定微信(value=userId)
*/
String PC_PWD_VERIFIED = GLOBALLY + "pc:pwd:verified:";
/**
* PC 密码已校验待微信扫码确认(value=userId)
*/
String PC_LOGIN_CONFIRM = GLOBALLY + "pc:login:confirm:";
/* ************************************* start 钱包相关 start ************************************* */
String WALLET_DEFAULT = "wallet_default";
@@ -46,10 +46,15 @@ public enum LoginTypeEnum {
QQ("QQ", "QQ登录"),
/**
* 微信登录
* 微信登录(移动端 App)
*/
WECHAT("WX", "微信登录"),
/**
* PC 后台微信扫码(网站应用 snsapi_login)
*/
WECHAT_PC("WX_PC", "PC微信登录"),
/**
* 微信小程序
*/
@@ -0,0 +1,21 @@
package com.bsd.pjcloud.common.security.exception;
import org.springframework.security.core.AuthenticationException;
/**
* PC 后台密码校验通过,但未绑定 PC 微信
*/
public class WechatBindRequiredException extends AuthenticationException {
private final String ticket;
public WechatBindRequiredException(String ticket) {
super(ticket);
this.ticket = ticket;
}
public String getTicket() {
return ticket;
}
}
@@ -0,0 +1,21 @@
package com.bsd.pjcloud.common.security.exception;
import org.springframework.security.core.AuthenticationException;
/**
* PC 后台密码校验通过,须微信扫码确认后签发 token
*/
public class WechatConfirmRequiredException extends AuthenticationException {
private final String ticket;
public WechatConfirmRequiredException(String ticket) {
super(ticket);
this.ticket = ticket;
}
public String getTicket() {
return ticket;
}
}
@@ -24,6 +24,12 @@ public interface OAuth2ErrorCodesExpand {
/** 证书过期 */
String CREDENTIALS_EXPIRED = "credentials_expired";
/** PC 后台须绑定微信 */
String WECHAT_BIND_REQUIRED = "wechat_bind_required";
/** PC 后台须微信扫码确认 */
String WECHAT_CONFIRM_REQUIRED = "wechat_confirm_required";
/** scope 为空异常 */
String SCOPE_IS_EMPTY = "scope_is_empty";
@@ -188,6 +188,12 @@ public class SysUser extends Model<SysUser> implements Serializable {
@Schema(description = "微信openid")
private String wxOpenid;
/**
* PC 后台微信 openid(网站应用)
*/
@Schema(description = "PC微信openid")
private String wxPcOpenid;
/**
* 企微微信 userid
*/
@@ -29,4 +29,11 @@ public class SysSocialDetailsVO {
*/
@Schema(description = "扩展字段")
private String ext;
/**
* 授权回调地址(PC 微信 qrconnect 使用,不含 #)
*/
@Schema(description = "回调地址")
private String redirectUrl;
}
@@ -91,6 +91,12 @@ public class UserVO implements BaseFacesInterface, Serializable {
@Schema(description = "微信open id")
private String wxOpenid;
/**
* PC 微信 openid
*/
@Schema(description = "PC微信open id")
private String wxPcOpenid;
/**
* QQ openid
*/
@@ -22,6 +22,7 @@ import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.admin.api.entity.SysSocialDetails;
import com.bsd.pjcloud.admin.service.PcWechatSocialService;
import com.bsd.pjcloud.admin.service.SysSocialDetailsService;
import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO;
import com.bsd.pjcloud.common.core.util.R;
@@ -55,6 +56,8 @@ public class SysSocialDetailsController {
private final SysSocialDetailsService sysSocialDetailsService;
private final PcWechatSocialService pcWechatSocialService;
/**
* 社交登录账户简单分页查询
* @param page 分页对象
@@ -168,6 +171,33 @@ public class SysSocialDetailsController {
return R.ok(sysSocialDetailsService.bindSocialByTicket(state, ticket));
}
/**
* PC 微信:解析 code,返回绑定状态与 ticket
*/
@Inner(value = false)
@PostMapping("/pc/wx/resolve")
public R<WxSocialResolveVO> resolveWxPcCode(String code) {
return R.ok(pcWechatSocialService.resolveWxPcCode(code));
}
/**
* PC 密码已校验、未绑定:扫码绑定并登录
*/
@Inner(value = false)
@PostMapping("/pc/bind-by-pwd-ticket")
public R<java.util.Map<String, Object>> bindPcByPwdTicket(String pwdTicket, String code) {
return R.ok(pcWechatSocialService.bindByPwdTicket(pwdTicket, code));
}
/**
* PC 密码已校验、已绑定:扫码确认登录
*/
@Inner(value = false)
@PostMapping("/pc/confirm-login")
public R<java.util.Map<String, Object>> confirmPcLogin(String ticket, String code) {
return R.ok(pcWechatSocialService.confirmLogin(ticket, code));
}
/**
* 导出
*/
@@ -0,0 +1,78 @@
/*
* PC 后台微信登录(网站应用 qrconnect,与移动端 WX 分离)
*/
package com.bsd.pjcloud.admin.handler;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysSocialDetails;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.mapper.SysSocialDetailsMapper;
import com.bsd.pjcloud.admin.service.SysUserService;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.constant.enums.LoginTypeEnum;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component("WX_PC")
@AllArgsConstructor
public class WeChatPcLoginHandler extends AbstractLoginHandler {
private final SysUserService sysUserService;
private final SysSocialDetailsMapper sysSocialDetailsMapper;
@Override
public String identify(String code) {
SysSocialDetails condition = new SysSocialDetails();
condition.setType(LoginTypeEnum.WECHAT_PC.getType());
SysSocialDetails socialDetails = sysSocialDetailsMapper.selectOne(new QueryWrapper<>(condition));
if (socialDetails == null) {
throw new CheckedException("未配置 PC 微信登录,请在后台添加 type=WX_PC 的网站应用");
}
String url = String.format(SecurityConstants.WX_AUTHORIZATION_CODE_URL, socialDetails.getAppId(),
socialDetails.getAppSecret(), code);
String result = HttpUtil.get(url);
log.debug("PC微信响应报文:{}", result);
JSONObject resultJsonObj = JSONUtil.parseObj(result);
if (resultJsonObj.containsKey("errcode")) {
log.error("PC微信授权失败:{}", result);
String errMsg = resultJsonObj.getStr("errmsg", "微信授权失败");
throw new CheckedException("微信授权失败:" + errMsg);
}
Object obj = resultJsonObj.get("openid");
if (obj == null) {
log.error("PC微信响应缺少openid:{}", result);
throw new CheckedException("微信授权失败,未获取到用户标识");
}
return obj.toString();
}
@Override
public UserInfo info(String openId) {
SysUser user = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getWxPcOpenid, openId));
if (user == null) {
log.info("PC微信未绑定:{}", openId);
return null;
}
return sysUserService.findUserInfo(user);
}
@Override
public Boolean bind(SysUser user, String identify) {
user.setWxPcOpenid(identify);
sysUserService.updateById(user);
return true;
}
}
@@ -0,0 +1,62 @@
package com.bsd.pjcloud.admin.handler;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.service.SysUserService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* PC 微信 ticket 登录(mobile=WX_PC_TICKET@{ticket})
*/
@Slf4j
@Component("WX_PC_TICKET")
@RequiredArgsConstructor
public class WxPcTicketLoginHandler implements LoginHandler {
private final StringRedisTemplate redisTemplate;
private final SysUserService sysUserService;
private String ticketKey(String ticket) {
return CacheConstants.WX_PC_SOCIAL_TICKET + ticket;
}
@Override
public Boolean check(String loginStr) {
return true;
}
@Override
public String identify(String ticket) {
String openid = redisTemplate.opsForValue().get(ticketKey(ticket));
if (StrUtil.isBlank(openid)) {
throw new CheckedException("微信授权已过期,请重新授权");
}
redisTemplate.delete(ticketKey(ticket));
return openid;
}
@Override
public UserInfo info(String openid) {
SysUser user = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getWxPcOpenid, openid));
if (user == null) {
log.info("PC微信未绑定:{}", openid);
return null;
}
return sysUserService.findUserInfo(user);
}
@Override
public UserInfo handle(String loginStr) {
String openid = identify(loginStr);
return info(openid);
}
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.admin.service;
import java.util.Map;
/**
* PC 后台微信:密码登录后的绑定 / 扫码确认
*/
public interface PcWechatSocialService {
/**
* 密码已校验、未绑定 PC 微信:扫码绑定并登录
*/
Map<String, Object> bindByPwdTicket(String pwdTicket, String code);
/**
* 密码已校验、已绑定:扫码确认后登录
*/
Map<String, Object> confirmLogin(String confirmTicket, String code);
/**
* 解析 PC 微信 code,返回绑定状态与 ticket
*/
com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO resolveWxPcCode(String code);
}
@@ -0,0 +1,203 @@
package com.bsd.pjcloud.admin.service.impl;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.dto.UserTokenDTO;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.api.feign.RemoteTokenService;
import com.bsd.pjcloud.admin.api.vo.WxSocialResolveVO;
import com.bsd.pjcloud.admin.handler.LoginHandler;
import com.bsd.pjcloud.admin.service.PcWechatSocialService;
import com.bsd.pjcloud.admin.service.SysUserService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import com.bsd.pjcloud.common.core.util.R;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class PcWechatSocialServiceImpl implements PcWechatSocialService {
private static final long TICKET_TTL_MINUTES = 15;
private final Map<String, LoginHandler> loginHandlerMap;
private final StringRedisTemplate redisTemplate;
private final SysUserService sysUserService;
private final RemoteTokenService remoteTokenService;
private final CacheManager cacheManager;
private String wxPcTicketKey(String ticket) {
return CacheConstants.WX_PC_SOCIAL_TICKET + ticket;
}
private String pwdVerifiedKey(String ticket) {
return CacheConstants.PC_PWD_VERIFIED + ticket;
}
private String loginConfirmKey(String ticket) {
return CacheConstants.PC_LOGIN_CONFIRM + ticket;
}
@Override
public WxSocialResolveVO resolveWxPcCode(String code) {
LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC");
String openid = wxPcHandler.identify(code);
UserInfo userInfo = wxPcHandler.info(openid);
String ticket = IdUtil.simpleUUID();
redisTemplate.opsForValue().set(wxPcTicketKey(ticket), openid, TICKET_TTL_MINUTES, TimeUnit.MINUTES);
WxSocialResolveVO vo = new WxSocialResolveVO();
vo.setBound(userInfo != null);
vo.setTicket(ticket);
return vo;
}
@Override
public Map<String, Object> bindByPwdTicket(String pwdTicket, String code) {
String userIdStr = redisTemplate.opsForValue().get(pwdVerifiedKey(pwdTicket));
if (StrUtil.isBlank(userIdStr)) {
throw new CheckedException("登录已过期,请重新输入账号密码");
}
LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC");
String openid = wxPcHandler.identify(code);
UserInfo existing = wxPcHandler.info(openid);
if (existing != null && !existing.getSysUser().getUserId().equals(Long.valueOf(userIdStr))) {
throw new CheckedException("该微信已绑定其他账号");
}
SysUser user = sysUserService.getById(Long.valueOf(userIdStr));
if (user == null) {
throw new CheckedException("用户不存在");
}
wxPcHandler.bind(user, openid);
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(user.getUsername());
redisTemplate.delete(pwdVerifiedKey(pwdTicket));
return issueToken(user);
}
@Override
public Map<String, Object> confirmLogin(String confirmTicket, String code) {
String userIdStr = redisTemplate.opsForValue().get(loginConfirmKey(confirmTicket));
if (StrUtil.isBlank(userIdStr)) {
throw new CheckedException("登录已过期,请重新输入账号密码");
}
SysUser user = sysUserService.getById(Long.valueOf(userIdStr));
if (user == null) {
throw new CheckedException("用户不存在");
}
if (StrUtil.isBlank(user.getWxPcOpenid())) {
throw new CheckedException("未绑定 PC 微信,请先绑定");
}
LoginHandler wxPcHandler = loginHandlerMap.get("WX_PC");
String openid = wxPcHandler.identify(code);
if (!StrUtil.equals(openid, user.getWxPcOpenid())) {
throw new CheckedException("微信账号与绑定不一致,请使用已绑定的微信扫码");
}
redisTemplate.delete(loginConfirmKey(confirmTicket));
return issueToken(user);
}
private Map<String, Object> issueToken(SysUser user) {
UserInfo userInfo = sysUserService.findUserInfo(user);
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setSysUser(userInfo.getSysUser());
userTokenDTO.setUsername(user.getUsername());
userTokenDTO.setAuthorities(buildTokenAuthorities(userInfo));
R<?> tokenResult = remoteTokenService.generateToken(userTokenDTO);
if (tokenResult == null || tokenResult.getCode() != 0 || tokenResult.getData() == null) {
log.error("PC微信签发 token 失败: {}", tokenResult);
throw new CheckedException("签发登录令牌失败");
}
Object data = tokenResult.getData();
if (data instanceof OAuth2AccessTokenAuthenticationToken authToken) {
OAuth2AccessToken accessToken = authToken.getAccessToken();
OAuth2RefreshToken refreshToken = authToken.getRefreshToken();
Map<String, Object> map = new HashMap<>(2);
map.put("access_token", accessToken.getTokenValue());
if (refreshToken != null) {
map.put("refresh_token", refreshToken.getTokenValue());
}
return map;
}
if (data instanceof Map<?, ?> map) {
String accessToken = extractTokenValue(map.get("accessToken"));
if (StrUtil.isBlank(accessToken)) {
accessToken = extractTokenValue(map.get("access_token"));
}
String refreshToken = extractTokenValue(map.get("refreshToken"));
if (StrUtil.isBlank(refreshToken)) {
refreshToken = extractTokenValue(map.get("refresh_token"));
}
if (StrUtil.isBlank(accessToken)) {
log.error("PC微信签发 token 解析失败, data={}", map);
throw new CheckedException("签发登录令牌失败");
}
Map<String, Object> result = new HashMap<>(2);
result.put("access_token", accessToken);
if (StrUtil.isNotBlank(refreshToken)) {
result.put("refresh_token", refreshToken);
}
return result;
}
log.error("PC微信签发 token 未知数据类型: {}", data.getClass());
throw new CheckedException("签发登录令牌失败");
}
/** 与 PjcloudUserDetailsService 一致:角色 ROLE_{id} + 权限标识,否则菜单接口 getRoleIds 为空 */
private List<String> buildTokenAuthorities(UserInfo userInfo) {
Set<String> authSet = new HashSet<>();
if (ArrayUtil.isNotEmpty(userInfo.getRoles())) {
Arrays.stream(userInfo.getRoles()).forEach(roleId -> authSet.add(SecurityConstants.ROLE + roleId));
}
if (ArrayUtil.isNotEmpty(userInfo.getPermissions())) {
authSet.addAll(Arrays.asList(userInfo.getPermissions()));
}
return new ArrayList<>(authSet);
}
/** Feign 反序列化 OAuth2AccessToken 常为 Map,需取 tokenValue */
private String extractTokenValue(Object tokenObj) {
if (tokenObj == null) {
return null;
}
if (tokenObj instanceof OAuth2AccessToken accessToken) {
return accessToken.getTokenValue();
}
if (tokenObj instanceof OAuth2RefreshToken refreshToken) {
return refreshToken.getTokenValue();
}
if (tokenObj instanceof Map<?, ?> tokenMap) {
Object tokenValue = tokenMap.get("tokenValue");
if (tokenValue == null) {
tokenValue = tokenMap.get("value");
}
return tokenValue != null ? String.valueOf(tokenValue) : null;
}
String str = String.valueOf(tokenObj);
return StrUtil.isBlank(str) || str.startsWith("{") ? null : str;
}
}
@@ -66,10 +66,17 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
private static final long WX_TICKET_TTL_MINUTES = 15;
private String wxTicketKey(String ticket) {
private String wxTicketKey(String type, String ticket) {
if ("WX_PC".equals(type)) {
return CacheConstants.WX_PC_SOCIAL_TICKET + ticket;
}
return CacheConstants.WX_SOCIAL_TICKET + ticket;
}
private String wxTicketKey(String ticket) {
return wxTicketKey("WX", ticket);
}
/**
* 绑定社交账号
*
@@ -92,7 +99,7 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
@Override
public Boolean bindSocialByTicket(String type, String ticket) {
String openid = redisTemplate.opsForValue().get(wxTicketKey(ticket));
String openid = redisTemplate.opsForValue().get(wxTicketKey(type, ticket));
if (StrUtil.isBlank(openid)) {
throw new CheckedException("微信授权已过期,请重新授权");
}
@@ -100,8 +107,7 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
SysUser sysUser = sysUserMapper.selectById(SecurityUtils.getUser().getId());
loginHandler.bind(sysUser, openid);
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(sysUser.getUsername());
// 绑定成功后再删除 ticket,失败时可重试
redisTemplate.delete(wxTicketKey(ticket));
redisTemplate.delete(wxTicketKey(type, ticket));
return Boolean.TRUE;
}
@@ -143,7 +149,27 @@ public class SysSocialDetailsServiceImpl extends ServiceImpl<SysSocialDetailsMap
return sysSocialDetailsList.stream().map(sysSocialDetails -> {
SysSocialDetailsVO newSysSocialDetails = new SysSocialDetailsVO();
BeanUtils.copyProperties(sysSocialDetails, newSysSocialDetails);
if ("WX_PC".equals(sysSocialDetails.getType())) {
newSysSocialDetails.setRedirectUrl(normalizeWxPcRedirectUrl(sysSocialDetails.getRedirectUrl()));
}
return newSysSocialDetails;
}).collect(Collectors.toList());
}
/** PC 微信 qrconnect:redirect_uri 不能含 # */
private static String normalizeWxPcRedirectUrl(String redirectUrl) {
if (StrUtil.isBlank(redirectUrl)) {
return redirectUrl;
}
String url = redirectUrl.trim();
int hashIdx = url.indexOf('#');
if (hashIdx >= 0) {
url = url.substring(0, hashIdx);
}
while (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return url;
}
}
@@ -907,6 +907,8 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
LambdaUpdateWrapper<SysUser> wrapper = null;
if (type.equals("wechat")) {
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxOpenid, null).eq(SysUser::getUserId, user.getId());
} else if (type.equals("wechat_pc")) {
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getWxPcOpenid, null).eq(SysUser::getUserId, user.getId());
} else if (type.equals("gitee")) {
wrapper = Wrappers.<SysUser>lambdaUpdate().set(SysUser::getGiteeLogin, null).eq(SysUser::getUserId, user.getId());
} else if (type.equals("osc")) {
@@ -33,6 +33,7 @@
<result column="gender" property="gender"/>
<result column="avatar" property="avatar"/>
<result column="wx_openid" property="wxOpenid"/>
<result column="wx_pc_openid" property="wxPcOpenid"/>
<result column="qq_openid" property="qqOpenid"/>
<result column="gitee_login" property="giteeOpenId"/>
<result column="osc_id" property="oscOpenId"/>
@@ -73,6 +74,7 @@
u.gender,
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,
@@ -107,6 +109,7 @@
u.gender,
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,
@@ -163,6 +166,7 @@
u.gender,
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,
@@ -263,6 +267,7 @@
u.phone,
u.avatar,
u.wx_openid,
u.wx_pc_openid,
u.qq_openid,
u.gitee_login,
u.osc_id,