SPRING CLOUD ZUUL通过自定义对称加密的请求头 防止请求伪造

引言

SPRING CLOUD开发中,出于安全性考虑,到达EUREKA CLIENT的请求我们需要对其是否是通过网关做必要的验证。这样既可以保证请求在网关接受了前置性处理,也可以保证服务不会在集群外部被直接访问。


设计思路大概如下:

  • 在网关拦截器中,对REQUEST塞入两个头。
        requestContext.addZuulRequestHeader("se-token", token);
        requestContext.addZuulRequestHeader("ci-text", ciphertext);

se-token:随机生成的一个UUID。
ci-text:对se-token进行AES加密后的密文。
AES加密的公钥,分别放置在各服务与ZUUL的项目中。

  • 当请求经过ZUUL到达服务节点时,首先判断是否存在自定义的请求头。
  • 定义一个有界队列来储存已经请求过的token,首先判断如果某个token在队列中,说明是伪造的请求,然后塞入队列的最头部。其实这样没办法100%避免请求伪造,但起码可以保证队列长度个请求是安全的。
  • 如果不在队列中,用秘钥解码密文,如果结果和token一致则验证通过,执行目标方法。
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedList;

public class SecurityTokenUtil {

    static LimitQueue queue = new LimitQueue<String>(2000);
    static Object lock = new Object();
    static final String SECURITY_KEY = "ThissTestkey18,.";

    /**
     * 通过两个header判断请求是否合法
     * se-token : 随机生成的uuid
     * ci-text : 密文
     * @param request
     * @return
     */
    public static boolean checkRequestLegality(HttpServletRequest request) {
        String token = request.getHeader("se-token");
        String ciphertext = request.getHeader("ci-text");
        if (token == null || token.equals("") || ciphertext == null || ciphertext.equals("")) return false;
        return parseSecurityHeader(token,ciphertext,SECURITY_KEY);
    }

    public static boolean parseSecurityHeader(String token, String ciphertext, String key) {
        //先查找这个token是不是已经在前面的请求被使用过了 如果已经使用过了 说明是伪造 返回false
        synchronized (lock) {
            if (queue.isExists(token)) {
                //如果是伪造的 把这个token放入队列的最头部 加速定位
                queue.remove(token);
                queue.offer(token);
                return false;
            }
            //如果token未使用 进行解码 看是否合法
            try {
                String decodedText = SymmetricEncoder.AESDncode(key, ciphertext);
                //解码后数据与token不一致 返回false
                if (decodedText != null && !decodedText.equals(token)) return false;
            } catch (Exception e) {
                return false;
            }
            //请求合法
            queue.offer(token);
            return true;
        }
    }

    public static String tokenEncode(String token){
        return SymmetricEncoder.AESEncode(SECURITY_KEY,token);
    }

    /**
     * 有界队列 头插尾出
     * @param <E>
     */
    private static class LimitQueue<E> {
        private int limit;
        private LinkedList<E> queue = new LinkedList<E>();

        public LimitQueue(int limit) {
            this.limit = limit;
        }

        public void offer(E e) {
            if (queue.size() >= limit) {
                queue.pollLast();
            }
            queue.offerFirst(e);
        }

        public boolean remove(E e) {
            return queue.remove(e);
        }

        public E get(int position) {
            return queue.get(position);
        }

        public E getLast() {
            return queue.getLast();
        }

        public E getFirst() {
            return queue.getFirst();
        }

        public int getLimit() {
            return limit;
        }

        public int size() {
            return queue.size();
        }

        public boolean isExists(E e) {
            for (Iterator<E> iterator = queue.iterator(); iterator.hasNext(); ) {
                E tmp = iterator.next();
                if (tmp.equals(e)) {
                    return true;
                }
            }
            return false;
        }
    }


    public static class SymmetricEncoder {
        /*
         * 加密
         * key 秘钥 content加密内容
         * AES固定秘钥格式为128/192/256 bits.即:16/24/32bytes
         */
        public static String AESEncode(String key, String content) {
            try {
                //1.两个参数,第一个为私钥字节数组, 第二个为加密方式
                Key keySpec = new SecretKeySpec(key.getBytes(), "AES");
                //2.根据指定算法AES自成密码器
                Cipher cipher = Cipher.getInstance("AES");
                //3.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作,第二个参数为使用的KEY
                cipher.init(Cipher.ENCRYPT_MODE, keySpec);
                //4.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
                byte[] byte_encode = content.getBytes("utf-8");
                //5.根据密码器的初始化方式--加密:将数据加密
                byte[] byte_AES = cipher.doFinal(byte_encode);
                //6.将加密后的数据转换为字符串
                //这里用Base64Encoder中会找不到包
                //解决办法:
                //在项目的Build path中先移除JRE System Library,再添加库JRE System Library,重新编译后就一切正常了。
                String AES_encode = new String(new BASE64Encoder().encode(byte_AES));
                //7.将字符串返回
                return AES_encode;
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            //如果有错就返加nulll
            return null;
        }

        /*
         * 解密
         * key 秘钥 content解密内容
         * AES固定秘钥格式为128/192/256 bits.即:16/24/32bytes
         */
        public static String AESDncode(String key, String content) {
            try {
                //1.构造密钥生成器,指定为AES算法,不区分大小写
                Key keySpec = new SecretKeySpec(key.getBytes(), "AES");
                //2.根据指定算法AES自成密码器
                Cipher cipher = Cipher.getInstance("AES");
                //3.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
                cipher.init(Cipher.DECRYPT_MODE, keySpec);
                //4.将加密并编码后的内容解码成字节数组
                byte[] byte_content = new BASE64Decoder().decodeBuffer(content);
                /*
                 * 解密
                 */
                byte[] byte_decode = cipher.doFinal(byte_content);
                String AES_decode = new String(byte_decode, "utf-8");
                return AES_decode;
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            }
            //如果有错就返加nulll
            return null;
        }
    }
}

仅此记录加深印象,如有帮助,不胜荣幸。

END

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值