微信扫码登录

一:什么是三方登录

 三方登录指的是基于用户在主流平台【微信、支付宝、QQ】上已有账号和密码来快速完成对己方应用的登录或注册的功能,三方登录的目的在于使用用户在主流平台的账号无需注册就能登录己方应用,这样可以扩大用户群,实现引流,同时用户体验度较高

二:微信登录接口

  要想实现微信登录,需要先行在微信开放平台https://open.weixin.qq.com/注册企业账号,然后创建应用等待微信审批,审批通过后微信会返回一个appid和secret,通过appid我们可以获取当前扫码登录用户的授权码,通过appid和secret以及授权码我们可以获取令牌,,根据令牌我们就可以获取到当前扫码用户的信息

三:微信登录原理

  1.用户访问第三方应用,请求微信扫码登录,第三方应用将会发送第一次请求,请求微信OAuth2.0授权登录,微信将绕过第三方应用直接向用户请求确认,用户确认登录后,微信根据redirect_uri重定向到第三方应用,并带上返回的授权码

  https://open.weixin.qq.com/connect/qrconnect?appid=wxd853562a0548a7d0&redirect_uri=http://bugtracker.itsource.cn/callback.html&response_type=code&    scope=snsapi_login&state=STATE#wechat_redirect

  需要注意的是,appid为微信开放平台审批通过返回给开发者的,redirect_uri为用户扫码后微信返回授权码的页面,一般在这个页面我们不做任何内容,直接在钩子函数中获取到地址栏的授权码然后发送给后端,让后端进行处理

 2.后端接口拿到了授权码后,业务层将通过授权码、appid、secret发送第二次请求,微信将会返回一个json对象,主要有用的就是access_token和openidopenid是唯一的,可以作为数据库的唯一标识去查询此用户是第一次登录还是已经扫码登录过本应用,如果是第一次登录,就要进行信息绑定,后端应返回相应的信息和access_token以及openid交给前端保存,前端获取后应将后端返回的由access_token和openid拼接成的字符串拼接到绑定信息界面的url地址栏进行储存,直接跳转页面到绑定信息界面,如果不是第一次登录,后端应直接免密通过,同时将此用户信息从logininfo查出来并通过uuid作为key存入Redis,设置30分钟过期,然后将这个token和对象返回给前端,前端获取到后将token和对象存入localStorage,然后跳转首页

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

  3.需要注意的是,第三次请求不是必须的,而是微信扫码登录用户第一次登录本应用才会使用,主要是为了通过令牌获取用户信息并存入数据库,不是第一次登录就不需要,直接免密登录即可,用户进入绑定信息界面,输入信息提交后后台作出信息校验,校验无误将发送第三次请求,通过access_token和openid发送第三次请求获取当前用户的信息,将返回的用户信息存入数据库后再做免密登录

  https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

  总的来说,微信扫码登录第一次请求是由前端发送,余下两次请求都是由后端发送,第一次是为了获取二维码和授权码,第二次是为了通过授权码和appid和secret获取access_token、openid、unionid【令牌】第三次是为了通过access_token和openid获取用户信息,写代码时围绕这三次请求来写即可,参考微信开放平台

四:代码

HttpUtil工具类:

package cn.ybl.basic.util;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;

/**
 * http 工具类
 */
public class HttpUtil {

    public static String post(String requestUrl, String accessToken, String params)
            throws Exception {
        String contentType = "application/x-www-form-urlencoded";
        return HttpUtil.post(requestUrl, accessToken, contentType, params);
    }

    public static String post(String requestUrl, String accessToken, String contentType, String params)
            throws Exception {
        String encoding = "UTF-8";
        if (requestUrl.contains("nlp")) {
            encoding = "GBK";
        }
        return HttpUtil.post(requestUrl, accessToken, contentType, params, encoding);
    }

    public static String post(String requestUrl, String accessToken, String contentType, String params, String encoding)
            throws Exception {
        String url = requestUrl + "?access_token=" + accessToken;
        return HttpUtil.postGeneralUrl(url, contentType, params, encoding);
    }

    public static String postGeneralUrl(String generalUrl, String contentType, String params, String encoding)
            throws Exception {
        URL url = new URL(generalUrl);
        // 打开和URL之间的连接
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        // 设置通用的请求属性
        connection.setRequestProperty("Content-Type", contentType);
        connection.setRequestProperty("Connection", "Keep-Alive");
        connection.setUseCaches(false);
        connection.setDoOutput(true);
        connection.setDoInput(true);

        // 得到请求的输出流对象
        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
        out.write(params.getBytes(encoding));
        out.flush();
        out.close();

        // 建立实际的连接
        connection.connect();
        // 获取所有响应头字段
        Map<String, List<String>> headers = connection.getHeaderFields();
        // 遍历所有的响应头字段
        for (String key : headers.keySet()) {
            System.err.println(key + "--->" + headers.get(key));
        }
        // 定义 BufferedReader输入流来读取URL的响应
        BufferedReader in = null;
        in = new BufferedReader(
                new InputStreamReader(connection.getInputStream(), encoding));
        String result = "";
        String getLine;
        while ((getLine = in.readLine()) != null) {
            result += getLine;
        }
        in.close();
        System.err.println("result:" + result);
        return result;
    }
    
    /**
     * 发送get请求
     * @param url 请求地址
     * @return 返回内容 json
     */
    public static String httpGet(String url){

        // 1 创建发起请求客户端
        try {
            HttpClient client = new HttpClient();
            // 2 创建要发起请求-tet
            GetMethod getMethod = new GetMethod(url);
//            getMethod.addRequestHeader("Content-Type",
//                    "application/x-www-form-urlencoded;charset=UTF-8");
            getMethod.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET,"utf8");
            // 3 通过客户端传入请求就可以发起请求,获取响应对象
            client.executeMethod(getMethod);
            // 4 提取响应json字符串返回
            String result = new String(getMethod.getResponseBodyAsString().getBytes("utf8"));
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

获取二维码和授权码:

<li>
    <a target="_blank" href="https://open.weixin.qq.com/connect/qrconnect?appid=wxd853562a0548a7d0&redirect_uri=http://bugtracker.itsource.cn/callback.html&response_type=code&scope=snsapi_login&state=STATE#wechat_redirect">
        <i class="am-icon-weixin am-icon-sm"></i><span>微信登录</span>
    </a>
</li>

用户扫码后,微信开放平台将根据redirect_uri响应到我们的应用并在地址栏带上授权码code,所以我们需要准备这个界面,需要注意的是,微信访问不到未上线的127.0.0.1地址,我们需要根据申请时的地址在system32——drivers——etc——hosts中作映射,将申请到的地址映射到127.0.0.1即可

接收微信响应界面准备

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!--引入vue-->
    <script src="js/vue/dist/vue.js"></script>
    <!--引入axios-->
    <script src="js/axios/dist/axios.js"></script>
    <script src="js/common.js"></script>
</head>
<body>
<!--接收微信开发平台返回的二维码用户登录后的页面,没有任何作用,只是为了给后台发送请求,将code发过去-->
<div id="app"></div>
</body>
<script type="text/javascript">
    new Vue({
        el:"#app",
        mounted(){
            /*用户扫码登录后返回的地址栏:
            http://bugtracker.itsource.cn/callback.html?code=071G7oml2je6B94B6col2pZjOF2G7omk&state=STATE
            */
            //页面只要加载就发送请求到后端
            var href = location.href
            //使用抽取的工具方法截取授权码code:code=071G7oml2je6B94B6col2pZjOF2G7omk
            var jsonObj = parseUrlParams2Obj(href)
            //获取授权码
            var code = jsonObj.code
            //发送请求到后端,携带授权码code
            this.$http.get("/login/weChat/"+code).then(res=>{
                if(res.data.resultObj.token!=null){    //不是第一次登录
                    var token = res.data.resultObj.token
                    var logininfo = res.data.resultObj.logininfo
                    localStorage.setItem("token",token)
                    localStorage.setItem("logininfo",JSON.stringify(logininfo))
                    location.href="index.html"
                }else{
                    //第一次登录,需要绑定信息,获取后端返回的access_token和openid拼成的字符串
                    var param = res.data.resultObj
                    //跳转绑定界面
                    location.href="binder.html"+param
                }
            })
        }
    })
</script>
</html>

抽取的工具方法【放在了common.js中】:

//封装公用方法 - 将字符串的参数解析为json对象
function parseUrlParams2Obj(url) {
    //url:http://bugtracker.itsource.cn/callback.html?code=071G7oml2je6B94B6col2pZjOF2G7omk&state=STATE
    let paramStr = url.substring(url.indexOf("?")+1);//xxx?code=xxx&state=1
    let paramArr = paramStr.split("&");
    let paramObj = {};
    for(let i = 0;i<paramArr.length;i++){
        let paramTemp = paramArr[i];
        let paramName = paramTemp.split("=")[0];
        let paramValue= paramTemp.split("=")[1];
        paramObj[paramName] = paramValue;
    }
    return paramObj;
}

 后端接口:

/**
     * 微信登录
     * @param code
     * @return AjaxResult
     */
    @GetMapping("/weChat/{code}")
    public AjaxResult weChat(@PathVariable("code") String code){
        try {
             return loginInfoService.weChat(code);
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.getResult().setMsg("网络繁忙");
        }
    }

后端业务【发送第二次请求】:

/**
     * 微信登录
     * @param code
     * @return AjaxResult
     */
    @Override
    public AjaxResult weChat(String code) {
        /*1.向微信发送请求
            根据授权码code和appid和secret获取令牌token【使用字符串替换方法replace替换请求中的数据】
         */
        String str = HttpUtil.httpGet(WeChatConstant.GET_ACK_URL
                .replace("APPID",WeChatConstant.APPID)
                .replace("SECRET",WeChatConstant.SECRET)
                .replace("CODE",code));
        //使用alibaba的fastjson字符串转json对象工具
        JSONObject jsonObject = JSONObject.parseObject(str);
        //获取令牌中的access_token和openid
        String access_token = jsonObject.getString("access_token");
        String openid = jsonObject.getString("openid");
        //根据openid查询t_wxuser表,判断是否微信扫码登录过本应用
        WxUser wxUser = wxUserMapper.loadByOpenId(openid);
        //如果不为空,说明不是第一次登录
        if(wxUser!=null&&wxUser.getUser_id()!=null){
            //根据user_id多表查询返回logininfo对象,将这个对象响应给前端
            LoginInfo loginInfo = loginInfoMapper.findByUserId(wxUser.getUser_id());
            loginInfo.setPassword(null);
            loginInfo.setSalt(null);
            String token = UUID.randomUUID().toString();
            redisTemplate.opsForValue().set(token,loginInfo,30,TimeUnit.MINUTES);
            Map<String, Object> map = new HashMap<>();
            map.put("token",token);
            map.put("logininfo",loginInfo);
            return AjaxResult.getResult().setResultObj(map);
        }
        //第一次登录,未绑定信息,需要先进行信息绑定,需要将令牌返回给前端保存
        else{
            String param = "?accessToken="+access_token+"&openId="+openid+"";
            return AjaxResult.getResult().setResultObj(param);
        }
    }

=========================非第一次登录功能到此完成,接下来为第一次登录绑定数据处理=======================

绑定数据界面binder.html:

<!DOCTYPE html>
<html>

<head lang="en">
    <meta charset="UTF-8">
    <title>注册</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="format-detection" content="telephone=no">
    <meta name="renderer" content="webkit">
    <meta http-equiv="Cache-Control" content="no-siteapp" />

    <link rel="stylesheet" href="./AmazeUI-2.4.2/assets/css/amazeui.min.css" />
    <link href="./css/dlstyle.css" rel="stylesheet" type="text/css">
    <script src="./AmazeUI-2.4.2/assets/js/jquery.min.js"></script>
    <script src="./AmazeUI-2.4.2/assets/js/amazeui.min.js"></script>

    <!--引入vue-->
    <script src="js/vue/dist/vue.js"></script>
    <!--引入axios-->
    <script src="js/axios/dist/axios.js"></script>
    <script src="js/common.js"></script>

</head>
<body>
<div class="login-boxtitle">
    <a href="home/demo.html"><img alt="" src="./images/logobig.png" /></a>
</div>

<div class="res-banner">
    <div class="res-main">
        <div class="login-banner-bg"><span></span><img src="./images/big.jpg" /></div>
        <div class="login-box">
            <div class="am-tabs" id="doc-my-tabs">
                <div class="am-tabs-bd">
                    <div class="am-tab-panel am-active"id="myDiv" >
                        <form method="post">
                            <div class="user-phone">
                                <label for="phone"><i class="am-icon-mobile-phone am-icon-md"></i></label>
                                <input type="tel" name="" id="phone" v-model="phoneUserForm.phone" placeholder="请输入手机号">
                            </div>
                            <div class="verification">
                                <label for="code"><i class="am-icon-code-fork"></i></label>
                                <input type="tel" name="" id="code" v-model="phoneUserForm.verifyCode" placeholder="请输入验证码">
                                <!--<a class="btn" href="javascript:void(0);" onclick="sendMobileCode();" id="sendMobileCode">-->
                                <!--<span id="dyMobileButton">获取</span></a>-->
                                <button type="button" id="dyMobileButton" @click="sendMobileCode">获取</button>
                            </div>
                        </form>
                        <div class="login-links">
                            <label for="reader-me">
                                <input id="reader-me" type="checkbox"> 点击表示您同意商城《服务协议》
                            </label>
                        </div>
                        <div class="am-cf">
                            <input type="button" @click="binder" name="" value="绑定授权" class="am-btn am-btn-primary am-btn-sm am-fl">
                        </div>

                        <hr>
                    </div>

                    <script>
                        $(function() {
                            $('#doc-my-tabs').tabs();
                        })
                    </script>

                </div>
            </div>

        </div>
    </div>

    <div class="footer ">
        <div class="footer-hd ">
            <p>
                <a href="# ">恒望科技</a>
                <b>|</b>
                <a href="# ">商城首页</a>
                <b>|</b>
                <a href="# ">支付宝</a>
                <b>|</b>
                <a href="# ">物流</a>
            </p>
        </div>
        <div class="footer-bd ">
            <p>
                <a href="# ">关于恒望</a>
                <a href="# ">合作伙伴</a>
                <a href="# ">联系我们</a>
                <a href="# ">网站地图</a>
                <em>© 2015-2025 Hengwang.com 版权所有. 更多模板 <a href="http://www.cssmoban.com/" target="_blank" title="模板之家">模板之家</a> - Collect from <a href="http://www.cssmoban.com/" title="网页模板" target="_blank">网页模板</a></em>
            </p>
        </div>
    </div>
</div>
</body>

<script type="text/javascript">
    new Vue({
        "el":"#myDiv",
        data:{
            phoneUserForm:{
                phone:"",
                //验证码
                verifyCode:"",
                accessToken:"",
                openId:""
            }
        },
        methods:{
            binder(){
                this.$http.post("/login/wechat/binder",this.phoneUserForm).then(result=>{
                    result = result.data;
                    if(result.success){
                        alert("绑定成功!")
                        //1.保存返回的token,logininfo到localStorage
                        // 结构对象
                        let {token,logininfo} = result.resultObj;
                        localStorage.setItem("token",token);
                        localStorage.setItem("logininfo",JSON.stringify(logininfo));
                        location.href = "/index.html"; //注册成功后跳转登录页面
                    }else{
                        alert(result.message)
                    }
                }).catch(result=>{
                    alert("系统错误!");
                })
            },
            sendMobileCode(){
                //1.判断手机号不为空
                if(!this.phoneUserForm.phone){
                    alert("手机号不能为空");
                    return;
                }
                //2.获取按钮,禁用按钮  发送时灰化不能使用,发送成功倒计时60才能使用,如果发送失败立即可以发送
                var sendBtn = $(event.target);
                sendBtn.attr("disabled",true);
                this.$http.post('/verifyCode/binderSmsCode/'+this.phoneUserForm.phone).then((res) => {

                    if(res.data.success){
                        alert("手机验证码已经发送到您的手机,请在3分钟内使用");
                        //3.1.发送成:倒计时
                        var time = 60;
                        var interval = window.setInterval( function () {
                            //每一条倒计时减一
                            time = time - 1 ;
                            //把倒计时时间搞到按钮上
                            sendBtn.html(time);
                            //3.2.倒计时完成恢复按钮
                            if(time <= 0){
                                sendBtn.html("重发");
                                sendBtn.attr("disabled",false);
                                //清除定时器
                                window.clearInterval(interval);
                            }
                        },1000);
                    }else{
                        //3.3.发送失败:提示,恢复按钮
                        sendBtn.attr("disabled",false);
                        alert("发送失败:"+res.data.msg);
                    }
                });
            }
        },
        mounted(){
            let paramObj = parseUrlParams2Obj(location.href);
            if(paramObj){
                this.phoneUserForm.accessToken = paramObj.accessToken;
                this.phoneUserForm.openId = paramObj.openId;
            }
        }
    })
</script>

</html>

用户点击获取时需要发送验证码,后端发送验证码接口:

 /**
     * 第一次微信扫码登录时绑定手机号发送验证码
     * @param phone
     * @return AjaxResult
     */
    @PostMapping("/binderSmsCode/{phone}")
    public AjaxResult binderSmsCode(@PathVariable("phone") String phone){
        try {
            verifyCodeService.binderSmsCode(phone);
            return AjaxResult.getResult();
        }
        catch (BusinessException e) {
            e.printStackTrace();
            return AjaxResult.getResult().setMsg(e.getMessage());
        }
        catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.getResult().setMsg("系统异常");
        }
    }

发送验证码业务层:

/**
     * 微信第一次登录绑定手机发送验证码
     * @param phone
     * @return
     */
    @Override
    public void binderSmsCode(String phone) {
        if(StringUtils.isEmpty(phone)){
            throw new BusinessException("手机号不能为空");
        }
        if(userService.findByPhone(phone)!=null){
            throw new BusinessException("此手机号已经绑定,请更换手机号");
        }
        //从Redis中取值 value===code:时间戳
        Object value = redisTemplate.opsForValue().get(phone + "binder");
        String code = null;
        if(value!=null){
            code = value.toString().split(":")[0];
            //截取,得到Redis中验证码的时间戳
            String time = value.toString().split(":")[1];
            Long codeTime = Long.valueOf(time);
            if(System.currentTimeMillis()-codeTime<1*60*1000){
                throw new BusinessException("请勿重复发送");
            }
        }else{
            //重新生成验证码并添加到数据库
            code = StrUtils.getComplexRandomString(4);
            redisTemplate.opsForValue().set(phone+"binder",code+":"+System.currentTimeMillis(),3,TimeUnit.MINUTES);
        }
        //发送短信
        //SmsUtils.sendSms(map.get("phone"),"验证码为"+code);
        System.out.println("用户:"+phone+",您的验证码为:"+code);
    }

用户得到验证码后点击绑定授权按钮,发送请求到后端:

/**
     * 微信第一次登录绑定信息
     * @param wxBinderDto
     * @return
     */
    @PostMapping("/wechat/binder")
    public AjaxResult weChatBinder(@RequestBody WxBinderDto wxBinderDto){
        try {
            return loginInfoService.weChatBinder(wxBinderDto);
        }
        catch (BusinessException e) {
            e.printStackTrace();
            return AjaxResult.getResult().setMsg(e.getMessage());
        }
        catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.getResult().setMsg("系统异常");
        }
    }

业务层绑定数据处理【发送第三次请求】:

/**
     * 微信第一次登录绑定信息
     * @param wxBinderDto
     * @return AjaxResult
     */
    @Override
    public AjaxResult weChatBinder(WxBinderDto wxBinderDto) {
        String phone = wxBinderDto.getPhone(); //手机号
        String verifyCode = wxBinderDto.getVerifyCode();  //验证码
        String accessToken = wxBinderDto.getAccessToken();  //access_token
        String openId = wxBinderDto.getOpenId();  //openid
        if(StringUtils.isEmpty(phone)){
            throw new BusinessException("手机号不能为空");
        }
        if(StringUtils.isEmpty(verifyCode)){
            throw new BusinessException("验证码不能为空");
        }
        Object code = redisTemplate.opsForValue().get(phone + "binder");
        if(code==null){
            throw new BusinessException("验证码已过期,请重新获取");
        }
        if(!code.toString().split(":")[0].equalsIgnoreCase(verifyCode)){
            throw new BusinessException("验证码错误");
        }

        //根据令牌的access_token和openid发送第三个请求向微信获取用户信息
        String str = HttpUtil.httpGet(WeChatConstant.GET_USER_URL
                .replace("ACCESS_TOKEN", accessToken)
                .replace("OPENID", openId));
        //将字符串转为json对象
        JSONObject userInfo = JSONObject.parseObject(str);
        WxUser wxUser = jsonToWxUser(userInfo);

        User user = userMapper.findByPhone(phone);
        if(user==null){  //判断user表中有没有这个用户
            user = phoneToUser(phone);
            LoginInfo loginInfo = user2Logininfo(user);
            loginInfoMapper.add(loginInfo);
            user.setLogininfo_id(loginInfo.getId());
            userMapper.add(user);
        }
        wxUser.setUser_id(String.valueOf(user.getId()));
        wxUserMapper.add(wxUser);

        //免密登录
        LoginInfo loginInfo = loginInfoMapper.loadById(Long.valueOf(user.getLogininfo_id()));
        String token = UUID.randomUUID().toString();
        loginInfo.setPassword(null);
        loginInfo.setSalt(null);
        redisTemplate.opsForValue().set(token,loginInfo,30,TimeUnit.MINUTES);
        Map<Object, Object> map = new HashMap<>();
        map.put("token",token);
        map.put("logininfo",loginInfo);
        return AjaxResult.getResult().setResultObj(map);
    }


    //=====================================工具方法===========================================
    //json对象生成wxuser对象
    private WxUser jsonToWxUser(JSONObject jsonObject){
        WxUser wxUser = new WxUser();
        wxUser.setHeadImgUrl(jsonObject.getString("headimgurl"));
        wxUser.setNickName(jsonObject.getString("nickname"));
        wxUser.setOpenId(jsonObject.getString("openid"));
        wxUser.setUnionId(jsonObject.getString("unionid"));
        wxUser.setSex(Integer.valueOf(jsonObject.getString("sex")));
        wxUser.setAddress(jsonObject.getString("country")
                +jsonObject.getString("province")
                +jsonObject.getString("city"));
        return wxUser;
    }

    //根据phone生成一个User对象
    private User phoneToUser(String phone){
        User user = new User();
        user.setUsername(phone);
        user.setPhone(phone);
        return user;
    }

    //根据user生成logininfo
    private LoginInfo user2Logininfo(User user) {
        LoginInfo logininfo = new LoginInfo();
        BeanUtils.copyProperties(user,logininfo);
        logininfo.setType(1);
        return logininfo;
    }

============================================到此微信登录完成======================================

posted @ 2022-08-01 10:51  yyybl  阅读(301)  评论(0)    收藏  举报