Spring Security 整合 微信小程式登入的思路探討

碼農小胖哥發表於2021-03-05

1. 前言

原本打算把Spring SecurityOAuth 2.0的機制講完後,用小程式登入來實戰一下,發現小程式登入流程和Spring SecurityOAuth 2.0登入的流程有點不一樣,就把寫了半天的東西全部推翻了。但是,但是過了一天之後,突然感覺又可以了。我們來一起試一試。

2. 小程式登入流程分析

小程式的登入流程是這樣的:

微信小程式登入時序圖

而在Spring Security中的OAuth 2.0 Code模式是這樣的:

Spring Security OAuth2.0 Code模式時序圖

從這兩張圖上看最大的差別就是微信小程式中獲取code不需要通過後端伺服器的呼叫,而Spring Security中需要(第1步,第2步,第3步)。騰訊應該也是借鑑了OAuth 2.0,但是做了一些改動。

讓我放棄的也是這個差別,有沒有人能想到解決方法呢?

假如說小程式已經持有了code,它依然需要將code傳遞給後端伺服器來執行後面的流程。那麼我們能不能利用圖2中第3個呼叫redirectUri的步驟呢?換個角度來看問題第三方就是小程式反正它也是將一個code傳遞給了後端伺服器,只要返回登入狀態就行了,反正剩下的登入流程都跟小程式無關。我覺得它是可以的。在Spring Security中我們可以使用code通過tokenUri來換取token。那麼在微信小程式登入流程中,code最終換取的只是登入態,沒有特定的要求。但是後端肯定需要去獲取使用者的一些資訊,比如openId,使用者微信資訊之類的。總之要根據微信平臺提供的API來實現。通過改造tokenUriuserInfoUri可以做到這一點。

3. 思路借鑑

所有的猜想都沒有錯,而且我也實現了,但是改造成本過高了,寫了很多相容性的程式碼,如果不深入Spring Security,很難實現這,而且也不好理解。

為了簡化實現,我決定借鑑Spring SecurityOAuth 2.0的思路。Filter攔截小程式登入URL,然後通過RestTemplate執行向微信伺服器請求獲取結果,處理後返回登入態。時序圖如下:

小程式登入開發時序圖

對應的虛擬碼實現:

package cn.felord.spring.security.filter;

import org.springframework.http.ResponseEntity;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;

/**
 * 小程式登入過濾器
 *
 * @author felord.cn
 * @since 1.0.4.RELEASE
 */
public class WeChatAppLoginFilter extends OncePerRequestFilter {

    private final RequestMatcher requiresAuthenticationRequestMatcher;
    private final RestTemplate restTemplate;
    private String appId;
    private String secret;
    private static final String WX_URL = "https://api.weixin.qq.com/sns/jscode2session";

    public WeChatAppLoginFilter(String loginProcessingUrl, String appId, String secret) {
        this.appId = appId;
        this.secret = secret;
        Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
        this.requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
        this.restTemplate = new RestTemplate();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 攔截微信登入
        if (requiresAuthenticationRequestMatcher.matches(request)) {
            //todo 從request中獲取 code 引數 這裡邏輯根據你的情況自行實現
            String jsCode = "你自行實現從request中獲取";
            //todo 必要的校驗自己寫
            MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
            queryParams.add("appid", this.appId);
            queryParams.add("secret", this.secret);
            queryParams.add("js_code", jsCode);
            queryParams.add("grant_type", "authorization_code");


            URI uri = UriComponentsBuilder.fromHttpUrl(WX_URL)
                    .queryParams(queryParams)
                    .build()
                    .toUri();
            //todo 這裡 Object 自行封裝為具體物件
            ResponseEntity<Object> result = this.restTemplate.getForEntity(uri, Object.class);

            //todo 處理 result 比如後端儲存、後端授權、角色資源處理、註冊、對session_key的處理等等你需要的業務邏輯
            // 最後放入HttpServletResponse中返回前端返回

        } else {
            filterChain.doFilter(request, response);
        }
    }
}

最後一定別忘了把過濾器配置到WebSecurityConfigurerAdapterHttpSecurity中去。

4. 總結

本篇講解了Spring Security和微信小程式登入相結合的思路歷程。本來不需要長篇大論OAuth 2.0,之所以寫出來是讓你明白開發中要善於發現一些相似的東西,通過差異對比來探討他們結合的可能性,這也是一種自我提升的方法。方法遠比結果重要,形成自己的方法論就能富有創造力。我是:碼農小胖哥,多多關注,分享更多原創程式設計乾貨。

關注公眾號:Felordcn 獲取更多資訊

個人部落格:https://felord.cn

相關文章