微信公衆號帳號登陸功能實現

1.分析html

如今咱們須要爲已有項目添加一個微信公衆號,公衆號部分功能須要用戶進行登陸才能操做。對於微信用戶來講,每一個用戶有一個惟一的標識OpenId,咱們只須要在本來的userInfo表中添加一個openId字段,將微信用戶openId和用戶名、密碼綁定就能夠了。java

具體的實現有如下兩種方式:web

  第一種:1)用戶點擊「帳號綁定」,菜單,開始綁定帳號;redis

      2)公衆號回覆一條包含帳號綁定頁面連接的文本消息,連接中包含openId參數;spring

      3)用戶點擊文本消息中的網頁連接,進入帳號綁定頁面。填寫用戶名和密碼,點擊提交。須要注意的是,openId是綁定頁面的一個隱藏域,用戶名和密碼須要用戶填寫;數據庫

      4)後臺獲取openId,用戶名,密碼,調用業務系統的登陸接口驗證用戶名和密碼是否正確,正確則將openId添加在數據庫,不正確則提示用戶名和密碼不正確。json

      例子:花生殼公衆號api

  第二種:1)沒有「帳號綁定」菜單,當用戶點擊須要綁定才能操做的菜單時,頁面重定向到帳號綁定頁面數組

      2)填寫用戶名和密碼,點擊提交。須要注意的是,openId是綁定頁面的一個隱藏域,用戶名和密碼須要用戶填寫;服務器

      3)後臺獲取openId,用戶名,密碼,調用業務系統的登陸接口驗證用戶名和密碼是否正確,正確則將openId添加在數據庫,不正確則提示用戶名和密碼不正確。

 

對於帳號綁定功能來講,其實就兩個關鍵點1.如何獲取openId;2.如何過濾須要登陸的頁面。

以上兩種方式在實現上第一種方式比較簡單,而第二種方式須要用到微信網頁受權獲取openId,因此這邊重點介紹第二種方式,可是第一種方式也會稍微說明。

 

2.第一種方式關鍵點分析

這裏須要提早了解自定義菜單的建立各類消息的接收和響應

能夠參考柳峯大神寫的專欄:http://blog.csdn.net/column/details/wechatmp.html

2.1獲取openId

首先須要建立類型爲click的自定義菜單。當用戶在微信上點擊菜單時,微信會向咱們推送xml數據包,這個數據包中有一個字段FromUserName,也就是用戶的openId。詳細的數據包信息可到微信公衆號開發文檔查看。這裏說的數據包推送到的地址是咱們在微信公衆號管理上配置的接入url,以下

 

2.2過濾須要登陸的頁面

在建立click類型的自定義菜單時,能夠設置菜單的key值,這個key微信也會在用戶點擊以後,經過上述的數據包推送給咱們,對應的字段是EventKey。拿到這個key值,咱們就能區分哪些菜單須要登陸,哪些不須要。當點擊須要登陸的菜單時,後臺判斷openId是否綁定,沒有綁定就回復一條包含帳號綁定頁面連接的文本消息,連接中包含openId參數,有綁定就回復一條包含對應頁面的連接的文本消息。

 

3.第二種方式關鍵點分析

這裏須要提早了解自定義菜單的建立微信網頁受權

3.1獲取openId

3.1.1獲取code

首先先建立類型爲view的自定義菜單,不須要登陸的url配置成對應的controller地址就能夠了,須要登陸的url配置的是網頁受權獲取code的url連接,連接以下

//(APPID:替換實際appId,REDIRECT_URI:替換成對應的回調地址,SCOPE:填寫snsapi_base或snsapi_userinfo,STATE:可選參數,可填寫a-zA-Z0-9
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
由於咱們只獲取openId,因此SCOPE替換成「snsapi_base」,而回調地址REDIRECT_URI替換成咱們點擊菜單須要跳轉的網頁連接(也就是對應Controller的連接)。
須要注意的是這個回調地址須要進行urlEncode編碼。以下:


redirect_url就是用來接收微信發送過來的coed的地址,在對應的controller中咱們能夠經過request.getParameter("code")獲取code,因此,對於不一樣的菜單,咱們只需

要配置不一樣的redirect_url就能在菜單對應的controller下獲取code,並經過code獲取openId。

3.1.2經過code換取網頁受權憑證access_token

 獲取到code以後,咱們須要以code爲參數向微信提供的接口發起https get請求,獲取包含openId的網頁受權憑證。

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

具體代碼以下:

1)編寫發送https請求工具

import com.alibaba.fastjson.JSONObject;
import com.iport.framework.util.JsonUtil;
import com.tmall.wechat.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.ConnectException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

/**
 * 微信工具類
 * 
 */
public class WechatUtil {
    private static Logger logger = LoggerFactory.getLogger(WechatUtil.class);

    /**
     * 發送https請求
     * @param requestUrl 請求地址
     * @param requestMethod 請求方式(GET、POST)
     * @param outputStr 提交的數據
     * @return JSONObject(經過JSONObject.get(key)的方式獲取json對象的屬性值)
     */
    public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
        JSONObject jsonObject = null;
        try {
            // 建立SSLContext對象,並使用咱們指定的信任管理器初始化
            TrustManager[] tm = { new MyX509TrustManager() };
            SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            // 從上述SSLContext對象中獲得SSLSocketFactory對象
            SSLSocketFactory ssf = sslContext.getSocketFactory();

            URL url = new URL(requestUrl);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setSSLSocketFactory(ssf);

            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 設置請求方式(GET/POST)
            conn.setRequestMethod(requestMethod);

            // 當有數據須要提交時
            if (null != outputStr) {
                OutputStream outputStream = conn.getOutputStream();
                // 注意編碼格式
                outputStream.write(outputStr.getBytes("UTF-8"));
                outputStream.close();
            }
            // 將返回的輸入流轉換成字符串
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            // 釋放資源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            inputStream = null;
            conn.disconnect();
            jsonObject = JSONObject.parseObject(buffer.toString());
        } catch (ConnectException ce) {
            ce.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return jsonObject;
    }
}

 

import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * 證書信任管理器(用於https請求)
 * 這個證書管理器的做用就是讓它信任咱們指定的證書,上面的代碼意味着信任全部證書,不論是否權威機構頒發
 */
public class MyX509TrustManager implements X509TrustManager {
    // 檢查客戶端證書
    @Override
    public void checkClientTrusted(X509Certificate[] arg0, String arg1)
            throws CertificateException {
    }
    // 檢查服務器端證書
    @Override
    public void checkServerTrusted(X509Certificate[] arg0, String arg1)
            throws CertificateException {
    }
    // 返回受信任的X509證書數組
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}

2)獲取微信網頁受權憑證的工具類

import com.alibaba.fastjson.JSONObject;
import com.tmall.wechat.model.WeixinOauth2Token;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 微信網頁受權工具類
 */
public class AdvancedUtil {
    private static Logger logger = LoggerFactory.getLogger(AdvancedUtil.class);
    /**
     * 獲取網頁受權憑證
     * @param appId 公衆帳號的惟一標識
     * @param appSecret 公衆帳號的密鑰
     * @param code
     * @return WeixinAouth2Token
     */
    public static WeixinOauth2Token getOauth2AccessToken(String appId, String appSecret, String code) {
        WeixinOauth2Token wat = null;
        // 拼接請求地址:該地址參數順序固定
        String requestUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
        requestUrl = requestUrl.replace("APPID", appId);
        requestUrl = requestUrl.replace("SECRET", appSecret);
        requestUrl = requestUrl.replace("CODE", code);
        // 獲取網頁受權憑證
        JSONObject jsonObject = WechatUtil.httpsRequest(requestUrl,"GET", null);
        if (null != jsonObject) {
            try {
                wat = new WeixinOauth2Token();
                wat.setAccessToken(jsonObject.getString("access_token"));
                wat.setExpiresIn(jsonObject.getIntValue("expires_in"));
                wat.setRefreshToken(jsonObject.getString("refresh_token"));
                wat.setOpenId(jsonObject.getString("openid"));
                wat.setScope(jsonObject.getString("scope"));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return wat;
    }
}

 

/**
 * 經過code換取網頁受權access_token返回的
 * 網頁受權信息
 */
public class WeixinOauth2Token {
    // 網頁受權接口調用憑證
    private String accessToken;
    // 憑證有效時長(單位:秒)
    private int expiresIn;
    // 用於刷新憑證
    private String refreshToken;
    // 用戶惟一標識
    private String openId;
    // 用戶受權做用域
    private String scope;

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public int getExpiresIn() {
        return expiresIn;
    }

    public void setExpiresIn(int expiresIn) {
        this.expiresIn = expiresIn;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    public String getOpenId() {
        return openId;
    }

    public void setOpenId(String openId) {
        this.openId = openId;
    }

    public String getScope() {
        return scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }
}

 

3.2對須要登陸的頁面進行過濾

這裏咱們經過spring攔截器來實現登陸攔截。首先咱們對須要攔截的地址進行配置,當咱們訪問這些地址時,會首先進入到攔截器方法中。由於在第一步建立自定義菜單時咱們已經將這些須要登陸攔截的頁面配置成了獲取code的回調地址,因此咱們能夠在攔截器中獲取到code。而後利用第二部寫的方法獲取到包含openId的access_token憑證信息,拿到openId就能判斷用戶是否綁定,有綁定放過,沒綁定就將openId做爲參數,轉發到登陸界面。

具體代碼以下:

攔截器配置:

<!--配置攔截器, 多個攔截器,順序執行 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- 匹配的是url路徑, 若是不配置或/**,將攔截全部的Controller -->
            <mvc:mapping path="/wechat/**/*.html" />
            <!-- 不進行攔截 -->
            <mvc:exclude-mapping path="/wechat/index.html"/>
            <mvc:exclude-mapping path="/wechat/createMenu.html"/>
            <bean class="com.wechat.interceptor.LoginInterceptor"></bean>
        </mvc:interceptor>
        <!-- 當設置多個攔截器時,先按順序調用preHandle方法,而後逆序調用每一個攔截器的postHandle和afterCompletion方法 -->
    </mvc:interceptors>

登陸攔截器:

import com.iport.cm.model.po.CmLoginAccount;
import com.iport.cm.service.ICmLoginAccountServiceEx;
import com.iport.framework.cache.redis.JedisTemplate;
import com.iport.framework.context.Sc;
import com.iport.framework.util.ValidateUtil;
import com.iport.park.wechat.model.WeixinOauth2Token;
import com.iport.park.wechat.util.AdvancedUtil;
import com.iport.park.wechat.util.WechatConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 微信登陸攔截器
 * Created by caiyl on 2017/12/6.
 */
public class LoginInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private ICmLoginAccountServiceEx cmLoginAccountServiceEx;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 用戶贊成受權後,能獲取到code
        String code = request.getParameter("code");
        JedisTemplate jedisTemplate = (JedisTemplate) Sc.getBean("jedisTemplate");
        //根據code獲取openId
        String oldOpenId = jedisTemplate.get(code);
        String openId = null;
        boolean ret = false;
        //openId爲空,表示code沒有重複,從新得到openId
        if (ValidateUtil.isEmpty(oldOpenId)) {
            response.setCharacterEncoding("utf-8");
            // 用戶贊成受權
            if (!"authdeny".equals(code)) {
                // 獲取網頁受權access_token
                WeixinOauth2Token weixinOauth2Token = AdvancedUtil.getOauth2AccessToken(WechatConstants.APPID, WechatConstants.APPSECERT, code);
                //用戶惟一表示openId
                openId = weixinOauth2Token.getOpenId();
                jedisTemplate.setex(code,openId,5*60);//code有效期5分鐘
            }
        } else {
            openId = oldOpenId;
        }
        //獲取用戶信息,判斷是否綁定
        CmLoginAccount userInfo = cmLoginAccountServiceEx.getAccountByOpenId(openId);
        if (null != userInfo) {
            ret = true;
        }
        if (!ret) {
            request.setAttribute("openId",openId);
            request.getRequestDispatcher(WechatConstants.WECHAT_BASE_PATH+"/login.jsp").forward(request, response);
        }
        return ret;
    }
}

補充:能夠看到攔截器代碼中咱們用到了redis,redis的做用是對code進行去重,解決微信服務器屢次請求獲取code回調方法,形成code失效的問題

 

4.總結

使用第一種方法有一個弊端:當咱們未登陸時,點擊菜單,公衆號回覆一條帶有登陸頁面的連接,而當咱們已登陸,點擊菜單,公衆號一樣回覆一條帶有對應頁面的連接,而沒有辦法實如今已登陸狀態下直接跳轉響應頁面。爲何呢?由於這種方式用的是類型爲click的菜單,click只能用來回復各類消息,不能跳轉頁面,即便使用了轉發或重定向也沒用。

剛開始進行微信開發的第一步咱們須要在微信管理後臺配置一個連接,用來驗證咱們服務器的有效性。當用戶在微信公衆號操做時,無論進行什麼操做,都會觸發該連接對應的controller方法,只不過是post請求,而驗證服務器有效性是get請求。因此該連接也是全部消息接收和響應總入口。當用戶在公衆號上操做時,微信服務器會返回給咱們一個數據包,數據包中包含了FromUserName(用戶openId),在方法一中咱們就是在這邊獲取openId來判斷用戶是否綁定的。那咱們第二種方法是否也能夠在接收和響應消息總入口這邊獲取openId實現登陸驗證呢?這樣不就不用編寫什麼過濾器了嗎?畢竟這是總入口。答案是否認的。爲何呢?由於這種方式使用的是view類型的菜單,咱們在建立菜單的時候已經指定了跳轉的url了,因此沒有辦法使用轉發或重定向到登陸界面。而view類型的菜單也不能向用戶返回消息,因此也就不能像click類型的菜單那樣返回一條帶連接的消息給用戶。

相關文章
相關標籤/搜索