说明:本篇博客本人安装小白思路进行书写,以下内容及第三方跳转环境主要来源于余胜军,请大家在转载的时候说明来源出处。
一、实现思路
1.在登录页面,从后台查询出可以使用的联合登录接口(含第三方登录头像、requestAddress)
2、点击头像,进入QQ扫码界面
3、QQ扫码后,根据requestAddress中的回调地址,进入后台联合登录自定义的回调方法unionLoginCallback
4、回调方法中获取用户的openId 组装后返回到定义的前台关联页面(根据openId查询是否用户以关联,如果关联就把用户信息传递到关联账号页面,用户点击时直接跳转至首页,以下流程是没有进行关联的情况)
5、点击《关联到已有账号》跳转到关联账号页面,并把用户openId传递到该页面
6、用户在关联账号页面输入该平台的登录账号密码,并把该用户的openId一起传递到后台,该调用方法为该平台的登录接口,如果输入的账号密码正确,则将该用户的openId添加到该用户对应的数据库数据中并跳转至首页。
二、开发代码
(一)数据库数据
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50647 Source Host : localhost:3306 Source Schema : cyb Target Server Type : MySQL Target Server Version : 50647 File Encoding : 65001 Date: 12/04/2020 02:42:17 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for meite_user -- ---------------------------- DROP TABLE IF EXISTS `meite_user`; CREATE TABLE `meite_user` ( `USER_ID` int(12) NOT NULL AUTO_INCREMENT COMMENT 'user_id', `MOBILE` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '手机号', `PASSWORD` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码', `USER_NAME` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名', `SEX` tinyint(1) NULL DEFAULT 0 COMMENT '性别 1男 2女', `AGE` tinyint(3) NULL DEFAULT 0 COMMENT '年龄', `CREATE_TIME` timestamp(0) NULL DEFAULT NULL COMMENT '注册时间', `IS_AVALIBLE` tinyint(1) NULL DEFAULT 1 COMMENT '是否可用 1正常 2冻结', `PIC_IMG` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户头像', `QQ_OPENID` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'QQ联合登陆id', `WX_OPENID` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '微信公众号关注id', PRIMARY KEY (`USER_ID`) USING BTREE, UNIQUE INDEX `MOBILE_UNIQUE`(`MOBILE`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 87 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户会员表' ROW_FORMAT = Compact; -- ---------------------------- -- Records of meite_user -- ---------------------------- INSERT INTO `meite_user` VALUES (86, '18774833827', 'E10ADC3949BA59ABBE56E057F20F883E', '1', 1, 0, '2020-03-15 22:34:45', 0, '1', '3EAB229E0EAAB047174224A5845B224E', 'oOX38w3WD3JUjL5ORcr4OADNqfSw'); SET FOREIGN_KEY_CHECKS = 1;
(二)联合登录接口
import com.cyb.base.BaseResponse; import com.cyb.member.api.dto.resp.UnionLoginDto; import io.swagger.annotations.Api; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.util.List; @Api(tags = "联合登陆接口") public interface MemberUnionLoginService { /** * 根据不同的联合登陆id * * @param unionPublicId * @return */ @GetMapping("/unionLogin") BaseResponseunionLogin(@RequestParam("unionPublicId") String unionPublicId); /** * 联合登陆回调接口 * * @return */ @GetMapping("/login/oauth/callback") String unionLoginCallback(@RequestParam("unionPublicId") String unionPublicId); /** * 查询当前开通的渠道 * * @return */ @GetMapping("/unionLoginList") @ResponseBody BaseResponse > unionLoginList(); }
(三)联合登录实现类
import com.alibaba.fastjson.JSONObject; import com.cyb.base.BaseApiService; import com.cyb.base.BaseResponse; import com.cyb.bean.CybBeanUtils; import com.cyb.member.api.dto.resp.UnionLoginDto; import com.cyb.member.api.service.MemberUnionLoginService; import com.cyb.member.impl.entitydo.UnionLoginDo; import com.cyb.member.impl.mapper.UnionLoginMapper; import com.cyb.member.impl.strategy.UnionLoginStrategy; import com.cyb.utils.SpringContextUtils; import com.cyb.utils.TokenUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.CrossOrigin; 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 java.util.List; //@RestController @Controller @CrossOrigin public class MemberUnionLoginServiceImpl extends BaseApiService implements MemberUnionLoginService { @Autowired private UnionLoginMapper unionLoginMapper; @Autowired private TokenUtils tokenUtils; @Value("${cyb.login.vue.bindingurl}") private String bindingurl; @Override public BaseResponseunionLogin(String unionPublicId) { if (StringUtils.isEmpty(unionPublicId)) { return setResultError("unionPublicId不能为空"); } // 根据渠道id查询 联合基本信息 UnionLoginDo unionLoginDo = unionLoginMapper.selectByUnionLoginId(unionPublicId); if (unionLoginDo == null) { return setResultError("该渠道可能已经关闭或者不存在"); } String state = tokenUtils.createToken("member.unionLogin", ""); String requestAddres = unionLoginDo.getRequestAddress() + "&state=" + state; JSONObject dataObjects = new JSONObject(); dataObjects.put("requestAddres", requestAddres); return setResultSuccess(dataObjects); } @Override public String unionLoginCallback(String unionPublicId) { // 根据渠道id查询 联合基本信息 UnionLoginDo unionLoginDo = unionLoginMapper.selectByUnionLoginId(unionPublicId); String unionBeanId = unionLoginDo.getUnionBeanId(); // 从Spring容器中根据beanid 查找到我们的策略类 UnionLoginStrategy unionLoginStrategy = SpringContextUtils.getBean(unionBeanId, UnionLoginStrategy.class); // 根据当前线程获取request对象 HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); String openId = unionLoginStrategy.unionLoginCallback(request, unionLoginDo); JSONObject jsonObject = new JSONObject(); jsonObject.put("openId", openId); jsonObject.put("unionPublicId", unionPublicId); String openToken = tokenUtils.createToken("mayikt.unionLogin.", jsonObject.toJSONString()); return "redirect:" + bindingurl + openToken; } @Override public BaseResponse > unionLoginList() { List
unionLoginList = unionLoginMapper.selectByUnionLoginList(); if (unionLoginList == null) { return setResultError("当前没有可用渠道"); } List unionLoginDtos = CybBeanUtils.doToDtoList(unionLoginList, UnionLoginDto.class); return setResultSuccess(unionLoginDtos); } }
(四)联合登录策略模式接口
/* * 联合登录回调 * @Author 陈远波 * @Date 2020-04-12 * @param null * @return */ String unionLoginCallback(HttpServletRequest request, UnionLoginDo unionLoginDo); /* * 根据用户openID获取渠道信息 * @Author 陈远波 * @Date 2020-04-12 * @param openId 用户openID * @return */ UserDo getDbOpenId(String openId); /* * 根据用户id修改用户的openID * @Author 陈远波 * @Date 2020-04-12 * @param userId 用户id * @param openId 用户openId * @return */ int updateUseOpenId(Long userId,String openId); }
(五)QQ联合登录策略模式实现类
@Component public class QQUnionLoginStrategy implements UnionLoginStrategy { @Value("${cyb.login.qq.accesstoken}") private String qqAccessTokenAddres; @Value("${cyb.login.qq.openid}") private String qqOpenIdAddres; @Autowired private UserMapper userMapper; @Override public String unionLoginCallback(HttpServletRequest request, UnionLoginDo unionLoginDo) { String code = request.getParameter("code"); if (StringUtils.isEmpty(code)) { return null; } //1.根据授权码获取accessToken // 1.根据授权码获取accessToken String newQQAccessTokenAddres = qqAccessTokenAddres.replace("{client_id}" , unionLoginDo.getAppId()).replace("{client_secret}", unionLoginDo.getAppKey()). replace("{code}", code).replace("{redirect_uri}", unionLoginDo.getRedirectUri()); String resultAccessToken = HttpClientUtils.httpGetResultString(newQQAccessTokenAddres); boolean contains = resultAccessToken.contains("access_token="); if (!contains) { return null; } String[] split = resultAccessToken.split("="); String accessToken = split[1]; if (StringUtils.isEmpty(accessToken)) { return null; } // 2.根据accessToken获取用户的openid String resultQQOpenId = HttpClientUtils.httpGetResultString(qqOpenIdAddres + accessToken); if (StringUtils.isEmpty(resultQQOpenId)) { return null; } boolean openid = resultQQOpenId.contains("openid"); if (!openid) { return null; } String array[] = resultQQOpenId.replace("callback( {", "").replace("} );", "").replace("\"", "").trim().split(":"); String openId = array[2]; return openId; } @Override public UserDo getDbOpenId(String openId) { return userMapper.selectByQQOpenId(openId); } @Override public int updateUseOpenId(Long userId, String openId) { return userMapper.updateUserOpenId(userId,openId); } }
(六)登录接口
@RestController @Api(tags = "会员登录服务") public interface MemberLoginService { /* * * @Author 陈远波 * @Date 2020-03-25 * @param @RequestHeader("X-Real-IP") 从nginx请求头中获取 浏览器真实ip * @RequestHeader("channel") 从请求头中获取登录来源 pc,安卓,iOS * @return */ @PostMapping("/login") @ApiOperation(value = "会员登录",notes = "接收参数进行序列化") BaseResponselogin(@RequestBody UserLoginDto userLoginDto, @RequestHeader("X-Real-IP") String sourceIp, @RequestHeader("channel") String channel, @RequestHeader("deviceInfor") String deviceInfor); }
登录接口实现
@RestController @Slf4j @CrossOrigin public class MemberLoginServiceImpl extends BaseApiService implements MemberLoginService { @Autowired private UserMapper userMapper; @Autowired private TokenUtils tokenUtils; @Value("${cyb.login.token.prefix}") private String loginTokenPrefix; @Autowired private AsyncLoginLogManage asyncLoginLogManage; @Autowired private UserLoginLogMapper userLoginLogMapper; @Autowired private ChannelUtils channelUtils; @Override public BaseResponselogin(UserLoginDto userLoginDto, String sourceIp , String channel, String deviceInfor) { // 参数验证 String mobile = userLoginDto.getMobile(); if (StringUtils.isEmpty(mobile)) { return setResultError("mobile参数不能为空"); } String passWord = userLoginDto.getPassWord(); if (StringUtils.isEmpty(userLoginDto.getPassWord())) { return setResultError("passWord参数不能为空"); } if (!channelUtils.existChannel(channel)) { return setResultError("登陆类型出现错误!"); } // 查询我们的数据库 String newPassWord = MD5Util.MD5(passWord); UserDo loginUserDo = userMapper.login(mobile,newPassWord); if (loginUserDo == null) { return setResultError("手机号码或者密码不正确!"); } // 设备信息 if (StringUtils.isEmpty(deviceInfor)) { return setResultError("设备信息不能为空!"); } //获取userId Long userId = loginUserDo.getUserId(); String userToken = tokenUtils.createToken(loginTokenPrefix, userId+""); JSONObject resultJSON = new JSONObject(); resultJSON.put("userToken", userToken); String wxOpenId = loginUserDo.getWxOpenId(); String openIdToken = userLoginDto.getOpenIdToken(); // 写入日志 log.info(Thread.currentThread().getName() + " 处理流程1"); asyncLoginLogManage.loginLog(openIdToken,wxOpenId, mobile,userId, sourceIp, new Date(), userToken , channel, deviceInfor); log.info(Thread.currentThread().getName() + " 处理流程3"); return setResultSuccess(resultJSON); } public void loginLog(Long userId, String loginIp, Date loginTime, String loginToken, String channel, String equipment) { UserLoginLogDo userLoginLogDo = new UserLoginLogDo(userId, loginIp, loginTime, loginToken, channel, equipment); log.info(Thread.currentThread().getName() + ",userLoginLogDo:" + userLoginLogDo.toString() + ",流程2"); userLoginLogMapper.insertUserLoginLog(userLoginLogDo); log.info(Thread.currentThread().getName() + " 处理流程2"); } }
(七)配置文件
cyb: login: token: prefix: memberlogin channel: pc,android,ios qq: accesstoken: https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri} openid: https://graph.qq.com/oauth2.0/me?access_token= wx: accesstoken: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code vue: bindingurl: http://127.0.0.1:8849/mayikt_mt_shop/relation_login.html?openIdToken=
三、注意事项:
在此过程中会遇到跨域的错误
解决办法有很多,在此本人使用注解的形式解决,具体解决方案有:
1.在响应头中设置允许跨域的 只适合于小公司
响应配置response.setHeader("Access-Control-Allow-Origin", "*");
2.使用HttpClient转发 效率低
3.使用jsonp处理,jsonp最大的缺陷支持get请求不支持post请求
4.使用nginx配置浏览器访问的项目与接口项目的域名或者端口号码一致性。
www.mayikt.com/vue 转发到vue项目
www.mayikt.com/api 转发到接口项目
5.可以直接在nginx中配置允许跨域的代码
"Access-Control-Allow-Origin", "*"
6.网关中也可以配置类似与nginx允许跨域的代码
"Access-Control-Allow-Origin", "*"
7.使用SpringBoot注解形式解决跨域问题@CrossOrigin
8.使用微服务网关也可以配置配置浏览器访问的项目与接口项目的域名或者端口号码一致性。
四、效果展示
2、点击第三方登录跳转到扫码界面
手机扫码时手机界面
进入确认关联界面
输入登录账号密码进行关联
如果对以上内容有所疑问的可以关注留言,转载请说明出处