关于前后端分离的思考和总结

超、凢脫俗 2023-06-25 10:22 112阅读 0赞

对目前的web来说,前后端分离已经变得越来越流行了,越来越多的企业/网站都开始往这个方向靠拢。那么,为什么要选择前后端分离呢?前后端分离对实际开发有什么好处呢?我之前一直对前后端分离的思想一直很模糊,最近恰好碰上公司的项目进行重构,也采用前后端分离。所以就根据自己在实际项目中的开发,总结自己对于前后端分离中遇到的一些疑惑。

前言

首先在此之前,我跟大多数人一样,心中有如下疑问?

  • 什么是前后端分离?
  • 前后端分离的意义打不打?
  • 如何进行前后端分离?

那么文章将围绕这三个疑问进行展开,当然文章的重点还是总结如何进行前后端分离。

前后端分离的意义大不大?

  • 如果系统的业务比较复杂,网站前端变化远比后端变化频繁,则意义大。
  • 该网站尚处于原始开发模式,数据逻辑与表现逻辑混杂不请,则意义大。
  • 该网站要适配多平台,需要对设备的兼容性有要求,则意义大。
  • 该网站将业务拆分成微服务,则意义大。

如何进行前后端分离?

那如何进行前后端分离,这里我只针对后台来讨论,因为现在主要负责后台的开发,那至于说前端如何请求数据,前端数据的缓存等等这个就不在这里讨论了。要想实现前后端解耦,后端必须遵守RESTful API的设计准则。RESTful API 是目前比较成熟的一套互联网应用程序的 API 设计理论,至于具体什么是 RESTful API,可以参考阮一峰老师的博文:RESTful API 设计指南,便会对RESTful API 有个大概的了解。那要搭建一个RESTful API的后台项目具体需要考虑哪些东西呢?我根据我自己的实际开发,总结了如下几个点:

  • 统一响应结构
  • 前台请求规范
  • API接口文档
  • 统一异常处理
  • 后台参数验证
  • 跨域请求处理
  • 请求鉴权机制

统一响应结构

我们在开发之前,需要跟前后端约定好,每次ajax请求,后端都需要返回一个统一的数据格式。如果格式不统一,前端请求每次拿到的数据很乱,如果前端页面变化的比较频繁,那么后期维护的成本很大。下面就是一个json格式的响应结构:

  1. {
  2. data : { // 请求数据,对象或数组均可
  3. user_id: 123,
  4. user_name: "tutuge",
  5. user_avatar_url: "http://tutuge.me/avatar.jpg"
  6. ...
  7. },
  8. msg : "请求成功!", // 请求状态描述,调试用
  9. code: 500, // 业务自定义状态码,比如500表示请求失败,200表示请求成功
  10. extra : { // 全局附加数据,字段、内容不定,可能为null
  11. type: 1,
  12. desc: "签到成功!"
  13. }
  14. }

对应的java的实体类ResultBean.java

  1. public class ResultBean {
  2. /**
  3. * 数据集
  4. */
  5. private Object data = null;
  6. /**
  7. * 返回信息
  8. */
  9. private String msg = "Request Success!";
  10. /**
  11. * 业务自定义状态码
  12. */
  13. private Integer code = 200;
  14. /**
  15. * 全局附加数据
  16. */
  17. private Object etxra = null;
  18. public Object getData() {
  19. return data;
  20. }
  21. public void setData(Object data) {
  22. this.data = data;
  23. }
  24. public String getMsg() {
  25. return msg;
  26. }
  27. public void setMsg(String msg) {
  28. this.msg = msg;
  29. }
  30. public Integer getCode() {
  31. return code;
  32. }
  33. public void setCode(Integer code) {
  34. this.code = code;
  35. }
  36. public Object getEtxra() {
  37. return etxra;
  38. }
  39. public void setEtxra(Object etxra) {
  40. this.etxra = etxra;
  41. }
  42. }

前台请求规范

后台的响应结构已经确定好,那么前端的请求是不是得规范一下呢,答案肯定是的!因为我们采用的是RESTful API设计原则,我们会严格按照约定来使用 HTTP method:

  • GET: 查询

    • 若查询参数在3个以下(包含3个),采用如下的请求方式:http://localhost:8080/app/getUserList?age=12&name=Jack&sex=1,将参数拼接到url后面,后台采用@RequestParam注解接收。
    • 若查询参数在三个以上,后台采用domain实体接收封装的参数。
  • POST: 创建

    • 请求参数类型为body,也就是json对象,将对应的参数封装成一个类,然后后台使用@RequestBody注解将参数自动解析成该类的一个实例。
  • PUT: 修改

    • 第一个主键参数,他的请求url为:http://localhost:8080/app/updateUser/{userId},采用@PathVariable注解接收。
    • 第二个请求参数,类型为body,json对象,跟POST创建请求一样,只是该json对象只放修改的属性内容,采用@RequestBody注解接收。
  • DELETE: 删除

    • 请求url:http://localhost:8080/app/deleteUser/{userId}
    • 后台采用@PathVariable注解接收参数。

标准的RESTful API请求示例:

RESTful

对于controller层规范问题,我觉得有如下几点可以考虑:

  • controller里面的方法参数,尽量不要使用json,map去接收,因为map,json这种格式灵活,但是可读性差,如果放业务数据,每次阅读起来都比较困难,定义一个bean看着工作量多了,但代码清晰多了。
  • controller方法统一返回ResultBean。
  • ResultBean是controller专用的,其他层不能用。
  • 不要把json、map这类数据往service层传。

API接口文档

写后台的同学有没有这样的烦劳,每次写完相关的接口,都要写相关的接口文档,然后跟前端小伙伴进行联调,过程很是繁琐和费时间。那为了解决这些问题,Swagger2 就是一个很好的解决方案,它与 spring mvc 整合后,我们只需要少量的注解,它便可以自动的帮我们生成一份 RESTful API 文档,大大的减轻了劳动力。因为之前有写过一篇关于这个问题文章:Spring MVC中使用Swagger2构建Restful API,这里就不在重复叙述了。

统一异常处理

采用spring的AOP(面向切面编程),编写一个全局的异常处理切面类,统一处理所有的异常。定义一个类,然后用@ControllerAdvice注解将其标注即可,同时用@ResponseBody注解表示返回值可序列化为JSON字符串。代码如下(ExceptionAspect.java):

  1. /**
  2. * 全局异常处理切面
  3. * @author leeyom
  4. * @date 2017年10月19日 10:41
  5. */
  6. @ControllerAdvice
  7. @ResponseBody
  8. public class ExceptionAspect {
  9. private static final Logger log = Logger.getLogger(ExceptionAspect.class);
  10. /**
  11. * 400 - Bad Request
  12. */
  13. @ResponseStatus(HttpStatus.BAD_REQUEST)
  14. @ExceptionHandler(HttpMessageNotReadableException.class)
  15. public ResultBean handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
  16. ResultBean resultBean = new ResultBean();
  17. resultBean.setCode(400);
  18. resultBean.setMsg("Could not read json...");
  19. log.error("Could not read json...", e);
  20. return resultBean;
  21. }
  22. /**
  23. * 400 - Bad Request
  24. */
  25. @ResponseStatus(HttpStatus.BAD_REQUEST)
  26. @ExceptionHandler({MethodArgumentNotValidException.class})
  27. public ResultBean handleValidationException(MethodArgumentNotValidException e) {
  28. ResultBean resultBean = new ResultBean();
  29. resultBean.setCode(400);
  30. resultBean.setMsg("参数检验异常!");
  31. log.error("参数检验异常!", e);
  32. return resultBean;
  33. }
  34. /**
  35. * 405 - Method Not Allowed。HttpRequestMethodNotSupportedException
  36. * 是ServletException的子类,需要Servlet API支持
  37. */
  38. @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
  39. @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
  40. public ResultBean handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
  41. ResultBean resultBean = new ResultBean();
  42. resultBean.setCode(405);
  43. resultBean.setMsg("请求方法不支持!");
  44. log.error("请求方法不支持!", e);
  45. return resultBean;
  46. }
  47. /**
  48. * 415 - Unsupported Media Type。HttpMediaTypeNotSupportedException
  49. * 是ServletException的子类,需要Servlet API支持
  50. */
  51. @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
  52. @ExceptionHandler({HttpMediaTypeNotSupportedException.class})
  53. public ResultBean handleHttpMediaTypeNotSupportedException(Exception e) {
  54. ResultBean resultBean = new ResultBean();
  55. resultBean.setCode(415);
  56. resultBean.setMsg("内容类型不支持!");
  57. log.error("内容类型不支持!", e);
  58. return resultBean;
  59. }
  60. /**
  61. * 401 - Internal Server Error
  62. */
  63. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  64. @ExceptionHandler(TokenException.class)
  65. public ResultBean handleTokenException(Exception e) {
  66. ResultBean resultBean = new ResultBean();
  67. resultBean.setCode(401);
  68. resultBean.setMsg("Token已失效");
  69. log.error("Token已失效", e);
  70. return resultBean;
  71. }
  72. /**
  73. * 500 - Internal Server Error
  74. */
  75. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  76. @ExceptionHandler(Exception.class)
  77. public ResultBean handleException(Exception e) {
  78. ResultBean resultBean = new ResultBean();
  79. resultBean.setCode(500);
  80. resultBean.setMsg("内部服务器错误!");
  81. log.error("内部服务器错误!", e);
  82. return resultBean;
  83. }
  84. /**
  85. * 400 - Bad Request
  86. */
  87. @ResponseStatus(HttpStatus.BAD_REQUEST)
  88. @ExceptionHandler(ValidationException.class)
  89. public ResultBean handleValidationException(ValidationException e) {
  90. ResultBean resultBean = new ResultBean();
  91. resultBean.setCode(400);
  92. resultBean.setMsg("参数验证失败!");
  93. log.error("参数验证失败!", e);
  94. return resultBean;
  95. }
  96. }

为了能让@ControllerAdvice注解生效,还需要在spring MVC的配置文件:spring-mvc.xml添加如下一句:

  1. <context:component-scan base-package="com.artisan.*" use-default-filters="false">
  2. <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
  3. <!-- 控制器增强,使一个Contoller成为全局的异常处理类,类中用@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常 -->
  4. <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
  5. </context:component-scan>

这样就完成了全局的异常处理,一旦后台出现异常,就返回给前台指定的异常的JSON数据。前台开发人员看到此异常后,就应该立即反馈给后台开发人员。

后台参数验证

前台在请求之前也会进行参数验证,但是为了程序更加严谨,后台也需要进行参数验证,这样做的好处就是,可以防止脏数据的出现,过滤掉一些不符合要求的请求。打个比方吧,就比如新增一个用户,username不能为null,password长度大于6,假如说前端没有做判断,这个时候用户点击保存,后台没做参数验证,就将这个脏数据保存进数据库。

这里我们将采用Hibernate Validator框架去实现后台的参数校验。别看到这里有hibernate这个单词,其实跟hibernate这个orm框架一毛钱关系都没有,他们之间是没有任何的依赖关系的。在pom.xml中添加如下依赖:

  1. <!--Hibernate Validator-->
  2. <dependency>
  3. <groupId>org.hibernate</groupId>
  4. <artifactId>hibernate-validator</artifactId>
  5. <version>6.0.4.Final</version>
  6. </dependency>

在spring的配置文件applicationConext.xml中装配参数验证器:

  1. <!--Hibernate Validator-->
  2. <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

在对应的controller的请求方法中,对需要验证的请求参数用@Valid进行标注,表示这个实体类的有些属性是需要进行参数验证的。

  1. @ApiOperation(value = "新增User")
  2. @ResponseBody
  3. @RequestMapping(value = "/", method = RequestMethod.POST)
  4. public ResultBean add(@ApiParam(value = "新增User实体", required = true) @RequestBody @Valid User user, BindingResult result) {
  5. ResultBean resultBean = new ResultBean();
  6. StringBuilder errorMsg = new StringBuilder("");
  7. if (result.hasErrors()) {
  8. List<ObjectError> list = result.getAllErrors();
  9. for (ObjectError error : list) {
  10. errorMsg = errorMsg.append(error.getCode()).append("-").append(error.getDefaultMessage()).append(";");
  11. }
  12. }
  13. try {
  14. userService.insert(user);
  15. } catch (Exception e) {
  16. resultBean.setCode(StatusCode.HTTP_FAILURE);
  17. resultBean.setMsg(errorMsg.toString());
  18. LOGGER.error("新增User失败!参数信息:User = " + user.toString(), e);
  19. }
  20. return resultBean;
  21. }

对应的User.java实体类中,需要使用@NotEmpty@Length@Max@Min等这些注解去校验参数:

  1. public class User {
  2. /**
  3. * 主键
  4. */
  5. @Id
  6. @Column(name = "u_id")
  7. @GeneratedValue(strategy = GenerationType.IDENTITY)
  8. private Integer uId;
  9. /**
  10. * 用户名
  11. */
  12. @Column(name = "user_name")
  13. @NotEmpty(message = "姓名不能为空")
  14. private String userName;
  15. /**
  16. * 密码
  17. */
  18. @NotEmpty(message = "密码不能为空")
  19. @Length(min = 6, message = "密码长度不能小于 6 位")
  20. private String password;
  21. /**
  22. * 生日
  23. */
  24. private Date birthday;
  25. /**
  26. * 性别
  27. */
  28. private Integer sex;
  29. /**
  30. * 年龄
  31. */
  32. @Max(value = 100, message = "年龄不能大于 100 岁")
  33. @Min(value = 18, message = "必须年满 18 岁!")
  34. private Integer age;
  35. }

若参数没有通过校验,将返回如下的提示信息:

  1. {
  2. "data": null,
  3. "msg": "NotEmpty-姓名不能为空;Min-必须年满 18 岁!;Length-密码长度不能小于 6 位;",
  4. "code": 500,
  5. "etxra": null
  6. }

那当然是不止上面所说的检验注解,Hibernate Validator框架给我们提供了丰富的校验注解,常用的如下:

  • Bean Validation 中内置的 constraint:

    • @Null:被注释的元素必须为 null
    • @NotNull:被注释的元素必须不为 null
    • @AssertTrue:被注释的元素必须为 true
    • @AssertFalse:被注释的元素必须为 false
    • @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    • @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    • @DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    • @DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    • @Size(max, min):被注释的元素的大小必须在指定的范围内
    • @Digits (integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
    • @Past:被注释的元素必须是一个过去的日期
    • @Future:被注释的元素必须是一个将来的日期
    • @Pattern(value):被注释的元素必须符合指定的正则表达式
  • Hibernate Validator 附加的 constraint:

    • @Email:被注释的元素必须是电子邮箱地址
    • @Length:被注释的字符串的大小必须在指定的范围内
    • @NotEmpty:被注释的字符串的必须非空
    • @Range:被注释的元素必须在合适的范围内

这样我们的项目就集成了Bean Validation特性,就可以使用这些注解要进行参数校验了。

跨域请求处理

前端是纯静态的页面,通过ajax请求后台,但是我们知道,ajax存在一个问题就是不支持跨域访问的。也就是说,前后端两个应用必须在同一个域名下才能访问。那该怎么样才能解决这个问题呢?这里采用的是CORS(Cross Origin Resource Sharing)方案,翻译过来就是:跨域资源共享。CORS技术很简单,现在大多数的浏览器都已经支持了,只需后台将CORS相应头写入response对象中即可。

那后台就需要编写一个过滤器:CorsFilter.java,拦截所有的http请求,然后将CORS响应头写入到response对象中即可,代码如下:

  1. /**
  2. * 处理跨域的过滤器
  3. * @author Leeyom Wang
  4. * @date 2017年10月19日 14:47
  5. */
  6. @Component
  7. public class CorsFilter implements Filter {
  8. private static final Logger LOGGER = Logger.getLogger(CorsFilter.class);
  9. private String allowOrigin;
  10. private String allowMethods;
  11. private String allowCredentials;
  12. private String allowHeaders;
  13. private String exposeHeaders;
  14. @Override
  15. public void init(FilterConfig filterConfig) throws ServletException {
  16. allowOrigin = filterConfig.getInitParameter("allowOrigin");
  17. allowMethods = filterConfig.getInitParameter("allowMethods");
  18. allowCredentials = filterConfig.getInitParameter("allowCredentials");
  19. allowHeaders = filterConfig.getInitParameter("allowHeaders");
  20. exposeHeaders = filterConfig.getInitParameter("exposeHeaders");
  21. }
  22. /**
  23. * 通过CORS技术实现AJAX跨域访问, 只要将CORS响应头写入response对象中即可
  24. * @param req
  25. * @param res
  26. * @param chain
  27. * @throws IOException
  28. * @throws ServletException
  29. */
  30. @Override
  31. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
  32. HttpServletResponse response = (HttpServletResponse) res;
  33. if (StringUtil.isNotEmpty(allowOrigin)) {
  34. //允许访问的客户端域名,例如:http://web.xxx.com,若为*,则表示从任意域都能访问,即不做任何限制;
  35. response.setHeader("Access-Control-Allow-Origin", allowOrigin);
  36. }
  37. if (StringUtil.isNotEmpty(allowMethods)) {
  38. //允许访问的请求方式,多个用逗号分割,例如:GET,POST,PUT,DELETE,OPTIONS;
  39. response.setHeader("Access-Control-Allow-Methods", allowMethods);
  40. }
  41. if (StringUtil.isNotEmpty(allowCredentials)) {
  42. //是否允许请求带有验证信息,若要获取客户端域下的cookie时,需要将其设置为true;
  43. response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
  44. }
  45. if (StringUtil.isNotEmpty(allowHeaders)) {
  46. //允许服务端访问的客户端请求头,多个请求头用逗号分割,例如:Content-Type,Access-Token,timestamp
  47. response.setHeader("Access-Control-Allow-Headers", allowHeaders);
  48. }
  49. if (StringUtil.isNotEmpty(exposeHeaders)) {
  50. //允许客户端访问的服务端响应头,多个响应头用逗号分割。
  51. response.setHeader("Access-Control-Expose-Headers", exposeHeaders);
  52. }
  53. chain.doFilter(req, res);
  54. }
  55. @Override
  56. public void destroy() {
  57. }
  58. }

web.xml中配置CorsFilter过滤器:

  1. <!-- 通过CORS技术实现AJAX跨域访问 -->
  2. <filter>
  3. <filter-name>corsFilter</filter-name>
  4. <filter-class>com.artisan.common.filter.CorsFilter</filter-class>
  5. <init-param>
  6. <param-name>allowOrigin</param-name>
  7. <param-value>*</param-value>
  8. </init-param>
  9. <init-param>
  10. <param-name>allowMethods</param-name>
  11. <param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
  12. </init-param>
  13. <init-param>
  14. <param-name>allowCredentials</param-name>
  15. <param-value>true</param-value>
  16. </init-param>
  17. <init-param>
  18. <param-name>allowHeaders</param-name>
  19. <param-value>Content-Type,Access-Token</param-value>
  20. </init-param>
  21. </filter>

这样我们就解决了跨域的问题。

请求鉴权机制

由于http请求是无状态的,我们后端写好了API接口,然后发布出去,如果不做安全控制,谁都可以调用,这很明显是非常不安全的,所以我们需要采用JWT(Json web token)鉴权机制去保护我们的API接口安全,整个的思路如下:

  1. 用户登陆后,服务器端使用 jjwt(当然也可以采用其他的方式,比如时间戳,签名url) 生成 Token ,保存在 Redis 中,以用户名作为 Key,同时将此token值返回给前端。
  2. 通过设置 Redis 键的 TTL 来实现 Token 自动过期。
  3. 前端将token值存到localStorage中,后面每次请求,都将次token放到header(请求头)中。
  4. 服务端通过在 Filter 中拦截请求判断 Token 是否有效,如果有效,则请求通过,无效,返回401,提示此token已经失效。
  5. 由于 Redis 是基于 Key-Value 进行存储,因此可以实现新的 Token 将覆盖旧的 Token ,保证一个用户在一个时间段只有一个可用 Token,但是如果有些系统允许当前用户可以多处登陆,则不需要处理这一步。
  6. 从头至尾,整个过程没有涉及cookie,所以CSRF或者XXS等相关的攻击 是不可能发生的。

首先定义一个管理token的接口,TokenManager.java

  1. /**
  2. * 对Token进行操作的接口
  3. * @author leeyom
  4. * @date 2017年10月19日 10:41
  5. */
  6. public interface TokenManager {
  7. /**
  8. * 创建一个token关联上指定用户
  9. * @param userId 指定用户的id
  10. * @return 生成的token
  11. */
  12. TokenModel createToken(long userId);
  13. /**
  14. * 检查token是否有效
  15. * @param model token
  16. * @return 是否有效
  17. */
  18. boolean checkToken(TokenModel model);
  19. /**
  20. * 从字符串中解析token
  21. * @param authentication 加密后的字符串
  22. * @return
  23. */
  24. TokenModel getToken(String authentication);
  25. /**
  26. * 清除token
  27. * @param userId 登录用户的id
  28. */
  29. void deleteToken(long userId);
  30. /**
  31. * 保证一个用户在一个时间段只有一个可用 Token
  32. * @param userId
  33. * @return
  34. */
  35. boolean hasToken(long userId);
  36. }

其对应的接口实现类RedisTokenManager.java,对token进行增删改查操作:

  1. @Component
  2. public class RedisTokenManager implements TokenManager {
  3. private RedisTemplate<Long, String> redis;
  4. private final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmss");
  5. @Autowired
  6. public void setRedis(RedisTemplate<Long, String> redis) {
  7. this.redis = redis;
  8. //泛型设置成Long后必须更改对应的序列化方案
  9. redis.setKeySerializer(new JdkSerializationRedisSerializer());
  10. }
  11. @Override
  12. public TokenModel createToken(long userId) {
  13. //uuid
  14. String uuid = UUID.randomUUID().toString().replace("-", "");
  15. //时间戳
  16. String timestamp = SDF.format(new Date());
  17. //token => userId_timestamp_uuid;
  18. String token = userId + "_" + timestamp + "_" + uuid;
  19. TokenModel model = new TokenModel(userId, uuid, timestamp);
  20. //存储到redis并设置过期时间(有效期为2个小时)
  21. redis.boundValueOps(userId).set(Base64Util.encodeData(token), Constants.TOKEN_EXPIRES_HOUR, TimeUnit.HOURS);
  22. return model;
  23. }
  24. @Override
  25. public TokenModel getToken(String authentication) {
  26. if (authentication == null || authentication.length() == 0) {
  27. return null;
  28. }
  29. String[] param = authentication.split("_");
  30. if (param.length != 3) {
  31. return null;
  32. }
  33. //使用userId和源token简单拼接成的token,可以增加加密措施
  34. long userId = Long.parseLong(param[0]);
  35. String timestamp = param[1];
  36. String uuid = param[2];
  37. return new TokenModel(userId, uuid, timestamp);
  38. }
  39. @Override
  40. public boolean checkToken(TokenModel model) {
  41. if (model == null) {
  42. return false;
  43. }
  44. String token = redis.boundValueOps(model.getUserId()).get();
  45. if (token == null || !(Base64Util.decodeData(token)).equals(model.getToken())) {
  46. return false;
  47. }
  48. //如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间(2个小时)
  49. redis.boundValueOps(model.getUserId()).expire(Constants.TOKEN_EXPIRES_HOUR, TimeUnit.HOURS);
  50. return true;
  51. }
  52. @Override
  53. public void deleteToken(long userId) {
  54. if (redis.hasKey(userId)) {
  55. redis.delete(userId);
  56. }
  57. }
  58. @Override
  59. public boolean hasToken(long userId) {
  60. String token = redis.boundValueOps(userId).get();
  61. return StringUtils.notNull(token);
  62. }
  63. }

利用spring的APO技术,编写一个切面类SecurityAspect.java,拦截所有Controller类的方法,并从请求头中获取token,最后对token有效性进行判断,代码如下:

  1. @Component
  2. @Aspect
  3. public class SecurityAspect {
  4. private static final Logger LOGGER = Logger.getLogger(SecurityAspect.class);
  5. @Autowired
  6. TokenManager tokenManager;
  7. @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
  8. public Object execute(ProceedingJoinPoint pjp) throws Throwable {
  9. SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
  10. // 从切点上获取目标方法
  11. MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
  12. Method method = methodSignature.getMethod();
  13. // ====放行swagger相关的请求url,开发阶段打开,生产环境注释掉
  14. HttpServletRequest request = WebContextUtil.getRequest();
  15. URL requestUrl = new URL(request.getRequestURL().toString());
  16. if (requestUrl.getPath().contains("configuration")) {
  17. return pjp.proceed();
  18. }
  19. if (requestUrl.getPath().contains("swagger")) {
  20. return pjp.proceed();
  21. }
  22. if (requestUrl.getPath().contains("api")) {
  23. return pjp.proceed();
  24. }
  25. // ====
  26. // 若目标方法忽略了安全性检查,则直接调用目标方法
  27. if (method.isAnnotationPresent(IgnoreSecurity.class)) {
  28. return pjp.proceed();
  29. }
  30. // 从 request header 中获取当前 token
  31. String authentication = request.getHeader(Constants.DEFAULT_TOKEN_NAME);
  32. TokenModel tokenModel = tokenManager.getToken(Base64Util.decodeData(authentication));
  33. // 检查 token 有效性(检查是否登录)
  34. if (!tokenManager.checkToken(tokenModel)) {
  35. String message = "token " + Base64Util.decodeData(authentication) + " is invalid!!!";
  36. LOGGER.debug("message : " + message);
  37. throw new TokenException(message);
  38. }
  39. // 调用目标方法
  40. return pjp.proceed();
  41. }
  42. }

若要使SecurityAspect生效,则需要在SpringMVC配置文件中添加如下Spring 配置:

  1. <!-- 支持Controller的AOP代理 -->
  2. <aop:aspectj-autoproxy />

最后还需要在web.xml中添加Access-Token。

  1. <init-param>
  2. <param-name>allowHeaders</param-name>
  3. <param-value>Content-Type,Access-Token</param-value>
  4. </init-param>

ok,这样我们的后端的API接口就有安全保障,这个只是鉴权,如果涉及到权限管理的话,还需要进行授权操作,这个以后有时间,再整理下,这里就不阐述了。

总结

以上便是我自己在实际开发中对于前后端分离的一些思考,可能有些地方考虑的不够周全,但是也算是一个基础的RESTful API接口平台了,文章相关的示例代码我已经整理成了一个基本的项目,托管在github,大家可以自由下载,github地址:https://github.com/wangleeyom/code-artisan,如果对你有帮助的话,就点个star,有疑惑的地方,就在文章下面评论吧,大家一起讨论。

发表评论

表情:
评论列表 (有 0 条评论,112人围观)

还没有评论,来说两句吧...

相关阅读

    相关 关于前后分离思考总结

    对目前的web来说,前后端分离已经变得越来越流行了,越来越多的企业/网站都开始往这个方向靠拢。那么,为什么要选择前后端分离呢?前后端分离对实际开发有什么好处呢?我之前一直对前后

    相关 前后分离总结

    前言 对于刚入行时的小白误以为前后端分离就是,前端代码不使用java了。但是其实并不是这样的,前后端分离就是前段写前端的代码,后端完成后端的代码互不干扰。前端编写的时候,