說明:本篇博客本人安裝小白思路進行書寫,以下內容及第三方跳轉環境主要來源於餘勝軍,請大家在轉載的時候說明來源出處。
一、實現思路
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") BaseResponse<String> unionLogin(@RequestParam("unionPublicId") String unionPublicId); /** * 聯合登陸回調接口 * * @return */ @GetMapping("/login/oauth/callback") String unionLoginCallback(@RequestParam("unionPublicId") String unionPublicId); /** * 查詢當前開通的渠道 * * @return */ @GetMapping("/unionLoginList") @ResponseBody BaseResponse<List<UnionLoginDto>> 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 BaseResponse<String> unionLogin(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<List<UnionLoginDto>> unionLoginList() { List<UnionLoginDo> unionLoginList = unionLoginMapper.selectByUnionLoginList(); if (unionLoginList == null) { return setResultError("當前沒有可用渠道"); } List<UnionLoginDto> 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 = "接收參數進行序列化") BaseResponse<JSONObject> login(@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 BaseResponse<JSONObject> login(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.使用微服務網關也可以配置配置瀏覽器訪問的項目與接口項目的域名或者端口號碼一致性。
四、效果展示
1、登錄首頁
2、點擊第三方登錄跳轉到掃碼界面
手機掃碼時手機界面
進入確認關聯界面
輸入登錄賬號密碼進行關聯
如果對以上內容有所疑問的可以關注留言,轉載請說明出處