基於引入了 Spring MVC 的 Spring boot 環境。前端
接入QQ的官方文檔:傳送門java
獲取接入資格從而獲取網站的app_id和app_key等內容官網已經足夠詳盡,此處再也不贅述。每一步要向QQ提供的哪一個API網址發請求,要帶什麼參數等官網文檔也已經介紹清楚,再也不贅述。正則表達式
重點在於徹底使用java的後端技術調用QQ的第三方登陸API完成整個登陸流程——獲得能夠惟一標識一個QQ的openid——中,java代碼該如何寫,爲何,我遇到哪些坑。spring
在類資源文件夾 resources 下,存放一個 QQLogin.properties 文件,其中存放了整個過程當中須要用到的參數。以後出現的QQLoginUtil.getQQLoginInfo(...)
會從這個文件中讀取對應內容。具體實現不是本文重點,能夠參考這篇文章:java讀取.properties配置文件的幾種方法。數據庫
文檔已經足夠詳盡。不過強調一下應用設置中的回調地址和咱們以後使用的回調地址必須如出一轍,不要想着我在回調地址中填一個/Handler
,而後回調地址寫/Handler/AHandler
,這是不容許的。後端
大部分是前端內容,本文主要討論後端代碼,故很少作討論。但咱們這裏將點擊QQ登陸按鈕的超連接定位發給咱們本身的後端服務器的一個請求,由於整個過程都由咱們的後端完成嘛。服務器
這裏貼一下咱們後端的結構app
@Controller @RequestMapping("/login") public class loginServlet { @RequestMapping("/loginByQQ") public void loginByQQ(HttpServletRequest req, HttpServletResponse resp) throws Exception {} @RequestMapping("/loginByQQCallbackHandler") public void loginByQQCallbackHandler(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {} }
因此我將QQ登陸的超連接定位到/login/loginByQQ
了異步
這一步也沒有什麼好說的,官方文檔把 ①請求發給誰②帶什麼參數③發完以後如何響應 這三點都說的清清楚楚了。因此代碼中第一步是從配置文件中獲取參數的值,而後經過 String 的 format方法 造成要發送的URL,這裏屢次使用+
進行字符串拼接也可,但這樣從語義上來說更加好理解。而後服務器跳轉到指定URL。函數
@RequestMapping("/loginByQQ") public void loginByQQ(HttpServletRequest req, HttpServletResponse resp) throws Exception { String response_type = QQLoginUtil.getQQLoginInfo("response_type"); String client_id = QQLoginUtil.getQQLoginInfo("client_id"); String redirect_uri = QQLoginUtil.getQQLoginInfo("redirect_uri"); //client端的狀態值。用於第三方應用防止CSRF攻擊。 String state = new Date().toString(); req.getSession().setAttribute("state", state); String url = String.format("https://graph.qq.com/oauth2.0/authorize" + "?response_type=%s&client_id=%s&redirect_uri=%s&state=%s", response_type, client_id, redirect_uri, state); resp.sendRedirect(url); }
根據本文第二節展現的控制器路徑,回調路徑一定以/login/loginByQQCallbackHandler
結尾,從而使這一步發出請求後,QQ那邊帶上說好的參數(以get的請求參數的形式)跳轉回來後,會由loginByQQCallbackHandler
方法來處理。
首先獲取 Authorization Code:String authorization_code = req.getParameter("code");
而後按照官方文檔的要求完成咱們的URL:`
if (authorization_code != null && !authorization_code.trim().isEmpty()) { //client端的狀態值。用於第三方應用防止CSRF攻擊。 String state = req.getParameter("state"); if (!state.equals(req.getParameter("state"))) { throw new RuntimeException("client端的狀態值不匹配!"); } String urlForAccessToken = getUrlForAccessToken(authorization_code); }
咱們先對以前官方文檔提到的用來作先後校驗的state參數進行校驗,只有經過才容許下一步。
而後就是getUrlForAccessToken
這個方法的具體實現:
public String getUrlForAccessToken(String authorization_code) { String grant_type = QQLoginUtil.getQQLoginInfo("grant_type"); String client_id = QQLoginUtil.getQQLoginInfo("client_id"); String client_secret = QQLoginUtil.getQQLoginInfo("client_secret"); String redirect_uri = QQLoginUtil.getQQLoginInfo("redirect_uri"); String url = String.format("https://graph.qq.com/oauth2.0/token" + "?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s", grant_type, client_id, client_secret, authorization_code, redirect_uri); return url; }
這些都沒什麼可說的。
以後就是跳轉這個URL去獲取 access_token,這裏就是第一個坑了,按照官方文檔,搞得好像此次咱們跳轉到這個獲取 access_token 的URL後,騰訊那邊會跳轉咱們設定的回調地址並帶上咱們須要的參數,就像以前獲取 authorization code 同樣。但徹底不是這樣的!!!你按照要求向這個獲取 access_token 的URL發送請求後,對方並不會再跳轉,而是直接返回你一個數據,但願你得到這個數據而後處理。這有點像前端JS的異步請求後後回調函數處理data。
因此在這裏咱們也要使用java後端去模擬客戶端來發起這個請求,所以我使用了 Spring容器 中的 RestTemplate
模塊,這部分代碼以下:
RestTemplate restTemplate = (RestTemplate) applicationContext.getBean("RestTemplate");
能夠看到咱們是將這個 RestTemplate
對象註冊在了 spring 容器中。註冊具體代碼就在 spring boot 的根配置文件中,具體代碼以下:
@Bean(name="RestTemplate") @Autowired public RestTemplate getRestTemplate(RestTemplateBuilder restTemplateBuilder){ RestTemplate restTemplate = restTemplateBuilder.build(); return restTemplate; }
其中 RestTemplateBuilder
類對象因爲 RestTemplate 是 Spring MVC 集成的模塊,其有內置的配置而且已經足夠咱們使用,只需使用@Autowired
註解注入便可。
至於怎麼在 loginServlet 這個 Controller 中獲得 Spring容器(applicationContext):在這個loginServlet
類中定義了一個ApplicationContext
類的成員變量而後使用@Autowired
註解以後,Spring 會將容器自動注入給它。關於這件事是否有更優雅的實現,答案目前彷佛是沒有:請問 Springboot 有沒有什麼優雅的方法能夠調用 getBean(String beanname) 來獲取 Bean?
如今弄清楚瞭如何獲取一個RestTemplate
的實例,也獲得了用來獲取 access token 的URL,咱們使用restTemplate
來發起GET請求並處理。
//第一次用服務器發起模擬客戶端訪問,獲得的是包含access_token的字符串,其格式以下 //access_token=0FFD92ABD1DFD4F5&expires_in=7776000&refresh_token=04CE5D1F1E290B0974C5 String firstCallbackInfo = restTemplate.getForObject(urlForAccessToken, String.class); String[] params = firstCallbackInfo.split("&"); String access_token = null; for (String param : params) { String[] keyvalue = param.split("="); if(keyvalue[0].equals("access_token")){ access_token = keyvalue[1]; break; } }
是的,正如官方文檔說的那樣,你獲得的返回的data是access_token=0FFD92ABD1DFD4F5&expires_in=7776000&refresh_token=04CE5D1F1E290B0974C5
這麼個奇葩玩意兒!不是一個JSON,騰訊在作API這件事上真是領先業界一百年啊,這多是五百年後人類從第四次世界大戰再次恢復到電氣時代後的最優秀的格式吧。
我這裏處理它的思路是先用&
分紅三段,而後再將每段用=
分紅兩個字符串,能夠認爲這兩個字符串呈 key-value 關係,而後獲得其中 key=access_token 的 value,也就是咱們的目標了。
另外再提一下RestTemplate
類的getForObject
方法對JSON有很好的支持,你能夠在第二個參數填入對應JSON的實體類的字節碼,但若是不勞煩RestTemplate
(不過其實內部是被 Spring boot 配置 Jackson 去解析的)的話,傳String.class
,就會獲得原本來本的返回數據了。
我沒用上。
此次獲取 data 的技術點基本上上一節都提到了,惟一的難點在於,此次返回的數據更加很差處理了。callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
這種JSONP的格式,騰訊你是真的牛逼。
先上前面獲取這個返回數據的代碼吧。
if (access_token != null && !access_token.trim().isEmpty()) { String url = String.format("https://graph.qq.com/oauth2.0/me?access_token=%s", access_token); //第二次模擬客戶端發出請求後獲得的是帶openid的返回數據,格式以下 //callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} ); String secondCallbackInfo = restTemplate.getForObject(url, String.class); }
接下來就是咱們要如何處理這個獲得的數據了,個人思路是使用正則表達式(正則表達式功能集成於JDK中),截取出{"client_id":"YOUR_APPID","openid":"YOUR_OPENID"}
這段內容,而後用 spring boot 內部集成的 jackson 模塊將其轉換爲一個 Map 對象後經過 get 方法獲得,代碼以下
//正則表達式處理 String regex = "\\{.*\\}"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(secondCallbackInfo); if(!matcher.find()){ throw new RuntimeException("異常的回調值: "+secondCallbackInfo); } //調用jackson ObjectMapper objectMapper = new ObjectMapper(); HashMap hashMap = objectMapper.readValue(matcher.group(0), HashMap.class); String openid = ((String) hashMap.get("openid"));
這裏有兩點值得一說
其一,爲何String regex = "\\{.*\\}";
,正則表達式中有\\
這東西呢?這時由於正則表達式中{
和}
都是有意義的,非字符的,咱們但願正則表達式把它們理解成字符,就須要對它們進行轉義,因此這裏須要一個轉義符\
,但\
自身在java字符串中並非字符,因此咱們還要轉義\
自身,因此會出現\\
。
其二,matcher
若是不經歷matcher.find()
,則就算有合適的匹配內容,也仍然不會有任何匹配能獲得。因此matcher.find()
是必須的,同時matcher.find()
一次後再來一次,那完了,返回false。
至此,就完成了獲得 openId 這個能惟一標識一個QQ的標識碼,以後的數據庫等操做就不屬於咱們的重點了,至於進一步地獲取用戶暱稱啊,頭像啊,結合官方文檔和以上經歷以後獲得的經驗,想來也不會有大問題了。那麼本文到此結束。
由於要讀取類資源路徑下的 properties 文件,又不但願讀取文件加載Properties
對象的無關代碼參入到方法中,我創建了一個工具類來集中這個過程。但面臨一個問題:靜態方法下沒法使用this.class.getClassLoader().getResourceAsStream(...)
,由於this
不出來。這時將其改成類的名字便可:QQLoginUtil.class.getClassLoader().getResourceAsStream(...)