SpringSecurity结合filter实现对请求数据解密、返回数据加密

3 篇文章 1 订阅

场景

  1. 现有需求是数据在传输时是明文传输,用抓包工具可对请求数据和响应数据修改后重放,不安全
  2. 现有架构是:前端-angular,后端-springboot+SpringSecurity
  3. 预期实现方式:前端-在angular 的http拦截器中,对请求和返回数据进行处理,后端-通过过滤器对数据进行处理

选择

  1. 后端为什么要选择过滤器进行处理
	因为项目采用了SpringSecurity,SpringSecurity就是通过过滤器实现的,如果采用其他方式,
	无法处理登录时候的数据包
  1. 如何将自定义的过滤器添加在SpringSecurity之前
  • 方式一:自己注册filter,并设置order为小于-100的数,SpringSecurity的filter的顺序是-100
	@Bean
    public FilterRegistrationBean registerLoginCheckFilter() {
        EncryptFilter requireLoginFilter = new EncryptFilter();
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(requireLoginFilter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("EncryptFilter");
        registrationBean.setOrder(-101);
        return registrationBean;
    }
  • 方式二:将自己的filter配置到SpringSecurity中
	@Override
    public void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new EncryptFilter(), UsernamePasswordAuthenticationFilter.class);
  1. 选择哪种加密方式

最终采用的是AES加密

  • MD5 : 不可逆
  • RSA:加密内容长度有限制,过长的数据需要处理分段加密
  • BASE64:不安全,可直接反编码看到明文
  1. springMvc请求中,body数据只能被读取一次

编写wapper类,将body数据保存下来,重写获取的方法,详见 实现

实现

java

过滤器

package net.rjgf.starter.config;

import com.alibaba.fastjson.JSONObject;
import net.rjtx.common.util.AESUtil;
import net.rjtx.common.util.HttpResponseUtil;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class EncryptFilter implements Filter {

    // AES 加密key,16位长度,换成自己的即可
    private String key = "1234567891234567";

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        MyRequestWrapper requestWrapper = new MyRequestWrapper((HttpServletRequest) servletRequest);
        MyResponseWrapper responseWrapper  = new MyResponseWrapper((HttpServletResponse) servletResponse);
        // POST 请求才进行解析
        if (requestWrapper.getMethod().equals("POST")){
            String body = requestWrapper.getBody();
            if (!StringUtils.isEmpty(body)) {
                JSONObject jsonObject = JSONObject.parseObject(body);
                Object data = jsonObject.get("data");
                String s = null;
                try {
                    s = AESUtil.aesDecrypt(data.toString(), key);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                requestWrapper.setBody(s);
            }
        }

        filterChain.doFilter(requestWrapper,responseWrapper);


        // 对返回数据进行加密
        String responseData = responseWrapper.getResponseData("UTF-8");
        JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("data",AESUtil.aesEncrypt(responseData,key));
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 输出
        HttpResponseUtil.output((HttpServletResponse) servletResponse,jsonObject);
    }
}

MyRequestWrapper

用于 request对象 body数据的重复读取

package net.rjgf.starter.config;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Map;

public class MyRequestWrapper extends HttpServletRequestWrapper {
    private  String body;
    public MyRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.body = getBodyString(request);
    }
    public String getBody() {
        return body;
    }
    public void setBody(String body) {
        this.body = body;
    }
    public String getBodyString(final HttpServletRequest request) throws IOException {
        String contentType = request.getContentType();
        String bodyString = "";
        StringBuilder sb = new StringBuilder();
        if (StringUtils.isNotBlank(contentType) && (contentType.contains("multipart/form-data") || contentType.contains("x-www-form-urlencoded"))) {
            Map<String, String[]> parameterMap = request.getParameterMap();
            for (Map.Entry<String, String[]> next : parameterMap.entrySet()) {
                String[] values = next.getValue();
                String value = null;
                if (values != null) {
                    if (values.length == 1) {
                        value = values[0];
                    } else {
                        value = Arrays.toString(values);
                    }
                }
                sb.append(next.getKey()).append("=").append(value).append("&");
            }
            if (sb.length() > 0) {
                bodyString = sb.toString().substring(0, sb.toString().length() - 1);
            }
            return bodyString;
        } else {
            return IOUtils.toString(request.getInputStream());
        }
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
           @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public int read() {
                return bais.read();
            }
            @Override
            public void setReadListener(ReadListener readListener) { }
        };
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

MyResponseWrapper

用于对response返回的body数据可重复读取处理

package net.rjgf.starter.config;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;

public class MyResponseWrapper extends HttpServletResponseWrapper {

    private ByteArrayOutputStream buffer = null;
    private ServletOutputStream out = null;
    private PrintWriter writer = null;
    public MyResponseWrapper(HttpServletResponse response) throws IOException{
        super(response);
        buffer = new ByteArrayOutputStream();
        out = new WapperedOutputStream(buffer);
        writer = new PrintWriter(new OutputStreamWriter(buffer, "UTF-8"));
    }
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return out;
    }
    @Override
    public PrintWriter getWriter() throws IOException {
        return writer;
    }
    @Override
    public void flushBuffer() throws IOException {
        if (out != null) {
            out.flush();
        }
        if (writer != null) {
            writer.flush();
        }
    }
    @Override
    public void reset() {
        buffer.reset();
    }

    public String getResponseData(String charset) throws IOException {
        flushBuffer();
        byte[] bytes = buffer.toByteArray();
        try {
            return new String(bytes, charset);
        } catch (UnsupportedEncodingException e) {
            return "";
        }
    }

    public void setResponseData(String charset) throws IOException {
        buffer = new ByteArrayOutputStream();
        buffer.write(charset.getBytes());
    }

    class WapperedOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos = null;
        public WapperedOutputStream(ByteArrayOutputStream stream) throws IOException {
            bos = stream;
        }
        @Override
        public void write(int b) throws IOException {
            bos.write(b);
        }
        @Override
        public boolean isReady() {
            return false;
        }
        @Override
        public void setWriteListener(WriteListener listener) {
        }
    }
}

AESUtil

AES 加解密 工具类

public class AESUtil {
    /**
     * 将byte[]转为各种进制的字符串
     *
     * @param bytes byte[]
     * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
     * @return 转换后的字符串
     */
    public static String binary(byte[] bytes, int radix) {
        return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
    }

    /**
     * base 64 encode
     *
     * @param bytes 待编码的byte[]
     * @return 编码后的base 64 code
     */
    public static String base64Encode(byte[] bytes) {
        byte[] encode = Base64.getEncoder().encode(bytes);
        return  new String(encode);
    }

    /**
     * base 64 decode
     *
     * @param base64Code 待解码的base 64 code
     * @return 解码后的byte[]
     * @throws Exception
     */
    public static byte[] base64Decode(String base64Code) throws Exception {
        return StringUtils.isEmpty(base64Code) ? null : Base64.getDecoder().decode(base64Code);
    }

    /**
     * 获取byte[]的md5值
     *
     * @param bytes byte[]
     * @return md5
     * @throws Exception
     */
    public static byte[] md5(byte[] bytes) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(bytes);

        return md.digest();
    }

    /**
     * 获取字符串md5值
     *
     * @param msg
     * @return md5
     * @throws Exception
     */
    public static byte[] md5(String msg) throws Exception {
        return StringUtils.isEmpty(msg) ? null : md5(msg.getBytes());
    }

    /**
     * 结合base64实现md5加密
     *
     * @param msg 待加密字符串
     * @return 获取md5后转为base64
     * @throws Exception
     */
    public static String md5Encrypt(String msg) throws Exception {
        return StringUtils.isEmpty(msg) ? null : base64Encode(md5(msg));
    }


    /**
     * AES加密
     *
     * @param content    待加密的内容
     * @param encryptKey 加密密钥
     * @return 加密后的byte[]
     * @throws Exception
     */
    public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {

        byte[] raw = encryptKey.getBytes("utf-8");
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);

        return cipher.doFinal(content.getBytes("utf-8"));
    }

    /**
     * AES加密为base 64 code
     *
     * @param content    待加密的内容
     * @param encryptKey 加密密钥
     * @return 加密后的base 64 code
     * @throws Exception
     */
    public static String aesEncrypt(String content, String encryptKey) throws Exception {
        return base64Encode(aesEncryptToBytes(content, encryptKey));
    }


    /**
     * AES解密
     *
     * @param encryptBytes 待解密的byte[]
     * @param decryptKey   解密密钥
     * @return 解密后的String
     * @throws Exception
     */
    public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {


        byte[] raw = decryptKey.getBytes("utf-8");
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);

        byte[] decryptBytes = cipher.doFinal(encryptBytes);

        return new String(decryptBytes);
    }


    /**
     * 获得密钥
     *
     * @param secretKey
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws InvalidKeySpecException
     */
    protected SecretKey generateKey(String secretKey) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException {

        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("AES");
        DESKeySpec keySpec = new DESKeySpec(secretKey.getBytes());
        keyFactory.generateSecret(keySpec);
        return keyFactory.generateSecret(keySpec);
    }

    /**
     * 将base 64 code AES解密
     *
     * @param encryptStr 待解密的base 64 code
     * @param decryptKey 解密密钥
     * @return 解密后的string
     * @throws Exception
     */
    public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
        return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
    }

    public static void main(String[] args) throws Exception {
        String content = "123456";
        System.out.println("加密前:" + content);

        System.out.println(content.length());
        String key = "1234567891234567";
        System.out.println("加密密钥和解密密钥:" + key);

        String encrypt = aesEncrypt(content, key);
        System.out.println("加密后:" + encrypt);

        String decrypt = aesDecrypt(encrypt, key);
        System.out.println("解密后:" + decrypt);
    }

    
    

}

angular

主要是对http拦截器的改写

  1. 安装 crypto-js
    npm install crypto-js
    npm install --save @types/crypto-js
  2. 使用
    import { AES, mode, pad, enc } from’crypto-js’;
  3. 方法
  • 加密
encryptByEnAES(data: string): string {
    let Key = enc.Utf8.parse("1234567891234567");// 秘钥长度需要是16的倍数,且需要进行Utf8转义。
    let tmpAES = AES.encrypt(data, Key, {
      mode: mode.ECB,
      padding: pad.Pkcs7
    });
    return tmpAES.toString();
  }
  • 解密
encryptByDeAES(data: string): string {
    let Key = enc.Utf8.parse("1234567891234567");// 秘钥长度需要是16的倍数,且需要进行Utf8转义。
    let tmpDeAES = AES.decrypt(data, Key, {
      mode: mode.ECB,
      padding: pad.Pkcs7
    });
    return tmpDeAES.toString(enc.Utf8);
  }
  • 调用思路

对前端的 HttpRequest 与 HttpResponse 对象的body数据进行替换即可

 // 发送数据加密
    let newReq
    if (req.body != null) {
      newReq = req.clone({
        url: url,
        body: {"data": this.encryptByEnAES(JSON.stringify(req.body))}
      });
    }else{
      newReq = req.clone({
        url: url
      });
    }
if (rsp.url.toString().indexOf(environment.SERVER_URL) != -1){
      // 处理解密
      console.log(JSON.parse(this.encryptByDeAES(rsp.body["data"])))
      ev = rsp.clone({
        body: JSON.parse(this.encryptByDeAES(rsp.body["data"]))
      });
    }
Spring Security 默认情况下只支持 POST 方法提交登录数据。要支持 GET 方法提交登录数据,需要通过配置 `org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter` 过滤器的 `setPostOnly(false)` 方法来实现。 具体实现步骤如下: 1. 创建一个继承自 `UsernamePasswordAuthenticationFilter` 的类,重写其 `attemptAuthentication` 方法。 ```java public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!"POST".equals(request.getMethod()) && !"GET".equals(request.getMethod())) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } // 处理登录请求 // ... } } ``` 2. 在 Spring Security 配置类中,将上述自定义的过滤器添加到过滤器链中,并设置 `setPostOnly(false)`。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .loginProcessingUrl("/login") .and() .logout() .logoutUrl("/logout") .and() .csrf().disable() .addFilterAt(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() throws Exception { CustomUsernamePasswordAuthenticationFilter filter = new CustomUsernamePasswordAuthenticationFilter(); filter.setAuthenticationManager(authenticationManagerBean()); filter.setFilterProcessesUrl("/login"); filter.setPostOnly(false); return filter; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } } ``` 这样就可以通过 GET 请求提交登录数据了。但是,由于 GET 请求会将参数暴露在 URL 中,存在安全风险,建议还是使用 POST 请求
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值