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类型的菜单那样返回一条带连接的消息给用户。