前言
各種官方網站通常都會有app、微信公眾號等。比如央視網,銀行等。
當我們關注公眾號或者app後,這些應用就可以在移動端方便地將資訊推送給使用者。
統一各產品線的賬號體系,實現一個賬號處處使用的目標是非常有必要的。
於是有需求:可以實現微信掃碼登陸。
微信對接
通常網站與個人微信的對接有兩種方式:
第一種是Oauth2登陸:
使用者透過掃描網站的二維碼, 授權公開資訊,如暱稱、頭像等。便可以成功登陸網站。
例如在愛奇藝網站掃描二維碼登陸後,授權給網站,之後成功登陸愛奇藝。
登陸完之後,微信會提示資訊。
第二種是關注微信公眾號
網站自己彈出一個二維碼,掃描二維碼後彈出公眾號的關注介面、只要一關注公眾號網站自動登入、第二次掃描登入的時候網站直接登入。
例如這種,隨便找的網站。
掃碼後跳到公眾號,關注後自動登陸。
這兩種掃碼登陸方法,對應微信提供的兩種開發方式
一種是基於微信公眾平臺的掃碼登入,另一種是基於微信開放平臺的掃碼登入。
微信開放平臺就是為了讓第三方應用投入微信的懷抱而設計的,這第三方應用指的是比如android、ios、網站、系統等;
引用
微信公眾平臺就是為了讓程式設計師小夥伴利用微信自家技術(公眾號、小程式)開發公眾號、小程式而準備的。
微信開放平臺入口:https://open.weixin.qq.com/
微信公眾平臺入口:https://mp.weixin.qq.com/
兩者使用微信掃碼登入的區別:
微信開放平臺需要開企業認證才能註冊。比較難申請
微信公眾平臺需要認證微信服務號,才能進行掃碼登入的開發。只需申請一個公眾號。
下面採用第二種。
後臺與公眾號配置
後臺使用的是spring boot。前臺用的angular
這裡引入github一個公眾號開發工具包
github倉庫
後臺pom.xml依賴:
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
1.申請一個公眾號。若無,可用測試公眾號。
2.配置伺服器資訊
URL為開發者伺服器的介面地址,微信伺服器透過該介面與開發者伺服器建立連線。Token可由開發者可以任意填寫,用作生成簽名(該Token會和介面URL中包含的Token進行比對,從而驗證安全性)
若是沒有伺服器,可以用ngfork進行內網穿透,免費好用。這裡用的就是ngfork
3.配置後臺:
# 公眾號配置(必填)
wx:
mp:
appid: wxaf7fe05a8xxxxxxxxx
secret: 57b48fcec2d5db1axxxxxxxxxxx
token: yunzhi
aesKey: 123
@Service
public class WeChatMpService extends WxMpServiceImpl {
@Autowired
private WxMpConfig wxMpConfig;
@PostConstruct
public void init() {
final WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
// 設定微信公眾號的appid
config.setAppId(this.wxMpConfig.getAppid());
// 設定微信公眾號的app corpSecret
config.setSecret(this.wxMpConfig.getAppSecret());
// 設定微信公眾號的token
config.setToken(this.wxMpConfig.getToken());
// 設定訊息加解密金鑰
config.setAesKey(this.wxMpConfig.getAesKey());
super.setWxMpConfigStorage(config);
}
}
4.驗證伺服器地址的有效性
開發者提交資訊後,微信伺服器將傳送GET請求到剛才填寫的伺服器地址URL上,
經過確認後,需要向微信伺服器原樣返回echostr引數內容,接入才生效,否則接入失敗。
例如,微信伺服器未正確收到返回資訊,顯示配置失敗。
GET請求攜帶引數如下表所示:
引數 | 描述 |
---|---|
signature | 微信加密簽名,signature結合了開發者填寫的 token 引數和請求中的 timestamp 引數、nonce引數 |
timestamp | 時間戳 |
nonce | 隨機數 |
echostr | 隨機字串 |
驗證簽名的主要程式碼:
用的驗證簽名的函式是weixin-java-mp包提供的。
@RequestMapping("wechat")
@RestController
public class WechatController {
@Autowired
WeChatMpService weChatMpService;
/*
* @param signature 微信加密簽名,signature結合了開發者填寫的 token 引數和請求中的 timestamp 引數、nonce引數。
* @param timestamp 時間戳
* @param nonce 這是個隨機數
* @param echostr 隨機字串,驗證成功後原樣返回
*/
@GetMapping
public void get(@RequestParam(required = false) String signature,
@RequestParam(required = false) String timestamp,
@RequestParam(required = false) String nonce,
@RequestParam(required = false) String echostr,
HttpServletResponse response) throws IOException {
if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {
this.logger.warn("接收到了未透過校驗的微信訊息,這可能是token配置錯了,或是接收了非微信官方的請求");
return;
}
response.setCharacterEncoding("UTF-8");
response.getWriter().write(echostr);
response.getWriter().flush();
response.getWriter().close();
}
}
掃碼登入的開發
先來看一下大致流程
1.向微信伺服器傳送請求,獲取access_token。
access_token是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需使用access_token。開發者需要進行妥善儲存。access_token的儲存至少要保留512個字元空間。access_token的有效期目前為2個小時,需定時重新整理,重複獲取將導致上次獲取的access_token失效。
url(1)請求示例地址: https://api.weixin.qq.com/cgi...
所以假如直接對微信文件開發的話,需要先請求access_token。
但用了weixin-java-mp包,會幫我們做這件事。下面跳到下一步
2.請求獲取ticket,用於換取登陸二維碼。
獲取帶引數的二維碼的過程包括兩步:
- 首先建立二維碼ticket
- 憑藉 ticket 到指定 URL 換取二維碼。
目前有2種型別的二維碼:
- 臨時二維碼,有過期時間的,最長可以設定為在二維碼生成後的30天后過期,能夠生成較多數量。
- 永久二維碼,無過期時間的,數量較少(目前為最多10萬個)。
臨時二維碼足以滿足需求。
用包提供的方法獲取ticket
String qrUuid = UUID.randomUUID().toString();
WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);
每次建立二維碼 ticket 需要提供一個開發者自行設定的引數(scene_id), 這裡用了隨機生成的uuid。
3.用ticket向微信請求二維碼。前端顯示
獲取二維碼 ticket 後,開發者可用 ticket 換取二維碼圖片。
HTTP GET請求(請使用 https 協議)https://mp.weixin.qq.com/cgi-...
提醒:TICKET記得進行UrlEncode
ticket正確情況下,http 返回碼是200,是一張圖片的url地址,可以直接展示或者下載。
使用包提供的方法進行請求獲取
String qrCodeUrl = this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());
之後返回給前端,前端用<img>標籤處理,作為src的值。
<img class="img-thumbnail" [src]="qrCodeUrl"/>
4.使用者掃描二維碼,微信進行事件推送
微信使用者使用公共號時可能會產生很多事件,例如
- 關注/取消關注事件
- 掃描帶引數二維碼事件
- 自定義選單事件
- 點選選單拉取訊息時的事件推送
- 點選選單跳轉連結時的事件推送
而當前的場景就是使用者掃描帶引數二維碼事件
使用者掃描帶場景值二維碼時,可能推送以下兩種事件:
- 如果使用者還未關注公眾號,則使用者可以關注公眾號,關注後微信會將帶場景值關注事件推送給開發者。
- 如果使用者已經關注公眾號,則微信會將帶場景值掃描事件推送給開發者。
接受微信的事件推送:
/**
* 當設定完微信公眾號的介面後,微信會把使用者傳送的訊息,掃碼事件等推送過來
*
* @param signature 微信加密簽名,signature結合了開發者填寫的 token 引數和請求中的 timestamp 引數、nonce引數。
* @param encType 加密型別(暫未啟用加密訊息)
* @param msgSignature 加密的訊息
* @param timestamp 時間戳
* @param nonce 隨機數
* @throws IOException
*/
@PostMapping(produces = "text/xml; charset=UTF-8")
public void api(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
@RequestParam("signature") String signature,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce) throws IOException {
if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {
this.logger.warn("接收到了未透過校驗的微信訊息,這可能是token配置錯了,或是接收了非微信官方的請求");
return;
}
BufferedReader bufferedReader = httpServletRequest.getReader();
String str;
StringBuilder requestBodyBuilder = new StringBuilder();
while ((str = bufferedReader.readLine()) != null) {
requestBodyBuilder.append(str);
}
String requestBody = requestBodyBuilder.toString();
this.logger.info("\n接收微信請求:[signature=[{}], encType=[{}], msgSignature=[{}],"
+ " timestamp=[{}], nonce=[{}], requestBody=[\\n{}\\n]",
signature, encType, msgSignature, timestamp, nonce, requestBody);
}
5. 後臺進行邏輯處理
微信推送給公眾號的訊息型別很多,而公眾號也需要針對使用者不同的輸入做出不同的反應。
如果使用if ... else ...來實現的話非常難以維護,這時可以根據weixin-java-mp開發文件,使用 WxMpMessageRouter
來對訊息進行路由維護。
先寫出幾個事件的handler。比如掃碼事件和關注事件。 原始碼在文末
/**
* 處理關注事件
* 新使用者走關注事件;老使用者走掃碼事件
* @author Binary Wang
*/
@Component
public class SubscribeHandler extends AbstractHandler {
private final WeChatMpService weChatMpService;
private final WechatService wechatService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public SubscribeHandler(WeChatMpService weChatMpService , WechatService wechatService) {
super(weChatMpService);
this.weChatMpService = weChatMpService;
this.wechatService = wechatService;
}
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager) throws WxErrorException {
this.logger.info("新關注使用者 OPENID: " + wxMessage.getFromUser());
if (this.logger.isDebugEnabled()) {
this.logger.info("新關注使用者 OPENID: " + wxMessage.getFromUser());
}
WeChatUser weChatUser = this.wechatService.getOneByOpenidAndAppId(wxMessage.getFromUser(), wxMessage.getToUser());
if (wxMessage.getEventKey().startsWith("qrscene_")) {
String key = wxMessage.getEventKey().substring("qrscene_".length());
return this.handleByEventKey(key, weChatUser, wxMessage);
}
return new TextBuilder().build("感謝關注,祝您生活愉快!",
wxMessage,
weChatMpService);
}
}
對寫的幾個handler進行路由註冊
private WxMpMessageRouter router;
private void refreshRouter() {
final WxMpMessageRouter newRouter = new WxMpMessageRouter(this);
// 關注事件
newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();
// 取消關注事件
newRouter.rule().async(false).msgType(EVENT).event(UNSUBSCRIBE).handler(this.unsubscribeHandler).end();
// 掃碼事件 newRouter.rule().async(false).msgType(EVENT).event(SCAN).handler(this.scanHandler).end();
// 預設
newRouter.rule().async(false).handler(this.msgHandler).end();
this.router = newRouter;
}
/**
* 微信事件透過這個入口進來
* 根據不同事件,呼叫不同handler
*/
public WxMpXmlOutMessage route(WxMpXmlMessage message) {
try {
return this.router.route(message);
} catch (Exception e) {
this.logger.error(e.getMessage(), e);
}
return null;
}
現在已經配置好了,微信推送的掃碼事件或者關注事件已經被我們定義的handler獲取到了。
那麼有下一個問題:假如有多個使用者同時在使用微信登入,我們怎麼要推送給哪個客戶端,讓掃碼成功的使用者成功登陸呢?
這時就需要在返回使用者登陸二維碼前,
- 記錄該客戶端的seesionId
- 同時新增一個key為uuid,value為自定義handler到hashmap中。
這裡的uuid為,前面獲取ticket隨機生成的uuid。
這樣就可以在掃碼事件handler中, 呼叫這個自定義的handler,實現向指定客戶端傳送資訊。
在返回二維碼前,定義自定義handler
@Override
public String getLoginQrCode(String wsLoginToken, HttpSession httpSession) {
try {
if (this.logger.isDebugEnabled()) {
this.logger.info("1. 生成用於回撥的uuid,請將推送給微信,微信當推送帶有UUID的二維碼,使用者掃碼後微信則會把帶有uuid的資訊回推過來");
}
String qrUuid = UUID.randomUUID().toString();
WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);
this.wxMpService.addHandler(qrUuid, new WeChatMpEventKeyHandler() {
long beginTime = System.currentTimeMillis();
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public boolean getExpired() {
return System.currentTimeMillis() - beginTime > 10 * 60 * 1000;
}
/**
* 掃碼後呼叫該方法
* @param wxMpXmlMessage 掃碼訊息
* @param weChatUser 掃碼使用者
* @return 輸出訊息
*/
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, WeChatUser weChatUser) {
if (this.logger.isDebugEnabled()) {
this.logger.info("2. 使用者掃描後觸發該方法, 傳送掃碼成功的同時,將wsUuid與微信使用者繫結在一起,用後面使用wsU");
}
String openid = wxMpXmlMessage.getFromUser();
if (openid == null) {
this.logger.error("openid is null");
}
if (weChatUser.getUser() != null) {
String uuid = UUID.randomUUID().toString();
bindWsUuidToWeChatUser(uuid, weChatUser);
simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
"/stomp/scanLoginQrCode",
uuid);
return new TextBuilder().build(String.format("登入成功,登入的使用者為: %s", weChatUser.getUser().getName()),
wxMpXmlMessage,
null);
} else {
simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
"/stomp/scanLoginQrCode",
false);
return new TextBuilder().build(String.format("登入原則,原因:您尚未繫結微信使用者"),
wxMpXmlMessage,
null);
}
}
});
return this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());
} catch (Exception e) {
this.logger.error("獲取臨時公眾號圖片時發生錯誤:" + e.getMessage());
}
return "";
}
利用websocket向前端傳送資訊
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議。
WebSocket 使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。
simpMessagingTemplate.convertAndSendToUser(wsLoginToken,
"/stomp/scanLoginQrCode",
uuid);
之後前端就可以利用這個身份資訊loginUid登陸了。
前臺向後臺傳送登陸請求
/**
* 微信掃碼登入
*/
onWeChatLogin() {
this.userService.getLoginQrCode()
.subscribe(src => {
this.qrCodeSrc = src;
this.loginModel = 'wechat';
this.userService.onScanLoginQrCode$.pipe(first()).subscribe(data => {
const uuid = data.body;
this.login({username: uuid, password: uuid});
});
});
}
後臺再判斷loginUid是否在map中,是則登陸成功。
/**
* 校驗微信掃碼登入後的認證ID是否有效
* @param wsAuthUuid websocket認證ID
*/
@Override
public boolean checkWeChatLoginUuidIsValid(String wsAuthUuid) {
return this.map.containsKey(wsAuthUuid);
}
原始碼地址: 待整理中,整理後放出