同步用户信息到服务端,服务端登录
多多洛
上一章节中,获取的用户信息,用户是否登录这些都是微信处理的,大多数情况,我们希望知道用户是否登录了我们的服务端,我们想将微信里面的用户信息跟我们的服务端同步,这一章就讲讲如何保存用户信息到服务端。
基本流程就是这个图:



有的同学会问,我们已经获取到用户的头像了,昵称了,还有一些其他信息,直接传到服务端不就可以了,首先这么做不安全,其次,我们没有一个始终唯一的标识来标记这个用户,我们要找到用户唯一的东西
确认用户下次授权微信登录的时候我们还能获取到这个唯一的标识,这个唯一标识就是openId,有的人又说openId不是唯一的,是的,unionid才是唯一的,但是openid对单独的应用是唯一的,就比如一个小程序是唯一的,所以用openid就足够了。
如果考虑到有多个小程序或者公众号,想把这个用户在所有应用中都是唯一的,那么就要用unionid了。而且未认证的公众号是不可以获取unionid,所以个人的小程序是无法获取unionid的,所以就用openid吧。

1.微信端

我们还是直接看代码,我们还是那上一章的栗子继续写,上一章只是获取了微信的信息,没有授权到我们的服务端。
wxml:
<view class="container">
   <view class='avatar'><image src="{{userIcon}}"></image></view>
   <view class="user-name">{{userName}}</view>
   <block wx:if="{{!haveLogin}}">
      <button  bindtap="openSetting"  class='btn'>点我登录</button>
   </block>
   <button bindtap="openSetting"  class='btn'>设置</button>
</view>

js:
const app = getApp()

Page({
  data:{
    userIcon:"../../images/default_icon.gif",
    userName:"未登录",
    haveLogin:true,
  }, onShow:function(){
    let that = this;
    wx.getSetting({
      success(res) {
        if (!res.authSetting['scope.userInfo']) {
          console.info("用户未授权");
          that.setData({
            userIcon: "../../images/default_icon.gif",
            userName: "未登录",
            haveLogin:false
          })
        } else {
          console.info("用户已经授权");
          that.setUserInfo();
        }
      }
    }) 
  },setUserInfo:function(){
    let that = this;
    //微信登录
    wx.login({
      success: function (loginRes) {
        //获取微信用户信息
        wx.getUserInfo({
          success: function (userRes) {
            //这里是我自定义的发送后端ajax请求的方法,主要传code,userData(加密后的信息),iv 这三个参数到服务端
            app.ajaxRequest({
              url: app.api.wxLogin,//服务端的授权地址
              params: {
                code: loginRes.code,//从登录信息中获取code,每次登录都不一样
                userData: userRes.encryptedData,//从用户信息中获取加密的用户信息
                iv: userRes.iv//从用户信息中获取iv
              }, callback: function (res) {
                //res是服务端返回的信息,包含用户的头像,用户名,还有sessionId
                that.setData({
                  userIcon: res.userInfo.avatarUrl,
                  userName: res.userInfo.userName,
                  haveLogin: true
                });
                //设置sessionId到全局的变量中,后续的请求都在hader里面带上这个JESSIONID,服务端就跟普通的session处理一样就可以了。
                app.globalData.header.Cookie = 'JSESSIONID=' + res.sessionId;
                app.globalData.haveLogin = true;
              }
            });
          }
        });
      }
    })
  },//打开权限设置 
  openSetting: function () {
    let that = this;
    wx.openSetting()
  }
})
重点看下setUserInfo 方法,里面有跟服务端交互的逻辑,微信官方说的是要跟服务端交互两次,我使用一次也很正常,没有发现什么问题。

这是我封装的微信端的ajax请求:
 ajaxRequest: function (config){
    var loadType = config.loadType == null ? this.loadType.loading : this.loadType.top;
    var url = config.url;
    var callback = config.callback;
    var params = config.params;
    if (loadType == this.loadType.loading){
      wx.showLoading({
        title: '玩命加载中'
      })
    } else if (loadType == this.loadType.top){
      wx.showNavigationBarLoading();
    }
    var that = this;
    wx.request({
      url: getApp().globalData.domain + url,//上线的话必须是https,没有appId的本地请求貌似不受影响  
      data: params,
      method: 'GET', // OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT  
      header: that.globalData.header, // 设置请求的 header  
      success: function (res) {
        if (loadType == that.loadType.loading) {
          wx.hideLoading();
        } else if (loadType == that.loadType.top) {
          wx.hideNavigationBarLoading();
          wx.stopPullDownRefresh();
        }
        if (res.data.responseCode == that.responseCode.LOGIN_OUT){
          that.globalData.haveLogin = false;
          wx.showToast({
            title: "登录超时,请在'我的'点击登录按钮完成登录",
            icon: 'none',
            duration: 2000
          });
        } else if (res.data.responseCode != that.responseCode.SUCCESS && res.data.responseCode != that.responseCode.LOGIN_OUT) {
          wx.showToast({
            title: res.data.errorMsg,
            icon: 'none',
            duration: 2000
          });
          return;
        }else{
          callback(res.data.data);
        }
      },
      fail: function () {
        wx.showToast({
          title: "网络连接错误",
          icon: 'none',
          duration: 2000
        });  
        return;
      },
      complete: function () {
        // complete  
      }
    })
  }
注意:在app.js中添加以下内容,不然上面的发送ajax请求的方法会报错哦,这样在各个js中通过app 对象就可以调用了。
1.globalData 对象中加入这么一条哈 header: { 'Cookie': '', "x-requested-with":"XMLHttpRequest"}
2.添加一个数据对象,ajax请求loading类型
loadType:{
      top: 0,
      loading:1
}
3.添加服务端返回类型
responseCode:{
    LOGIN_OUT: 401,//登录超时
    SUCCESS:200,//请求成功
    ERROR:500//服务异常
}

 

2.服务端

服务端的一些代码


2.1.AES解密

微信官网提供了一些语言的AES解密的demo,下载地址:https://developers.weixin.qq.com/miniprogram/dev/demo/aes-sample.zip

大家感受下,对,没有java的,我能说啥,好吧,我提供一个,其实也不复杂
需要一个jar的支持:
maven配置 
<dependency>
   <groupId>org.bouncycastle</groupId>
   <artifactId>bcprov-jdk15on</artifactId>
   <version>1.54</version>
</dependency>
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;

public class AES {
    public static boolean initialized = false;
 
 /**
  * AES解密
  * @param content 密文
  * @return
  * @throws InvalidAlgorithmParameterException 
  * @throws NoSuchProviderException 
  */
 public byte[] decrypt(byte[] content, byte[] keyByte, byte[] ivByte) throws InvalidAlgorithmParameterException {
  initialize();
  try {
   Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
   Key sKeySpec = new SecretKeySpec(keyByte, "AES");
   
   cipher.init(Cipher.DECRYPT_MODE, sKeySpec, generateIV(ivByte));// 初始化 
   byte[] result = cipher.doFinal(content);
   return result;
  } catch (NoSuchAlgorithmException e) {
   e.printStackTrace();  
  } catch (NoSuchPaddingException e) {
   e.printStackTrace();  
  } catch (InvalidKeyException e) {
   e.printStackTrace();
  } catch (IllegalBlockSizeException e) {
   e.printStackTrace();
  } catch (BadPaddingException e) {
   e.printStackTrace();
  } catch (NoSuchProviderException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return null;
 }  
 
 public static void initialize(){  
        if (initialized) return;  
        Security.addProvider(new BouncyCastleProvider());  
        initialized = true;  
    }
 //生成iv  
    public static AlgorithmParameters generateIV(byte[] iv) throws Exception{  
        AlgorithmParameters params = AlgorithmParameters.getInstance("AES");  
        params.init(new IvParameterSpec(iv));  
        return params;  
    }  
}  


2.2.登录,自动注册逻辑

    @ResponseBody
    @RequestMapping("/wxLogin")
    public AjaxResponse wxLogin(HttpSession session, String code, String iv, String userData) {
        AjaxResponse response = new AjaxResponse();
        try {
            /**
             * 请求微信服务端,请求地址是:https://api.weixin.qq.com/sns/jscode2session  需要传入appid,secret,js_code ,grant_type(固定传这个串:authorization_code)
             */
            String url = configInfo.getWxUrlLogin() + "?appid=" + configInfo.getWxAppId() + "&secret=" + configInfo.getWxSecret() + "&js_code=" + code + "&grant_type=" +
                    configInfo.getWxGrantType();
            /**
             * 发送http请求
             */
            String wxResponse = OKHttpUtils.getRequest(url);
            Map<String, String> map = JsonUtils.convertJson2Obj(wxResponse, Map.class);
            /**
             * 从返回的信息中获取 session_key 和open_id
             */
            String session_key = map.get("session_key");
            String openid = map.get("openid");
            /**
             * 用openid查询下自己的数据库,数据库中openid必须是唯一的哦,记得设置唯一主键
             * 如果是第一登录,那么就会走自动注册逻辑,如果后续用户登录,那么就会走自动登录流程了。
             */
            User loginUser = userService.getUserByWxOpenId(openid);
            if (loginUser == null) {
                /**
                 * 用户不存在,那么就要解析加密串中的用户信息
                 */
                AES aes = new AES();
                byte[] resultByte = aes.decrypt(Base64.decodeBase64(userData), Base64.decodeBase64(session_key), Base64.decodeBase64(iv));
                if (null != resultByte && resultByte.length > 0) {
                    String userInfo = new String(resultByte, "UTF-8");
                    Map<String, String> userInfoData = JsonUtils.convertJson2Obj(userInfo, Map.class);
                    String nickName = userInfoData.get("nickName");
                    String userIcon = userInfoData.get("avatarUrl");
                    User user = new User();
                    user.setUserIcon(userIcon);
                    user.setUserName(nickName);
                    user.setWxOpenId(openid);
                    /**
                     * 解析出来后,将用户信息自动注册一下。
                     */
                    loginUser = userService.wxAutoRegister(user);
                }
            }

            /**
             * 获取sessionId
             */
            String sessionId = session.getId();
            /**
             * 设置session信息
             */
            SessionUser sessionUser = new SessionUser();
            sessionUser.setUserName(loginUser.getUserName());
            sessionUser.setUserIcon(loginUser.getUserIcon());
            sessionUser.setUserId(loginUser.getUserId());
            session.setAttribute(Constants.SESSION_USER_KEY, sessionUser);

            /**
             * 将用户信息返回
             */
            WxUserVO userVO = CopyTools.copy(loginUser, WxUserVO.class);
   /**
             * 自动注册的肯定是没有密码的,所以根据这个判断用户是否绑定账号,绑定账号,就是让用户自己设置一个账号,这样就可以在其他端登录,比如网站
             */
            userVO.setBindAccount(StringTools.isEmpty(loginUser.getPassword()) ? false : true);
   /*
    *返回用户信息,包括基本的用户信息,和sessionId信息
    */
            WxLoginUserVO loginUserVO = new WxLoginUserVO();
            loginUserVO.setSessionId(sessionId);
            loginUserVO.setUserInfo(userVO);
            response.setData(loginUserVO);
            return response;
        } catch (Exception e) {
            logger.error("微信登录异常", e);
            return AjaxResponse.getExceptionResponseVO();
        }
    }
以上代码是我的实现,你直接拷贝过去肯定是没法使用的,一些类和对象是没有的,注释我写的很清楚了,大家可以根据自己的实际场景进行修改哈。

记住要将sessionId返回,在微信端请求服务端的时候,将这个sessionId放到header中就可以了,这样服务端就可以知道用户是否登录了。

好了,服务端登录就讲到这里了,还有不明白的可以在下面留言。