Commit 18f93b0a authored by 技术部-任文超's avatar 技术部-任文超

重构微信关联:

1、关闭独立的关联请求接口;
2、创建微信关联、解绑、冲突用户下线事务
3、捆绑该关联动作到用户账密登录接口和快速登录接口
parent 75ca7b4c
...@@ -28,6 +28,8 @@ public interface Constants { ...@@ -28,6 +28,8 @@ public interface Constants {
String X_AUTH_TOKEN = "x-auth-token"; String X_AUTH_TOKEN = "x-auth-token";
/** 登录账号/手机号参数名 */ /** 登录账号/手机号参数名 */
String PHONE_NO = "phoneNo"; String PHONE_NO = "phoneNo";
/** 微信标识参数名 */
String OPEN_ID = "openId";
// -- Start -- IPV4安全策略常量组 // -- Start -- IPV4安全策略常量组
/** 账密不匹配错误 - 按账号计数 */ /** 账密不匹配错误 - 按账号计数 */
......
...@@ -3,6 +3,7 @@ package cn.quantgroup.xyqb.controller; ...@@ -3,6 +3,7 @@ package cn.quantgroup.xyqb.controller;
import cn.quantgroup.xyqb.exception.PasswordErrorLimitException; import cn.quantgroup.xyqb.exception.PasswordErrorLimitException;
import cn.quantgroup.xyqb.exception.UserNotExistException; import cn.quantgroup.xyqb.exception.UserNotExistException;
import cn.quantgroup.xyqb.exception.VerificationCodeErrorException; import cn.quantgroup.xyqb.exception.VerificationCodeErrorException;
import cn.quantgroup.xyqb.exception.WechatRelateUserException;
import cn.quantgroup.xyqb.model.JsonResult; import cn.quantgroup.xyqb.model.JsonResult;
import cn.quantgroup.xyqb.util.IPUtil; import cn.quantgroup.xyqb.util.IPUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
...@@ -63,6 +64,18 @@ public class ExceptionHandlingController implements IBaseController { ...@@ -63,6 +64,18 @@ public class ExceptionHandlingController implements IBaseController {
return new JsonResult(unee.getMessage(), 401L, null); return new JsonResult(unee.getMessage(), 401L, null);
} }
/**
* 微信关联异常
* @param wrue
* @return
*/
@ExceptionHandler(WechatRelateUserException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public JsonResult wechatRelateUserException(WechatRelateUserException wrue) {
LOGGER.info("throw WechatRelateUserException,msg={},businessCode={},code={}", wrue.getMessage(), 1L, 401L);
return new JsonResult(wrue.getMessage(), 401L, null);
}
/** /**
* 其他全局异常 * 其他全局异常
* @param e * @param e
......
...@@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Value; ...@@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
...@@ -206,6 +208,9 @@ public class WeChatController implements IBaseController { ...@@ -206,6 +208,9 @@ public class WeChatController implements IBaseController {
* @param response * @param response
*/ */
private void receiveCodeWithDefault(String code, String systemKey, String schema, Long registerFrom, String redirect, HttpServletResponse response){ private void receiveCodeWithDefault(String code, String systemKey, String schema, Long registerFrom, String redirect, HttpServletResponse response){
// 微信跳转请求入参监控
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
LOGGER.info("[WeChatController][receiveCodeWithDefault]微信授权及跳转:code:{},systemKey:{},schema:{},registerFrom:{},redirect:{},request:{},response:{}", code, systemKey, schema, registerFrom, redirect, request, response);
/* /*
* 预处理(容错) * 预处理(容错)
*/ */
...@@ -220,6 +225,7 @@ public class WeChatController implements IBaseController { ...@@ -220,6 +225,7 @@ public class WeChatController implements IBaseController {
// 从code获取token // 从code获取token
Merchant merchant = merchantService.findMerchantByName(systemKey); Merchant merchant = merchantService.findMerchantByName(systemKey);
AccessTokenResponse token = wechatService.getToken(code); AccessTokenResponse token = wechatService.getToken(code);
LOGGER.info("[WeChatController][receiveCodeWithDefault]微信授权及跳转:merchant:{},token:{}", merchant, token);
if (token == null) { if (token == null) {
// 让用户登录,不关联微信, 构造不关联微信的url // 让用户登录,不关联微信, 构造不关联微信的url
redirectNormalUrl(response, merchant, registerFrom,domain); redirectNormalUrl(response, merchant, registerFrom,domain);
...@@ -228,12 +234,14 @@ public class WeChatController implements IBaseController { ...@@ -228,12 +234,14 @@ public class WeChatController implements IBaseController {
WechatUserInfo userInfo = WechatUserInfo userInfo =
wechatService.getWechatUserInfoFromWechatServer(token.getAccessToken(), wechatService.getWechatUserInfoFromWechatServer(token.getAccessToken(),
token.getOpenId()); token.getOpenId());
LOGGER.info("[WeChatController][receiveCodeWithDefault]微信授权及跳转:WechatUserInfo - from wechat api:{}", userInfo);
if (userInfo == null || StringUtils.isEmpty(userInfo.getOpenId())) { if (userInfo == null || StringUtils.isEmpty(userInfo.getOpenId())) {
// 让用户登录,不关联微信, 构造不关联微信的url // 让用户登录,不关联微信, 构造不关联微信的url
redirectNormalUrl(response, merchant, registerFrom, domain); redirectNormalUrl(response, merchant, registerFrom, domain);
return; return;
} }
WechatUserInfo userInfoInDb = wechatService.findWechatUserInfoFromDb(userInfo.getOpenId()); WechatUserInfo userInfoInDb = wechatService.findWechatUserInfoFromDb(userInfo.getOpenId());
LOGGER.info("[WeChatController][receiveCodeWithDefault]微信授权及跳转:WechatUserInfo - from DB:{}", userInfo);
// welcome 首次登录 // welcome 首次登录
if (userInfoInDb == null) { if (userInfoInDb == null) {
// 微信用户首次登录界面, 首先保存userInfo, 跳入到微信注册登录界面 // 微信用户首次登录界面, 首先保存userInfo, 跳入到微信注册登录界面
...@@ -287,6 +295,7 @@ public class WeChatController implements IBaseController { ...@@ -287,6 +295,7 @@ public class WeChatController implements IBaseController {
} }
private String createUserSession(User user, Merchant merchant, String redirect, String domain, Long registerFrom) { private String createUserSession(User user, Merchant merchant, String redirect, String domain, Long registerFrom) {
LOGGER.info("[WeChatController][createUserSession]微信授权及跳转:user:{},merchant:{},redirect:{},domain:{},registerFrom:{}",user, merchant, redirect, domain, registerFrom);
if (StringUtils.isEmpty(redirect) || "redirect".equals(redirect)) { if (StringUtils.isEmpty(redirect) || "redirect".equals(redirect)) {
LOGGER.info("微信登录:redirect为null,走正常流程."); LOGGER.info("微信登录:redirect为null,走正常流程.");
if ("baitiao".equals(merchant.getName())) { if ("baitiao".equals(merchant.getName())) {
...@@ -311,19 +320,20 @@ public class WeChatController implements IBaseController { ...@@ -311,19 +320,20 @@ public class WeChatController implements IBaseController {
private String loginInWechatWithSessionCreated(User user, Merchant merchant, String target, Long channelId, String domain, Long registerFrom) { private String loginInWechatWithSessionCreated(User user, Merchant merchant, String target, Long channelId, String domain, Long registerFrom) {
AuthBean authBean = sessionService.createSession(channelId, registerFrom, "", user, merchant); AuthBean authBean = sessionService.createSession(channelId, registerFrom, "", user, merchant);
LOGGER.info("[WeChatController][loginInWechatWithSessionCreated]微信授权及跳转:user:{},merchant:{},target:{},channelId:{},domain:{},registerFrom:{}",user, merchant, target, channelId, domain, registerFrom);
return domain + "/landing?token=" + authBean.getToken() + "&registerFrom=" + registerFrom + "&channelId=" + channelId + "&key=" + merchant.getName() + "&target=" + target; return domain + "/landing?token=" + authBean.getToken() + "&registerFrom=" + registerFrom + "&channelId=" + channelId + "&key=" + merchant.getName() + "&target=" + target;
} }
private void redirectWechatLoginUrlWithoutLogin(HttpServletResponse response, Merchant merchant, WechatUserInfo userInfo, Long registerFrom,String domain) { private void redirectWechatLoginUrlWithoutLogin(HttpServletResponse response, Merchant merchant, WechatUserInfo userInfo, Long registerFrom,String domain) {
String redirectUrl = assembleWechatRedirectUrl(merchant, userInfo, registerFrom,domain); String redirectUrl = assembleWechatRedirectUrl(merchant, userInfo, registerFrom,domain);
LOGGER.info("redirectWechatLoginUrlWithoutLogin redirectUrl:[{}]",redirectUrl); LOGGER.info("[WeChatController][redirectWechatLoginUrlWithoutLogin]微信授权及跳转:redirectUrl:[{}]",redirectUrl);
response.setHeader("Location", redirectUrl); response.setHeader("Location", redirectUrl);
response.setStatus(301); response.setStatus(301);
} }
private void redirectNormalUrl(HttpServletResponse response, Merchant merchant, Long registerFrom,String domain) { private void redirectNormalUrl(HttpServletResponse response, Merchant merchant, Long registerFrom,String domain) {
String redirectUrl = assembleNormalRedirectUrl(merchant, registerFrom,domain); String redirectUrl = assembleNormalRedirectUrl(merchant, registerFrom,domain);
LOGGER.info("redirectNormalUrl redirectUrl:[{}]",redirectUrl); LOGGER.info("[WeChatController][redirectNormalUrl]微信授权及跳转: redirectUrl:[{}]",redirectUrl);
response.setHeader("Location", redirectUrl); response.setHeader("Location", redirectUrl);
response.setStatus(301); response.setStatus(301);
} }
......
package cn.quantgroup.xyqb.exception;
/**
* 微信关联用户异常
* @author 任文超
* @time 2018-04-03
*/
public class WechatRelateUserException extends RuntimeException {
private static final long serialVersionUID = -1L;
public WechatRelateUserException(String msg, Throwable t) {
super(msg, t);
}
public WechatRelateUserException(String msg) {
super(msg);
}
}
...@@ -7,6 +7,8 @@ import org.springframework.data.jpa.repository.Modifying; ...@@ -7,6 +7,8 @@ import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import static org.springframework.transaction.annotation.Propagation.MANDATORY;
/** /**
* Created by 11 on 2017/1/18. * Created by 11 on 2017/1/18.
*/ */
...@@ -23,8 +25,27 @@ public interface IWeChatUserRepository extends JpaRepository<WechatUserInfo, Lon ...@@ -23,8 +25,27 @@ public interface IWeChatUserRepository extends JpaRepository<WechatUserInfo, Lon
@Modifying @Modifying
int removeByUserId(Long userId); int removeByUserId(Long userId);
/**
* 关联用户
* @param userId - 用户标识
* @param phoneNo
* @param openId - 微信标识
* @return 记录更新行数
*/
@Transactional @Transactional
@Modifying @Modifying
@Query(value = "update wechat_userinfo set user_id=?1,phone_no=?2 where open_id=?3 and user_id is null", nativeQuery = true) @Query(value = "update wechat_userinfo set user_id=?1,phone_no=?2 where open_id=?3 and user_id is null", nativeQuery = true)
int bindingUser(Long userId, String phoneNo, String openId); int relateUser(Long userId, String phoneNo, String openId);
/**
* 解除关联关系 -- 包括:1、当前微信旧的关联用户;2、当前用户旧的关联微信
* 强制事务
* @param openId - 微信标识
* @param userId - 用户标识
* @return 记录更新行数
*/
@Transactional(propagation = MANDATORY)
@Modifying
@Query(value = "update wechat_userinfo set user_id=null,phone_no='*' where open_id=?1 or user_id=?2", nativeQuery = true)
int dissociateUser(String openId, Long userId);
} }
...@@ -12,7 +12,18 @@ import cn.quantgroup.xyqb.model.session.SessionValue; ...@@ -12,7 +12,18 @@ import cn.quantgroup.xyqb.model.session.SessionValue;
*/ */
public interface ISessionService { public interface ISessionService {
/**
* 更新session
* 用户信息存在,更新session中的最后访问时间,重新写入缓存.
* 存在则保存用户session信息,userId为uuid
*
* @param channelId
* @param createdFrom
* @param appChannel
* @param user
* @param merchant
* @return
*/
AuthBean createSession(Long channelId, Long createdFrom, String appChannel, User user, Merchant merchant); AuthBean createSession(Long channelId, Long createdFrom, String appChannel, User user, Merchant merchant);
SessionStruct createSessionAndPersist(User user, LoginProperties loginProperties); SessionStruct createSessionAndPersist(User user, LoginProperties loginProperties);
......
...@@ -44,6 +44,18 @@ public class SessionServiceImpl implements ISessionService { ...@@ -44,6 +44,18 @@ public class SessionServiceImpl implements ISessionService {
@Autowired @Autowired
private IUserService userService; private IUserService userService;
/**
* 更新session
* 用户信息存在,更新session中的最后访问时间,重新写入缓存.
* 存在则保存用户session信息,userId为uuid
*
* @param channelId
* @param createdFrom
* @param appChannel
* @param user
* @param merchant
* @return
*/
@Override @Override
public AuthBean createSession(Long channelId, Long createdFrom, String appChannel, User user, Merchant merchant) { public AuthBean createSession(Long channelId, Long createdFrom, String appChannel, User user, Merchant merchant) {
AuthBean authBean = new AuthBean(); AuthBean authBean = new AuthBean();
......
...@@ -17,8 +17,21 @@ public interface IWechatService { ...@@ -17,8 +17,21 @@ public interface IWechatService {
WechatUserInfo saveWechatUserInfo(WechatUserInfo userInfo); WechatUserInfo saveWechatUserInfo(WechatUserInfo userInfo);
int bindingUser(Long userId, String phoneNo, String openId); /**
* 关联用户和微信
* @param userId - 用户标识
* @param phoneNo
* @param openId - 微信标识
* @return
*/
int relateUser(Long userId, String phoneNo, String openId);
/**
* 按userId查微信信息
* 此接口实现处不应加缓存,否则解绑时(在关联用户的事务中)不好清除
* @param userId - 用户标识
* @return
*/
WechatUserInfo queryByUserId(Long userId); WechatUserInfo queryByUserId(Long userId);
int forbiddenUserWeChat(Long userId); int forbiddenUserWeChat(Long userId);
......
package cn.quantgroup.xyqb.service.wechat.impl; package cn.quantgroup.xyqb.service.wechat.impl;
import cn.quantgroup.xyqb.entity.WechatUserInfo; import cn.quantgroup.xyqb.entity.WechatUserInfo;
import cn.quantgroup.xyqb.exception.WechatRelateUserException;
import cn.quantgroup.xyqb.model.webchat.AccessTokenResponse; import cn.quantgroup.xyqb.model.webchat.AccessTokenResponse;
import cn.quantgroup.xyqb.repository.IWeChatUserRepository; import cn.quantgroup.xyqb.repository.IWeChatUserRepository;
import cn.quantgroup.xyqb.service.http.IHttpService; import cn.quantgroup.xyqb.service.http.IHttpService;
import cn.quantgroup.xyqb.service.session.ISessionService;
import cn.quantgroup.xyqb.service.wechat.IWechatService; import cn.quantgroup.xyqb.service.wechat.IWechatService;
import cn.quantgroup.xyqb.util.ValidationUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
...@@ -14,12 +17,14 @@ import org.springframework.beans.factory.annotation.Qualifier; ...@@ -14,12 +17,14 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
...@@ -32,19 +37,21 @@ import java.util.concurrent.TimeUnit; ...@@ -32,19 +37,21 @@ import java.util.concurrent.TimeUnit;
public class WechatServiceImpl implements IWechatService { public class WechatServiceImpl implements IWechatService {
private static final String WECHAT_TOKEN_KEY_PREFIX = "wechat:token:"; private static final String WECHAT_TOKEN_KEY_PREFIX = "wechat:token:";
private static final String WECHAT_USERINFO_KEY_PREFIX = "wechat:userinfo:"; private static final String WECHAT_USERINFO_KEY_PREFIX = "wechat:userinfo:";
@Autowired
private IHttpService httpService;
@Autowired
@Qualifier("stringRedisTemplate")
private RedisTemplate<String, String> redisTemplate;
@Value("${wechat.appid}") @Value("${wechat.appid}")
private String appId; private String appId;
@Value("${wechat.secret}") @Value("${wechat.secret}")
private String secret; private String secret;
@Autowired
private IWeChatUserRepository weChatUserRepository;
private String accessTokenUrl; private String accessTokenUrl;
private String refreshTokenUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"; private String refreshTokenUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s";
@Resource
private IHttpService httpService;
@Resource
private ISessionService sessionService;
@Resource
private IWeChatUserRepository weChatUserRepository;
@Autowired
@Qualifier("stringRedisTemplate")
private RedisTemplate<String, String> redisTemplate;
@PostConstruct @PostConstruct
private void init() { private void init() {
...@@ -136,15 +143,36 @@ public class WechatServiceImpl implements IWechatService { ...@@ -136,15 +143,36 @@ public class WechatServiceImpl implements IWechatService {
@Override @Override
@CacheEvict(value = "WechatUserInfo", key = "'openId:' + #openId", cacheManager = "cacheManager") @CacheEvict(value = "WechatUserInfo", key = "'openId:' + #openId", cacheManager = "cacheManager")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int bindingUser(Long userId, String phoneNo, String openId) { public int relateUser(Long userId, String phoneNo, String openId) {
if(Objects.isNull(userId) || Objects.isNull(openId)){ if(Objects.isNull(userId) || Objects.isNull(openId) || StringUtils.isBlank(openId)){
log.error("微信关联失败:非法入参:[service]:userId:{},phoneNo:{},openId:{}", userId, phoneNo, openId);
return 0; return 0;
} }
return weChatUserRepository.bindingUser(userId, Optional.ofNullable(phoneNo).orElse(""), openId); /* 如果当前openId已关联其他用户,则解绑成功后要注销其登录session */
WechatUserInfo wechatUserInfo = weChatUserRepository.findByUserId(userId);
// 强制解除关联
int dissociate = weChatUserRepository.dissociateUser(openId, userId);
if(dissociate < 1){
log.error("微信关联失败:解绑条数<1:[service]:userId:{},phoneNo:{},openId:{}", userId, phoneNo, openId);
throw new WechatRelateUserException("微信关联失败");
}
int relate = weChatUserRepository.relateUser(userId, Optional.ofNullable(phoneNo).orElse(""), openId);
if(relate < 1){
log.error("微信关联失败:绑定条数<1:[service]:userId:{},phoneNo:{},openId:{}", userId, phoneNo, openId);
throw new WechatRelateUserException("微信关联失败");
}else if(Objects.nonNull(wechatUserInfo) && Objects.nonNull(wechatUserInfo.getUserId())){
// 如果当前openId已关联其他用户,则解绑成功后要注销其登录session
sessionService.deleteByUserId(wechatUserInfo.getUserId());
}
log.info("微信关联成功:[service]:userId:{},phoneNo:{},openId:{},dissociate:{},relate:{}", userId, phoneNo, openId,dissociate,relate);
return relate;
} }
@Override @Override
public WechatUserInfo findWechatUserInfoByPhoneNo(String phoneNo) { public WechatUserInfo findWechatUserInfoByPhoneNo(String phoneNo) {
if(!ValidationUtil.validatePhoneNo(phoneNo)){
return null;
}
return weChatUserRepository.findByPhoneNo(phoneNo); return weChatUserRepository.findByPhoneNo(phoneNo);
} }
...@@ -163,15 +191,19 @@ public class WechatServiceImpl implements IWechatService { ...@@ -163,15 +191,19 @@ public class WechatServiceImpl implements IWechatService {
return httpService.get(String.format(refreshTokenUrl, refreshToken)); return httpService.get(String.format(refreshTokenUrl, refreshToken));
} }
/**
* 按userId查微信信息
* 此处不应加缓存,否则解绑时不好清除
* @param userId - 用户标识
* @return
*/
@Override @Override
@Cacheable(value = "WechatUserInfo", key = "'userId:' + #userId", unless = "#result == null", cacheManager = "cacheManager")
public WechatUserInfo queryByUserId(Long userId) { public WechatUserInfo queryByUserId(Long userId) {
return weChatUserRepository.findByUserId(userId); return weChatUserRepository.findByUserId(userId);
} }
@Override @Override
@CacheEvict(value = "WechatUserInfo", key = "'userId:' + #userId", cacheManager = "cacheManager")
public int forbiddenUserWeChat(Long userId) { public int forbiddenUserWeChat(Long userId) {
return weChatUserRepository.removeByUserId(userId); return weChatUserRepository.removeByUserId(userId);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment