2018-09-26 配置SpringMvc来支持来自ionic的请求。

在web中,我们使用spring-security基于http进行请求认证。在cordova中,由于请求实际上是访问手机本地的资源,因此,其资源的url,不是 http://ip:port/res,而是 file://res

它带来的问题大概有如下问题:

1. 从手机app端h5页面访问后台url资源时的跨域访问问题。

问题原因:

从手机app端主要访问本地路径上的h5页面,页面域名是 file://;而后台的url资源,其域名要么是 http://ip:port/appName,要么是类似 http://www.fredworks.cn/appName。因此构成了跨域。

一般而言,会因为cors规范导致请求被拒绝,结果得到403异常。

解决方案:

h5页面的跨域访问问题,需要后台服务器资源启用cors服务,并允许来自cordova的请求访问。

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服务。

你不启用cors配置,甚至通过类似这样的配置 http.cors().disable() 显式的关闭cors服务,并不会让cors不工作。你关闭的,其实只是服务器端对cors的授权控制。

而现代的浏览器,基本都已经实现了对cors规范的默认支持。无论服务器端是否有开启cors授权,浏览器都会向服务器询问cors授权结果。关闭服务器端的cors控制,只会导致浏览器向服务器发出跨域检查请求时,服务器无法对此进行授权,从而导致请求无法通过cors控制而得到403异常。

自定义CORS配置内容

SpringMvc通过CorsFilter来完成CORS控制。SpringMvc创建CorsFilter时,会自动寻找一个名字为 “corsConfigurationSource“,类型为 CorsConfigurationSource 的 Bean 作为配置参数来完成初始化。因此,我们只要自定义一个CorsConfigurationSource,就可以完成对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 方法支持两个特定的域:

  • "http://localhost:8100":是为了允许使用 ionic serve 进行浏览器端模拟测试阶段的请求不要被跨域控制拒绝。
  • "file://":是为了允许ionic的手机包中的页面资源请求服务器资源时不要被跨域控制拒绝。

2. 会话保持问题。

问题的原因:

来自cordova的请求和来自web的请求不同,来自cordova的请求不会携带cookie信息,从而导致不会携带会话ID,丢失认证信息。

当访问需要认证的资源时,会因为没有会话ID,从而被spring-security的安全控制拦截和拒绝,最终得到403异常。

解决方案:

第一步,要在服务器端的cors控制中,允许跨域请求携带认证信息。

否则spring-mvc将不会处理http请求头信息中携带的会话ID,从而导致请求找不到匹配的会话:

    @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要处理跨域请求中携带的会话ID信息。

第二步,服务器端登陆通过后,要返回会话ID给前端

cordova使用webview来渲染web页面,和正常的浏览器不一样,它不支持cookie,无法像正常的浏览器一样,在cookie中自动存储会话ID。因此,服务器端需要在登陆通过后,将会话ID作为请求返回数据的一部分来返回给cordova端的h5页面。

/**
 * 登录成功后,不要跳转到新页面,而是返回json数据,以满足手机端使用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: {//手机app端用户登录,需要将会话ID作为数据返回
                SingleResult 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: {//手机wap端用户登录,重定向到登录前原页面或主页。
                //设置手机wap的默认登录后页面为wap端主页
                this.setDefaultTargetUrl("");
                super.onAuthenticationSuccess(request, response, authentication);
                break;
            }
           default: {
                super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    }
}
  • 以上代码,是在登陆成功处理器中,判断是否是手机端登陆。如果是,则覆盖默认的行为(跳转到登陆前url,或配置中指定的特定页面),不跳转到特定页面(cordova的页面是无法在服务器端进行跳转的,它根本就不在服务器上,而是在手机app本地),而是通过json格式返回数据结构,其中就包含有登陆后的会话ID。

  • 由于会话ID,一般在cookie中使用变量名 JSESSIONID 来记录,因此我们也使用该变量名,只是做了java风格的命名规范改造。

第三步,需要在cordova的h5页面端接收并存储登陆后的会话ID

cordova使用webview来渲染web页面,和正常的浏览器不一样,它不支持cookie,无法像正常的浏览器一样,在cookie中自动存储会话ID。因此,h5页面需要将接收到的会话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>(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;
        }
      )
    }

这段代码的重点,在于通过 UserInfo.currentUser = rejObj.data;将返回的数据存储起来了。其中,UserInfo.currentUser 是一个存储当前登陆用户信息的和后端约定好的数据结构,其中就有会话ID。
你可以采用自己的存储数据的方法,比如存在全局变量中,或存储在手机端的sqlite数据库中,或其他适当的方式。

第四步,设置angular的http请求基础方法,在http header中设置会话参数 JSEESIONID,并启用认证
我用的是angular8,因此代码大致如下:

  /**
   * 预处理options对象,主要是在headers中添加会话ID的cookie的属性。
   * @param options 待处理的options对象
   */
  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;
  }

  /**
   * 构建一个Get请求,它发送请求前,先预处理请求参数,增加认证会话信息。
   * @return an `Observable` of the `HttpResponse` for the request, with a body type of `T`.
   */
  get(url: string, options?: {
      headers?: HttpHeaders;
      observe: 'body';
      params?: HttpParams;
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
  }): Observable {
      options = this.processOptions(options);
      return this.http.get(environment.ctx + url, options);
  }
  • processOptions方法中,检查全局变量中存储的已认证用户信息,并设置到请求的cookie中。
  • 设置请求的 withCredentials: true,确保认证信息会被传递过去。

你可能感兴趣的:(2018-09-26 配置SpringMvc来支持来自ionic的请求。)