• WebService安全机制的思考与实践


    转发原文:

    近来因业务需要,需要研究webservice,于是便有这篇文章:
    SpringBoot整合Apache-CXF实践

    一、WebService是什么?

    WebService是一个平台独立的、低耦合的、自包含的、基于可编程的web的应用程序,可使用开放的XML(标准通用标记语言下的一个子集)标准来描述、发布、发现、协调和配置这些应用程序,用于开发分布式的交互操作的应用程序。

    简单概括如下:
    WebService是一种跨平台,跨语言的规范,用于不同平台,不同语言开发的应用之间的交互

    二、Webservice安全机制有哪些?

    由于我之前从未实际接触过WebService,对于它的安全机制不了解。于是通过搜索,我得到了关于它的安全机制一些建议:

    • (1)对webservice发布的方法,方法名称和参数不要使用望文生义的描述;
    • (2)对webservice发布的方法,在入参中增加一个或多个字符串序列(这里的字符串可以要求必须满足指定的格式,同时字符串可以再通过客户端传参数的时候加密,服务端解密);
    • (3)对webservice发布的方法,入参中加上用户名和密码,然后服务端通过数据库校验;
    • (4)对webservice发布的方法,通过handler/chain方式来实现验证(用户名&密码校验/IP地址校验等);
    • (5)对webservice发布的方法,采用webservice的users.lst来进行验证;
    • (6)对webservice发布的服务,通过servlet的Filter来实现验证;
    • (7)对webservice传输过程中的数据进行加密;
    • (8)自己写校验框架来实现webservice的安全;
    • (9)其它方式.

    上述是搜索方面出现毕竟频繁的,也是webservice比较普遍的方式之一。

    我思虑再三决定结合以往开发HTTP应用安全经验和现有参考WebService安全机制结合起来。

    于是便有了如下的安全机制方案:

    • Token鉴权机制;
    • 公私钥签名校验;
    • IP白名单校验.

    三、如何实现Token鉴权、公私钥签名校验、IP白名单校验等WebService安全方案呢?

    本次代码已同步到我的Apache CXF代码例子里了,Github地址为:
    https://github.com/developers-youcong/blog-cxf

    核心代码,关键在于拦截器

    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    
    package com.blog.cxf.server.interceptor;
    
    import cn.hutool.core.util.StrUtil;
    import com.blog.cxf.server.security.SecretKey;
    import com.blog.cxf.server.utils.IpUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.cxf.binding.soap.SoapMessage;
    import org.apache.cxf.headers.Header;
    import org.apache.cxf.interceptor.Fault;
    import org.apache.cxf.message.Message;
    import org.apache.cxf.phase.AbstractPhaseInterceptor;
    import org.apache.cxf.phase.Phase;
    import org.apache.cxf.phase.PhaseInterceptorChain;
    import org.apache.cxf.transport.http.AbstractHTTPDestination;
    import org.springframework.stereotype.Component;
    import org.w3c.dom.Element;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Properties;
    import java.util.Set;
    
    /**
     * @description: 认证鉴权拦截器
     * @author: youcong
     * @time: 2020/10/31 17:07
     */
    @Slf4j
    @Component
    public class AuthInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
    
    
        public AuthInterceptor() {
            super(Phase.PRE_INVOKE);
        }
    
    
        public void handleMessage(SoapMessage msg) throws Fault {
    
    
            Message ipVerify = PhaseInterceptorChain.getCurrentMessage();
    
            HttpServletRequest request = (HttpServletRequest) ipVerify.get(AbstractHTTPDestination.HTTP_REQUEST);
    
            //处理IP
            handleIp(request);
    
            Header authHeader = null;
            //获取验证头
            List<Header> headers = msg.getHeaders();
    
            if (headers.isEmpty()) {
                throw new Fault(new Exception("请求头为空"));
            }
    
    
            for (Header h : headers) {
    
                log.info("h:" + h.getName().toString().contains("auth"));
                if (h.getName().toString().contains("auth")) {
                    authHeader = h;
                    break;
                } else {
                    throw new Fault(new Exception("请求头需包含auth"));
                }
    
            }
    
            Element auth = (Element) authHeader.getObject();
    
            NodeList childNodes = auth.getChildNodes();
    
            Set<String> reqHeader = new HashSet<String>();
            for (int i = 0; i < childNodes.getLength(); i++) {
                //处理节点
                handleNode(childNodes.item(i), reqHeader);
            }
            //处理请求Key
            handleSOAPReqHeader(reqHeader);
    
    
        }
    
        //处理IP
        private void handleIp(HttpServletRequest request) {
    
    
            String[] ip_arr = new String[]{"127.0.0.1", "192.168.52.50"};
    
            for (String str : ip_arr) {
                System.out.println("ip:" + str);
            }
    
            Set<String> ipSet = new HashSet<String>();
    
            for (String item : ip_arr) {
    
                ipSet.add(item);
                if (ipSet.contains(IpUtils.getIpAddr(request))) {
                    log.info("合法IP:" + item);
                } else {
                    throw new Fault(new Exception("非法IP"));
                }
            }
    
    
        }
    
        //处理节点
        private void handleNode(Node items, Set<String> reqHeader) {
    
            Node item = items;
    
            //存储请求头Key
            if (item.getLocalName() != null) {
                String str = new String(item.getLocalName());
                reqHeader.add(str);
            }
    
            //获取请求头token
            if (item.getNodeName().contains("token")) {
                String tokenValue = item.getTextContent();
    
                if (!StrUtil.isEmpty(tokenValue)) {
    
                    if ("soap".equals(tokenValue)) {
    
                        log.info("token Value:" + tokenValue);
                    } else {
                        throw new Fault(new Exception("token错误"));
                    }
    
                } else {
                    throw new Fault(new Exception("token不能为空"));
                }
    
            }
    
            //获取请求头sign
            if (item.getNodeName().contains("sign")) {
    
                String signValue = item.getTextContent();
    
                if (!StrUtil.isEmpty(signValue)) {
    
                    //原数据
                    String originData = "test_webservice_api_2020";
    
                    try {
    
                        //比对签名
                        boolean verifySign = SecretKey.verifySign(originData, signValue);
    
                        log.info("verifySign:" + verifySign);
    
                        if (verifySign) {
                            log.info("sign Value:" + signValue);
                        } else {
                            throw new Fault(new Exception("签名错误"));
                        }
                    } catch (Exception e) {
                        throw new Fault(new Exception("签名错误"));
                    }
    
    
                } else {
                    throw new Fault(new Exception("签名不能为空"));
                }
            }
        }
    
        //处理SOAP请求头Key
        private void handleSOAPReqHeader(Set<String> reqHeader) {
    
            if (reqHeader.contains("token")) {
                log.info("包含token");
            } else {
                throw new Fault(new Exception("请求头auth需包含token"));
            }
    
            if (reqHeader.contains("sign")) {
                log.info("包含sign");
            } else {
                throw new Fault(new Exception("请求头auth需包含sign"));
            }
    
        }
    
    
    }
    

    1.Token鉴权的目的是什么?

    每个用户生成的token不一样,获取token的接口是需要对应的用户名和密码,通过用户名和密码产生token,token放在请求头里,后台可根据token识别是哪个用户请求哪个接口,后面日志存储会提到的。

    2.Token的生成有哪些方案?

    可以参考我写的这篇文章:SpringCloud之Security
    这篇文章我结合了JWT。

    除此之外还可以结合某种规则(用户名+密码+特殊UUID+用户注册码)生成加密的token。

    3.签名的目的是什么?

    为了数据安全和防止重复提交。

    4.如何实现签名?

    签名的规则有很多,可以增加某种证书公私钥,也可以时间戳。

    5.为什么需要IP白名单校验?

    主要是为了安全,防止非法IP不停的请求,造成恶意攻击(如DOS攻击和DDOS攻击等)。

    6.IP白名单校验有哪些方案?

    可以将IP白名单放在对应的数据表中,也可以将其放到配置文件里,还可以将其存一个数组中(就像我在上述代码所写的那样)。

    7.开始测试

    (1)非法IP请求(不在数组内的IP)

    图一

    (2)携带错误的Token请求

    图二

    (3)携带错误的签名请求

    图三

    (4)正确请求(token正确、签名正确、IP合法)

    图三

    8.证书生成方案(公私钥)

    这一块我主要参考了这篇文章,这篇文章很完整,大家可以参考一下:
    Java 证书(keytool实例)代码实现加解密、加签、验签

    生成证书核心两条命令,如下(注意,其中的密码之类的,改成自己的):

    1
    2
    3
    4
    5
    
    ## 生成私钥
    keytool -genkey -alias yunbo2 -keypass 123456 -keyalg RSA -keysize 1024 -validity 3650 -keystore merKey.jks -storepass abc@2018 -dname "CN=localhost,OU=localhost, O=localhost, L=深圳, ST=广东, C=CN"
    
    ## 生成公钥
    keytool -export -alias yunbo2 -keystore merKey.jks -file yunbo2.cer
    

    9.数据加密

    数据加密主要体现在对请求体内的数据进行base64加密或者是其他的加密方式。

    10.补充说明

    之前搜索了不少文章提到过,请求头或者请求体传输用户名和密码,我个人觉得用户名和密码传输太过频繁并不安全,因此我选择了token,选择了多一步(通过用户名和密码拿到token,再通过token请求对其它业务webservice等)。

    四、总结

    技术往往有很多相似之处,可以复用和借鉴。之前在研究Apache CXF安全机制的时候,发现并没有那么多的资料可供参考,于是我换了一个思路,Apache CXF框架本质上就是对WebService简化,方便开发人员使用而不用配置一堆东西。我把核心聚焦在webservice安全,然后在发散,就有了这篇文章。
    简单的概括一点:
    遇到难题不要钻牛角尖,可以尝试换一个思路(发散自己的思维)来解决这个难题。

  • 相关阅读:
    iOS 关于NSNotificationCenter
    IOS UITableView reload 刷新某一个cell 或 section
    IOS AFNetWorking 设置超时时间
    IOS AFNetWorking
    IOS 十六进制字符串转换成UIColor
    IOS 长姿势---双击Home键
    IOS 关于 NSUserDefault
    方正璞华培训讲师
    localStorage使用总结
    Promise的简单用法
  • 原文地址:https://www.cnblogs.com/weigy/p/16104402.html
Copyright © 2020-2023  润新知