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<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: {//手机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<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;
        }
      )
    }

这段代码的重点,在于通过 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<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方法中,检查全局变量中存储的已认证用户信息,并设置到请求的cookie中。
  • 设置请求的 withCredentials: true,确保认证信息会被传递过去。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351