前后端分离实践小结

发布在 小结

背景

​ 公司项目转型,要开新的运营管理平台,我提议借此开新项目的机会,进行前后端分离,由我负责带领小组新技术学习/分享,探坑填坑.

分离原因

  1. 最关键一点,受不了jsp/freemarker里一塌糊涂的代码,分离之后,代码按规范写,简洁,好管理
  2. 原本我们后端就一直在给Android/iOS提供接口,本次分离,可以方便以后H5端的项目重构,实现Android+iOS+H5的统一,一套接口可以供三端同时使用,大量节省工作量,也可以更好地保证公司产品质量的统一性.
  3. 方便后端专心处理数据,前端实现页面效果.而不是前端仅仅画页面写css,后端还需要复制粘贴过来,套数据,修改已有页面时,前端更是不方便插手.

技术选型

​ 组员后端技术都是SSM,前端都只是略接触过AngularJS/Vue.

​ 我之前接触了一下Spring Boot,感觉配置很清爽,搭框架轻松,业务写起来快,可以让组员无缝切过来,无需费时学习,因此后端框架选定Spring Boot.而登录控制与权限管理一直是公司以前几个项目的弱项,因此本次决定引入shiro,而组员无一对shiro熟悉的,研究shiro的任务自然由我承担.

​ 前端框架是本次前后端分离的重点,由于我们组无人熟悉前端的新框架,所以这次势必每个人都需要学习许多新的前端知识.我拿公司的H5项目的几个页面进行过搭建vue框架重构练手,认为vue文档清晰,资料丰富,相关开源方案够多,我们这次可以放心学习,使用.(当然最主要的还是因为坑全都得由我来填,得挑个熟悉的)

目前进展

​ 后端:Spring Boot框架搭建完成,shiro可以进行权限管理,自定义拦截器,常用工具类完成,对常用的增删改查,返回结果,异常处理都可以快速搞定.

​ 前端:数据交互封装完毕,路由熟悉,前端权限管理初步熟悉,选用饿了么开源的Element框架,常用的增删改查的页面元素及工具方法都已让大家掌握.

​ 本周一开始教组员使用Intellij IDEA,教前端快速铺页面的方法,介绍前后端我封装的各种小轮子.经过一周时间,全部都已熟悉这套新的前后端框架,每个人都可以独立快速地推出常用页面.

接口端小技巧

​ 因为后端全部返回统一json格式的接口,所以我设计了一些小的工具方法,方便快速推出新接口,节省重复代码.

  1. 使用JSONObject而不是实体类

    ​ 因为后端业务不算复杂,所以舍弃了实体类的语义性,转而使用阿里的fastjson的JSONObject接收MyBatis返回的结果.比如一个简单的查询只需要

    1
    2
    3
    4
    5
    6
    7
    8
    <select id="getSimple" resultType="com.alibaba.fastjson.JSONObject">
    SELECT
    s.order_id shopOrderId,
    s.operation operation,
    date_format(s.create_time, '%Y.%m.%d %H:%i:%s') createTime
    FROM shop_order_log s
    WHERE s.user_delete_status = "1"
    </select>

    就可以将三个字段shopOrderId,operation,createTime放入json内,字段名称更加灵活,也免去了大量的建实体类,写resultMap的代码.

    ​ 更好的一点是,不使用实体类,可以在返回结果中避免掉很多空的需不要的字段.

    ​ 还有,因为所有的接口层都返回JSONObject,所以编写工具方法,快速返回成功/失败结果也很简单,我写了多个工具方法,包括入参转json,入参非空校验,返回成功/失败结果,分页,这里就不一一贴出代码了.

  2. 自定义Exception

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class CommonJsonException extends RuntimeException {
    private JSONObject resultJson;

    /**
    * 调用时可以在任何代码处直接throws这个Exception,
    * 都会统一被拦截,并封装好json返回给前台
    *
    * @param errorEnum 以错误的ErrorEnum做参数
    */
    public CommonJsonException(ErrorEnum errorEnum) {
    JSONObject jsonObject = new JSONObject();
    CommonUtil.returnJson(jsonObject, errorEnum);
    this.resultJson = jsonObject;
    }

    public CommonJsonException(JSONObject resultJson) {
    this.resultJson = resultJson;
    }

    public JSONObject getResultJson() {
    return resultJson;
    }
    }

    @ControllerAdvice
    @ResponseBody
    public class GlobalExceptionHandler {
    @ExceptionHandler(CommonJsonException.class)
    public JSONObject CommonJsonExceptionHandler(CommonJsonException commonJsonException) throws Exception {
    return commonJsonException.getResultJson();
    }
    }

    在需要返回给前端错误代码编号时,比如校验参数非空,校验手机号失败,可以直接抛出此异常,经错误拦截器拦截到此异常后,可以直接返回错误码给前端,节省大量的判断/返回的代码.这一点也是前阵子从Spring源码中学到的.

存在的问题

  1. 近期主要问题基本都是待我去研究深入的技术,比如shiro的动态权限与vue-router的异步路由的结合,vue组件的通信,vuex的状态管理
  2. 其次是组员需要对vue更加熟悉,我这次要求了大家抛弃jQuery,用数据绑定的思想去写前端.目前大家的前端水平也还只是可以快速复制粘贴出页面,对于复杂的页面和组件,甚至对我们前端项目的目录结构与各部分功能,都不算掌握.
  3. 目前我们所谓的前后端分离其实还只是初步的业务和代码上的分离,以后如果要重构H5项目,可能还需要开NodeJS项目来解决和Android/iOS一样的接口加密问题,统一session管理问题,以及更多的前后端分离可能给我们带来的问题.

评论和共享

​ 最近在做前后端分离的新框架,选用了后端springboot+shiro,前端vue+elementUI,第一次搭SSM之外的非demo项目,尤其shiro更是之前从未接触,折腾了很多天,遇到很多问题,大部分能百度出来,剩下的非常费时的问题且称之为坑吧.

跨域

​ 一大部分问题就是跨域造成的,本身vue-cli搭建的项目可以用

1
2
3
4
5
6
7
8
9
10
11
// 1. axios的baseURL设置为/api
// 2. 如下设置
proxyTable: {
'/api': {
target: 'http://127.0.0.1:8080/',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
}
},

来解决开发环境跨域的问题,生产环境反正可以打包静态文件到springboot项目中直接跑.

而我还是脑抽得选择了强行跨域,假装自己要把静态文件单独放个服务器跑…为此遇到很多问题,折腾好几天.

因此强力推荐上面的方案,简单快捷地解决跨域!

如果生产环境真的需要跨域的话,再按下面方法设置

大部分跨域的配置都能百度搜出来:

  1. axios要 withCredentials: true

  2. 用session而不用啥特殊的token之类的话,就不用 config.headers['X-Token'] = getToken() ,这句是vueAdmin-template 中带的,需要删掉.

  3. 后端需要配置允许跨域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Configuration
    public class CorsConfig {
    private CorsConfiguration buildConfig() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
    corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
    corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
    corsConfiguration.setAllowCredentials(true);
    return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", buildConfig()); // 4 对接口配置跨域设置
    return new CorsFilter(source);
    }
    }

shiro拦截ajax返回json

​ shiro拦截到需要登录而用户尚未的请求时,会重定向至 /login/auth (未配置时是login.jsp),而ajax是不允许重定向的,ajax会收到302错误码,报错

Failed to load http://localhost:8080/test: Redirect from ‘http://localhost:8080/test‘ to ‘http://localhost:8080/login/auth‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:9528‘ is therefore not allowed access.

因此我们需要对拦截器进行改造,不要它默认的重定向了,我们直接去response里把json写好输出给前端.

自定义filter,(因为我的后端全部返回json,所以这里不判断是否ajax了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class AjaxPermissionsAuthorizationFilter extends FormAuthenticationFilter {

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("returnMsg", "未登录或登录已失效");
if (this.isLoginRequest(request, response)) {
if (this.isLoginSubmission(request, response)) {
return this.executeLogin(request, response);
} else {
return true;
}
} else {
PrintWriter out = null;
HttpServletResponse res = (HttpServletResponse) response;
//下面这几行也是后面要讲的坑, 这里的Access-Control-Allow-Origin 设置为*的话,前端还是会报错.
res.setHeader("Access-Control-Allow-Origin", "http://localhost:9528");
// response1.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
try {
res.setCharacterEncoding("UTF-8");//设置编码
res.setContentType("application/json");//设置返回类型
out = response.getWriter();
out.println(jsonObject);//输出
} catch (Exception e) {
} finally {
if (null != out) {
out.flush();
out.close();
}
}
return false;
}
}
}

接下来就是最坑的地方了,拦截器的注入.

原本搜到的方法是在ShiroConfiguration类中注入.

1
2
3
4
5
6
7
8
9
@Bean(name = "ajaxPermissionsAuthorizationFilter")
public AjaxPermissionsAuthorizationFilter ajaxPermissionsAuthorizationFilter(){
return new AjaxPermissionsAuthorizationFilter();
}

//然后shiroFilterFactoryBean设置过滤器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new AjaxPermissionsAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);

结果自定义的拦截器把所有的请求都拦截了,直接无视了我设置的

1
2
3
4
5
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login/**", "anon");
filterChainDefinitionMap.put("/error", "anon");
filterChainDefinitionMap.put("/**", "authc");

各种百度,能搜到的相关的资料也就只有segmentfault的提问没解决,题主给我的回复也不能解决 , 百度知道提问的没解决 , 简书方案并不能解决 .

最终在某个百度结果的第三页找到这篇博客 ,博主对问题一步一步的排查分析,debug源码,最终知道

Springboot 先加载了我们自定义的 Filter,然后再加载了 ShiroFilter

解决方法:
在自定义的filter里加上下面的代码

1
2
3
4
5
6
@Bean
public FilterRegistrationBean registration(自定义Filter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}

在configration类里就不需要声明这个bean,只需要直接调用 filterMap.put("authc", new 自定义Filter());

Access-Control-Allow-Origin

在上面的自定义filter里,

1
res.setHeader("Access-Control-Allow-Origin", "http://localhost:9528");

如果设置为*的话,前端虽然可以收到json,但还是会报错

Failed to load http://localhost:8080/test: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. Origin ‘http://localhost:9528‘ is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

不允许设置为通配符* .

虽然此处设置了”http://localhost:9528“ 但是这种做法终究不合适.

百度继续搜到此博客

得到解决方法

1
res.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));

评论和共享

  • 第 1 页 共 1 页
作者的图片

heeexy

世上是不是就没有你不认识的字了?


JAVA


南京