2018-09-26 配置SpringMvc來支持來自ionic的請(qǐng)求。

在web中,我們使用spring-security基于http進(jìn)行請(qǐng)求認(rèn)證。在cordova中,由于請(qǐng)求實(shí)際上是訪問手機(jī)本地的資源,因此,其資源的url,不是 http://ip:port/res,而是 file://res 。

它帶來的問題大概有如下問題:

1. 從手機(jī)app端h5頁面訪問后臺(tái)url資源時(shí)的跨域訪問問題。

問題原因:

從手機(jī)app端主要訪問本地路徑上的h5頁面,頁面域名是 file://;而后臺(tái)的url資源,其域名要么是 http://ip:port/appName,要么是類似 http://www.fredworks.cn/appName。因此構(gòu)成了跨域。

一般而言,會(huì)因?yàn)閏ors規(guī)范導(dǎo)致請(qǐng)求被拒絕,結(jié)果得到403異常。

解決方案:

h5頁面的跨域訪問問題,需要后臺(tái)服務(wù)器資源啟用cors服務(wù),并允許來自cordova的請(qǐng)求訪問。

spring-mvc,是通過如下代碼啟用cors控制的:

/**
 * 各模塊安全配置類的基類。
 * @author wangqiang
 * 2018年8月26日 上午12:53:40
 */
public abstract class ModuleSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    
    /**
     * 啟用cors控制
     * 2018年8月26日 下午11:07:37 wangqiang添加此方法
     * @param http
     * @throws Exception
     */
    private void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
            ...
    }
}

上述代碼的目的,是為了確保啟用了cors服務(wù)。

你不啟用cors配置,甚至通過類似這樣的配置 http.cors().disable() 顯式的關(guān)閉cors服務(wù),并不會(huì)讓cors不工作。你關(guān)閉的,其實(shí)只是服務(wù)器端對(duì)cors的授權(quán)控制。

而現(xiàn)代的瀏覽器,基本都已經(jīng)實(shí)現(xiàn)了對(duì)cors規(guī)范的默認(rèn)支持。無論服務(wù)器端是否有開啟cors授權(quán),瀏覽器都會(huì)向服務(wù)器詢問cors授權(quán)結(jié)果。關(guān)閉服務(wù)器端的cors控制,只會(huì)導(dǎo)致瀏覽器向服務(wù)器發(fā)出跨域檢查請(qǐng)求時(shí),服務(wù)器無法對(duì)此進(jìn)行授權(quán),從而導(dǎo)致請(qǐng)求無法通過cors控制而得到403異常。

自定義CORS配置內(nèi)容

SpringMvc通過CorsFilter來完成CORS控制。SpringMvc創(chuàng)建CorsFilter時(shí),會(huì)自動(dòng)尋找一個(gè)名字為 “corsConfigurationSource“,類型為 CorsConfigurationSource 的 Bean 作為配置參數(shù)來完成初始化。因此,我們只要自定義一個(gè)CorsConfigurationSource,就可以完成對(duì)cors的配置。

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS"));
        config.setAllowedOrigins(Arrays.asList("http://localhost:8100", "file://"));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return source;
    }

上述代碼中, .allowedOrigins 方法支持兩個(gè)特定的域:

  • "http://localhost:8100":是為了允許使用 ionic serve 進(jìn)行瀏覽器端模擬測(cè)試階段的請(qǐng)求不要被跨域控制拒絕。
  • "file://":是為了允許ionic的手機(jī)包中的頁面資源請(qǐng)求服務(wù)器資源時(shí)不要被跨域控制拒絕。

2. 會(huì)話保持問題。

問題的原因:

來自cordova的請(qǐng)求和來自web的請(qǐng)求不同,來自cordova的請(qǐng)求不會(huì)攜帶cookie信息,從而導(dǎo)致不會(huì)攜帶會(huì)話ID,丟失認(rèn)證信息。

當(dāng)訪問需要認(rèn)證的資源時(shí),會(huì)因?yàn)闆]有會(huì)話ID,從而被spring-security的安全控制攔截和拒絕,最終得到403異常。

解決方案:

第一步,要在服務(wù)器端的cors控制中,允許跨域請(qǐng)求攜帶認(rèn)證信息。

否則spring-mvc將不會(huì)處理http請(qǐng)求頭信息中攜帶的會(huì)話ID,從而導(dǎo)致請(qǐng)求找不到匹配的會(huì)話:

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS"));
        config.setAllowedOrigins(Arrays.asList("http://localhost:8100", "file://"));
        
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return source;
    }

上述代碼中, .allowCredentials(true) 就是用來告訴spring-mvc要處理跨域請(qǐng)求中攜帶的會(huì)話ID信息。

第二步,服務(wù)器端登陸通過后,要返回會(huì)話ID給前端

cordova使用webview來渲染web頁面,和正常的瀏覽器不一樣,它不支持cookie,無法像正常的瀏覽器一樣,在cookie中自動(dòng)存儲(chǔ)會(huì)話ID。因此,服務(wù)器端需要在登陸通過后,將會(huì)話ID作為請(qǐng)求返回?cái)?shù)據(jù)的一部分來返回給cordova端的h5頁面。

/**
 * 登錄成功后,不要跳轉(zhuǎn)到新頁面,而是返回json數(shù)據(jù),以滿足手機(jī)端使用spring-security的目的。
 * @author wangqiang
 * 2018年8月26日 上午12:41:05
 */
@Service(SecurityModuleBeanNames.SecurityModuleAuthenticationSuccessHandler)
public class JsonReturningAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {

        String username = request.getParameter("username");
        AuthenInfo authenInfo = GsonUtils.fromJson(AuthenInfo.class, username);
        switch (authenInfo.getLoginType()) {
            case MobileUserLoginType.Value: {//手機(jī)app端用戶登錄,需要將會(huì)話ID作為數(shù)據(jù)返回
                SingleResult<AuthenResultDto> retObj = new SingleResult<>();
                retObj.setSuccess(true);
                AuthenResultDto data = new AuthenResultDto();
                retObj.setData(data);
                
                String sessionId = request.getSession().getId();
                data.setSessionId(sessionId);
                
                ISecurityUser user = (ISecurityUser) authentication.getPrincipal();
                data.setName(user.getUsername());
                data.setMobile(user.getMobile());
                
                response.getWriter().write(retObj.toString());
                response.getWriter().flush();
                break;
            }
            case WapUserLoginType.Value: {//手機(jī)wap端用戶登錄,重定向到登錄前原頁面或主頁。
                //設(shè)置手機(jī)wap的默認(rèn)登錄后頁面為wap端主頁
                this.setDefaultTargetUrl("");
                super.onAuthenticationSuccess(request, response, authentication);
                break;
            }
           default: {
                super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    }
}
  • 以上代碼,是在登陸成功處理器中,判斷是否是手機(jī)端登陸。如果是,則覆蓋默認(rèn)的行為(跳轉(zhuǎn)到登陸前url,或配置中指定的特定頁面),不跳轉(zhuǎn)到特定頁面(cordova的頁面是無法在服務(wù)器端進(jìn)行跳轉(zhuǎn)的,它根本就不在服務(wù)器上,而是在手機(jī)app本地),而是通過json格式返回?cái)?shù)據(jù)結(jié)構(gòu),其中就包含有登陸后的會(huì)話ID。

  • 由于會(huì)話ID,一般在cookie中使用變量名 JSESSIONID 來記錄,因此我們也使用該變量名,只是做了java風(fēng)格的命名規(guī)范改造。

第三步,需要在cordova的h5頁面端接收并存儲(chǔ)登陸后的會(huì)話ID

cordova使用webview來渲染web頁面,和正常的瀏覽器不一樣,它不支持cookie,無法像正常的瀏覽器一樣,在cookie中自動(dòng)存儲(chǔ)會(huì)話ID。因此,h5頁面需要將接收到的會(huì)話ID保存在本地。

比如如下的angular代碼:

    /**
     * 客戶登錄功能
     */
    login() {
      let me = this;
      // 密碼登錄
      const url = '/security/authen/login';
      let username = {
        loginType: 1,
        authenKey: me.mobileNo,
        password: me.password
      };
      let params = new HttpParams()
        .set('username', JSON.stringify(username))
        .set('password', me.password);
      me.http.post<SingleResult<UserInfo>>(url, params).subscribe(
        rejObj => {
          if (rejObj.success) {
            UserInfo.currentUser = rejObj.data;
            me.navCtrl.navigateForward(this.targetUrl);
          } else {
            me.errorMsg = rejObj.message;
          }
        },
        error => {
          console.error(error.message, error);
          me.errorMsg = error.message;
        }
      )
    }

這段代碼的重點(diǎn),在于通過 UserInfo.currentUser = rejObj.data;將返回的數(shù)據(jù)存儲(chǔ)起來了。其中,UserInfo.currentUser 是一個(gè)存儲(chǔ)當(dāng)前登陸用戶信息的和后端約定好的數(shù)據(jù)結(jié)構(gòu),其中就有會(huì)話ID。
你可以采用自己的存儲(chǔ)數(shù)據(jù)的方法,比如存在全局變量中,或存儲(chǔ)在手機(jī)端的sqlite數(shù)據(jù)庫(kù)中,或其他適當(dāng)?shù)姆绞健?/p>

第四步,設(shè)置angular的http請(qǐng)求基礎(chǔ)方法,在http header中設(shè)置會(huì)話參數(shù) JSEESIONID,并啟用認(rèn)證
我用的是angular8,因此代碼大致如下:

  /**
   * 預(yù)處理options對(duì)象,主要是在headers中添加會(huì)話ID的cookie的屬性。
   * @param options 待處理的options對(duì)象
   */
  private processOptions(options?: {
      headers?: HttpHeaders ;
      observe: 'body';
      params?: HttpParams;
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
  }): any {
      if (options) {
          let httpHeaders = options.headers || new HttpHeaders();
          if (UserInfo.currentUser && UserInfo.currentUser.sessionId) {
              options.headers = httpHeaders.set('Cookie', 'JSESSIONID=' + UserInfo.currentUser.sessionId);
          }
          options.withCredentials = true;
      } else {
          let httpHeaders = new HttpHeaders();
          if (UserInfo.currentUser && UserInfo.currentUser.sessionId) {
              httpHeaders = httpHeaders.set('Cookie', 'JSESSIONID=' + UserInfo.currentUser.sessionId);
          }
          options = {
              headers: httpHeaders,
              observe: 'body',
              params: null,
              reportProgress: false,
              responseType: 'json',
              withCredentials: true
          };
      }

      return options;
  }

  /**
   * 構(gòu)建一個(gè)Get請(qǐng)求,它發(fā)送請(qǐng)求前,先預(yù)處理請(qǐng)求參數(shù),增加認(rèn)證會(huì)話信息。
   * @return an `Observable` of the `HttpResponse` for the request, with a body type of `T`.
   */
  get<T>(url: string, options?: {
      headers?: HttpHeaders;
      observe: 'body';
      params?: HttpParams;
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
  }): Observable<T> {
      options = this.processOptions(options);
      return this.http.get<T>(environment.ctx + url, options);
  }
  • processOptions方法中,檢查全局變量中存儲(chǔ)的已認(rèn)證用戶信息,并設(shè)置到請(qǐng)求的cookie中。
  • 設(shè)置請(qǐng)求的 withCredentials: true,確保認(rèn)證信息會(huì)被傳遞過去。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容