spring boot架构设计——权限验证及API接口统一返回格式

Posted Sun_Yang_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring boot架构设计——权限验证及API接口统一返回格式相关的知识,希望对你有一定的参考价值。

昨天奋战了一天才搞定,记录一下。

 

权限验证

权限验证实现需要截取request参数,这个实现很简单,springboot中可以使用interceptor,Aspect,filter实现.具体实现网上一大把,就懒得写了,关键字搜就是。

通过request获取到请求参数后,按照自己定义的规则计算出sign值,例如把token+timestamp+逻辑方法参数字典排序后md5+base64位,然后和客户端传过来的sign值对比。

API接口统一返回格式

这个才是折腾人的玩意,在net下实现的时候觉得挺简单的,在spring boot下做就要了老命了。

统一返回格式的目的是把逻辑方法和系统返回格式解耦合。例如API接口返回格式如下:

 

//权限验证失败返回
{
    "data": null,
    "code": 1,
    "msg": "服务器权限验证失败"
}
//发生未处理异常事件的返回
{
    "data": null,
    "code": -1,
    "msg": "服务器异常,请稍后再试"
}
//请求成功返回
{
    "data": {
        "id": 1,
        "userid": "test1",
        "name": "孙杨",
        "phone": "183*******",
        "sourcetype": 1,
        "loginname": "test1",
        "loginpwd": "test1",
        "powertype": 1,
        "registertime": "2017-10-19"
    },
    "code": 10000,
    "msg": ""
}

这种格式由2部分组成,第一部分就是最外层的data,code,msg。我称之为系统级返回参数,data里面的是方法级返回参数。处理逻辑是捕获逻辑方法返回值,然后转换为标准格式返回给客户端。

spring boot的问题是interceptor不能获取到返回值,restAPI方式的请求,ModelAndView这个参数返回的是null,也没法从response参数里面取出返回值。google百度了一个下午,最后放弃。

然后选择Aspect方式,利用round注解,很轻松的就拿到了返回值,一切看上去很顺利,结果在修改返回值时,报错了。。。因为产生响应时,Aspect执行顺序在servlet前面,即逻辑方法返回model,然后Aspect执行,然后servlet再执行,而servlet默认会根据逻辑方法的返回值来对返回值进行序列化,这时候因为在Aspect中,我已经修改了返回值类型,于是就会出现类型转换错误。Aspect达不到目标,也宣告放弃。

最后看到外网一位网友建议使用filter。filter执行顺序在servlet之后,试了下,ok了,能捕获,能修改。不过还有个问题,因为是在servlet之后执行,而servlet默认是会把逻辑方法的返回值序列化的。。。于是data参数里面装的就是一个字符串,然后filter返回的时候再序列化一次,data里面的数据格式就惨不忍睹了,客户端还不骂死啊。夜深人懒,不想再累了,就直接把获取到的逻辑方法的json字符串反序列化为object,然后装配到data中,然后再把filter的参数序列化。。。最后接口返回的效果就是目前看的样子,效率低,达到最低效果要求,日后有空再优化吧。。。。。。下面贴下代码

过滤器代码:

/**
 * @author 孙杨
 * @date Created in 下午6:01 17/10/19
 */
@WebFilter(filterName = "全局过滤器", urlPatterns = "/*")
public class GlobalFilter implements Filter {

    private final String JsonArraySign = "[";
    private final boolean DEBUG = true;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        ResponseModel response = new ResponseModel();
        LogUtil logger = new LogUtil();
        StringBuilder sb = new StringBuilder();
        Gson gson = new GsonBuilder().serializeNulls().create();

        //权限校验参数
        String token = "";
        String timestamp = "";
        String path = "";
        String sign = "";

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String[] paths = request.getRequestURL().toString().split("/api/sx");
        if (paths.length > 1) {
            path = paths[1];
        }

        long startTime = System.currentTimeMillis();
        sb.append("\n路径: " + request.getRequestURL() + "\n");
        sb.append("method: " + request.getMethod() + "\n");
        sb.append("QueryString:  " + request.getQueryString() + "\n");
        sb.append("请求参数:\n");
        Enumeration<String> paras = request.getParameterNames();
        while (paras.hasMoreElements()) {
            String name = paras.nextElement();
            String value = request.getParameter(name);
            if ("token".equals(name)) {
                token = value;
            }
            if ("timestamp".equals(name)) {
                timestamp = value;
            }
            if ("sign".equals(name)) {
                sign = value;
            }
            sb.append(name + ":" + value + "\n");
        }
        if (!DEBUG && !sign.equals(AuthVerificationUtil.countSign(token, timestamp, path))) {
            sb.append("方法耗时: " + (System.currentTimeMillis() - startTime) + "毫秒\n");
            response.setCode(CodeTable.ACCESSDENIED);
            response.setMsg("服务器权限验证失败");
            response.setData(null);
            sb.append("逻辑错误:" + "服务器权限验证失败: token:" + token +
                    " timestamp:" + timestamp + " path:" + path
                    + " 服务器端sign: " + AuthVerificationUtil.countSign(token, timestamp, path));
            logger.error(sb.toString());
        } else {
            MyResponseWrapper responseWrapper = new MyResponseWrapper((HttpServletResponse) servletResponse);
            try {
                filterChain.doFilter(servletRequest, responseWrapper);
                String responseContent = new String(responseWrapper.getDataStream());
                //判断返回值是jsonObject还是jsonArray
                if (responseContent.startsWith(JsonArraySign)) {
                    response.setData(new JsonParser().parse(responseContent).getAsJsonArray());
                } else {
                    response.setData(new JsonParser().parse(responseContent).getAsJsonObject());
                }
                response.setCode(CodeTable.SUCCESS);
                response.setMsg("");
                sb.append("方法耗时: " + (System.currentTimeMillis() - startTime) + "毫秒\n");
                sb.append("响应参数: " + responseContent);
                logger.info(sb.toString());
            } catch (Exception e) {
                sb.append("方法耗时: " + (System.currentTimeMillis() - startTime) + "毫秒\n");
                if (e instanceof MyException) {
                    response.setCode(((MyException) e).getCode());
                    response.setMsg(((MyException) e).getMsg());
                    response.setData(null);
                    sb.append("逻辑错误:" + ((MyException) e).getLog());
                    logger.error(sb.toString());
                } else {
                    response.setCode(CodeTable.UNKNOWERROR);
                    response.setMsg("服务器异常,请稍后再试");
                    response.setData(null);
                    sb.append("未捕获异常:" + e.getMessage());
                    logger.error(sb.toString());
                }
            }
        }
        ((HttpServletResponse) servletResponse).setHeader("Content-type", "application/json;charset=UTF-8");
        servletResponse.getOutputStream().write(gson.toJson(response).getBytes());
    }

    @Override
    public void destroy() {

    }
}

逻辑方法类似如下: 

    @RequestMapping("login")
    public User login(LoginModel loginModel) {
        User user = userRepository.findByLoginnameAndLoginpwd(loginModel.getLoginname(), loginModel.getLoginpwd());
        if (user == null) {
            throw new MyException(10001, "用户名或密码不对");
        }
        return user;
    }

折腾了这么多东西,目的就是彻底简化逻辑方法代码难度~定义好接口文档和路径,逻辑代码编写就可以丢给实习生了,又可以偷懒了~~

 

还有2个辅助类我也贴一下: 

 

public class MyResponseWrapper extends HttpServletResponseWrapper {
    ByteArrayOutputStream output;
    FilterServletOutputStream filterOutput;

    public MyResponseWrapper(HttpServletResponse response) {
        super(response);
        output = new ByteArrayOutputStream();
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (filterOutput == null) {
            filterOutput = new FilterServletOutputStream(output);
        }
        return filterOutput;
    }

    public byte[] getDataStream() {
        return output.toByteArray();
    }
}
public class FilterServletOutputStream extends ServletOutputStream {
    DataOutputStream output;

    public FilterServletOutputStream(OutputStream output) {
        this.output = new DataOutputStream(output);
    }

    @Override
    public void write(int arg0) throws IOException {
        output.write(arg0);
    }

    @Override
    public void write(byte[] arg0, int arg1, int arg2) throws IOException {
        output.write(arg0, arg1, arg2);
    }

    @Override
    public void write(byte[] arg0) throws IOException {
        output.write(arg0);
    }

    @Override
    public boolean isReady() {
        return false;
    }

    @Override
    public void setWriteListener(WriteListener writeListener) {

    }
}

 

以上是关于spring boot架构设计——权限验证及API接口统一返回格式的主要内容,如果未能解决你的问题,请参考以下文章

企业快速开发平台Spring Cloud+Spring Boot+Mybatis创业必备企业架构,可开发任意项目

企业快速开发平台Spring Cloud+Spring Boot+Mybatis创业必备企业架构,可开发任意项目

企业快速开发平台Spring Cloud+Spring Boot+Mybatis创业必备企业架构,可开发任意项目

企业快速开发平台Spring Cloud+Spring Boot+Mybatis+ElementUI

DT CMS致力于Spring Boot2.3.5 Vue前后端分离的RBAC权限框架(盛世美颜!)

DT CMS致力于Spring Boot2.3.5 Vue前后端分离的RBAC权限框架(盛世美颜!)