Spring Boot应用之数据加密以及字段过滤

来源:互联网 发布:php使用html模板 编辑:程序博客网 时间:2024/06/02 06:09

1、应用背景

在使用Spring Boot开发基于restful类型的API时,对于返回的JSON数据我们经常需要对数据进行加密,有的时候我们还必须过滤掉一些对象字段的值来减少网络流量

2、解决方案

1)加密

对返回的数据进行加密,我们必须对spring boot返回json数据前对数据进行拦截和加密处理,为了方便api调用解析还原数据,我们采用双向加密的方式,因为客户端需要解密为明文,加密的使用java本身提供。重点在于在返回数据前进行拦截处理,这时我们可以实现spring boot中的ResponseBodyAdvice接口来打到目的。该接口有两个方法

public interface ResponseBodyAdvice<T> {    boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);    T beforeBodyWrite(T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);}

从中可以看出我们着重需要对beforeBodyWrite这个方法进行实现,supports方法的话可以根据自己的需求来确定是否需要使用这个拦截处理

2)数据字段的过滤

对于数据字段的过滤我们这里有两种需求。第一是每个API返回某个对象的数据字段是相同的,比如User对象,每个API需要返回的都是去掉password这个字段,那这种情况我们可以采用JsonView的方式,具体网上可以找到解决方案。第二种需求是对于每一个API返回的某个对象的数据字段不一定相同,都可以通过配置的方式,简单而灵活的达到过滤数据的目的。这时我们的解决方案是在每一个API方法上自定义一个注解,可以配置返回的对象应该包含或者去除哪些字段 ,基于这样的思考我们也可以通过ResponseBodyAdvice中的beforeBodyWrite方法来实现

3、实施方案

1)新建maven项目,添加依赖

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>org.cyxl</groupId>    <artifactId>sprint-boot-responsebodyadvice</artifactId>    <version>1.0-SNAPSHOT</version>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>1.2.5.RELEASE</version>    </parent>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>    </dependencies></project>

主要是添加spring boot的支持
2) 搭建基本框架
文件结构图

具体各个文件代码如下
User模型

package org.cyxl.model;/** * Created by jeff on 15/10/23. */public class User {    private int id;    private String email;    private String password;    public int getId() {        return id;    }    public void setId(int id) {        this.id = id;    }    public String getEmail() {        return email;    }    public void setEmail(String email) {        this.email = email;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }}

Application启动类

package org.cyxl;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.ComponentScan;/** * Created by jeff on 15/10/23. */@SpringBootApplicationpublic class Application {    public static void main(String[] args){        SpringApplication.run(Application.class, args);    }}

UserController类

package org.cyxl.controller;import org.cyxl.model.User;import org.cyxl.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;/** * Created by jeff on 15/10/23. */@RestController@RequestMapping("/user")public class UserController {    @Autowired    UserService userService;    @RequestMapping("/{id}")    public User findUserById(@PathVariable("id")int id){        return userService.getUserById(id);    }    @RequestMapping("/all")    public List<User> findAllUser(){        return userService.getAllUser();    }}

UserService类

package org.cyxl.service;import org.cyxl.model.User;import org.cyxl.repository.UserRepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;/** * Created by jeff on 15/10/23. */@Servicepublic class UserService {    @Autowired    UserRepository userRepository;    public User getUserById(int id){        return userRepository.getUserById(id);    }    public List<User> getAllUser(){        return userRepository.getAllUser();    }}

UserRepository类

package org.cyxl.repository;import org.cyxl.model.User;import org.springframework.stereotype.Repository;import javax.jws.soap.SOAPBinding;import java.util.ArrayList;import java.util.List;/** * Created by jeff on 15/10/23. */@Repositorypublic class UserRepository {    //模仿数据    private static  List<User> users = new ArrayList<User>();    static {        //初始化User数据        for (int i=0;i<10;i++){            User user = new User();            user.setId(i);            user.setEmail("email" + i);            user.setPassword("password" + i);            users.add(user);        }    }    public User getUserById(int id){        for (User user : users){            if(user.getId() == id){                return user;            }        }        return  null;    }    public List<User> getAllUser(){        return users;    }}

此处没有用数据库,采用模拟数据

当前访问http://localhost:8080/user/2的结果是

{"id":2,"email":"email2","password":"password2"}

访问http://localhost:8080/user/all的结果是

[{"id":0,"email":"email0","password":"password0"},{"id":1,"email":"email1","password":"password1"},{"id":2,"email":"email2","password":"password2"},{"id":3,"email":"email3","password":"password3"},{"id":4,"email":"email4","password":"password4"},{"id":5,"email":"email5","password":"password5"},{"id":6,"email":"email6","password":"password6"},{"id":7,"email":"email7","password":"password7"},{"id":8,"email":"email8","password":"password8"},{"id":9,"email":"email9","password":"password9"}]

3)数据加密及过滤

实现自定义注解SerializedField

package org.cyxl.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * Created by jeff on 15/10/23. */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface SerializedField {    /**     * 需要返回的字段     * @return     */    String[] includes() default {};    /**     * 需要去除的字段     * @return     */    String[] excludes() default {};    /**     * 数据是否需要加密     * @return     */    boolean encode() default true;}

实现ResponseBodyAdvice接口的MyResponseBodyAdvice

package org.cyxl;import org.cyxl.annotation.SerializedField;import org.cyxl.util.Helper;import org.springframework.core.MethodParameter;import org.springframework.core.annotation.Order;import org.springframework.http.MediaType;import org.springframework.http.server.ServerHttpRequest;import org.springframework.http.server.ServerHttpResponse;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import java.lang.reflect.Field;import java.util.*;/** * Created by jeff on 15/10/23. */@Order(1)@ControllerAdvice(basePackages = "org.cyxl.controller")public class MyResponseBodyAdvice implements ResponseBodyAdvice {    //包含项    private String[] includes = {};    //排除项    private String[] excludes = {};    //是否加密    private boolean encode = true;    @Override    public boolean supports(MethodParameter methodParameter, Class aClass) {        //这里可以根据自己的需求        return true;    }    @Override    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {        //重新初始化为默认值        includes = new String[]{};        excludes = new String[]{};        encode = true;        //判断返回的对象是单个对象,还是list,活着是map        if(o==null){            return null;        }        if(methodParameter.getMethod().isAnnotationPresent(SerializedField.class)){            //获取注解配置的包含和去除字段            SerializedField serializedField = methodParameter.getMethodAnnotation(SerializedField.class);            includes = serializedField.includes();            excludes = serializedField.excludes();            //是否加密            encode = serializedField.encode();        }        Object retObj = null;        if (o instanceof List){            //List            List list = (List)o;            retObj = handleList(list);        }else{            //Single Object            retObj = handleSingleObject(o);        }        return retObj;    }    /**     * 处理返回值是单个enity对象     *     * @param o     * @return     */    private Object handleSingleObject(Object o){        Map<String,Object> map = new HashMap<String, Object>();        Field[] fields = o.getClass().getDeclaredFields();        for (Field field:fields){            //如果未配置表示全部的都返回            if(includes.length==0 && excludes.length==0){                String newVal = getNewVal(o, field);                map.put(field.getName(), newVal);            }else if(includes.length>0){                //有限考虑包含字段                if(Helper.isStringInArray(field.getName(), includes)){                    String newVal = getNewVal(o, field);                    map.put(field.getName(), newVal);                }            }else{                //去除字段                if(excludes.length>0){                    if(!Helper.isStringInArray(field.getName(), excludes)){                        String newVal = getNewVal(o, field);                        map.put(field.getName(), newVal);                    }                }            }        }        return map;    }    /**     * 处理返回值是列表     *     * @param list     * @return     */    private List handleList(List list){        List retList = new ArrayList();        for (Object o:list){            Map map = (Map) handleSingleObject(o);            retList.add(map);        }        return retList;    }    /**     * 获取加密后的新值     *     * @param o     * @param field     * @return     */    private String getNewVal(Object o, Field field){        String newVal = "";        try {            field.setAccessible(true);            Object val = field.get(o);            if(val!=null){                if(encode){                    newVal = Helper.encode(val.toString());                }else{                    newVal = val.toString();                }            }        } catch (IllegalAccessException e) {            e.printStackTrace();        }        return newVal;    }}

在beforeBodyWrite方法中,我们对拦截的数据根据配置文件进行是否加密和字段过滤。在类上面的注解Order是指定这个拦截器(切确的说是切入点,我们姑且叫做拦截器)的执行优先顺序,ControllerAdvice中的basePackages是指定哪些类需要使用该拦截器,这个很重要。

代码中用到两个工具类Helper和DesUtil这里也贴一下代码
Helper

package org.cyxl.util;import javax.crypto.BadPaddingException;import javax.crypto.IllegalBlockSizeException;import javax.crypto.NoSuchPaddingException;import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;import java.io.IOException;import java.io.UnsupportedEncodingException;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;/** * Created by jeff on 15/10/23. */public class Helper {    private static String key = "wow!@#$%";    public static boolean isStringInArray(String str, String[] array){        for (String val:array){            if(str.equals(val)){                return true;            }        }        return false;    }    public static String encode(String str){        String enStr = "";        try {            enStr = DesUtil.encrypt(str, key);        } catch (Exception e) {            e.printStackTrace();        }        return enStr;    }    public static String decode(String str) {        String deStr = "";        try {            deStr = DesUtil.decrypt(str, key);        } catch (Exception e) {            e.printStackTrace();        }        return deStr;    }}

DesUtil

package org.cyxl.util;import java.io.IOException;import java.security.SecureRandom;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.DESKeySpec;import sun.misc.BASE64Decoder;import sun.misc.BASE64Encoder;public class DesUtil {    private final static String DES = "DES";    public static void main(String[] args) throws Exception {        String data = "123 456";        String key = "wow!@#$%";        System.err.println(encrypt(data, key));        System.err.println(decrypt(encrypt(data, key), key));    }    /**     * Description 根据键值进行加密     * @param data     * @param key  加密键byte数组     * @return     * @throws Exception     */    public static String encrypt(String data, String key) throws Exception {        byte[] bt = encrypt(data.getBytes(), key.getBytes());        String strs = new BASE64Encoder().encode(bt);        return strs;    }    /**     * Description 根据键值进行解密     * @param data     * @param key  加密键byte数组     * @return     * @throws IOException     * @throws Exception     */    public static String decrypt(String data, String key) throws IOException,            Exception {        if (data == null)            return null;        BASE64Decoder decoder = new BASE64Decoder();        byte[] buf = decoder.decodeBuffer(data);        byte[] bt = decrypt(buf,key.getBytes());        return new String(bt);    }    /**     * Description 根据键值进行加密     * @param data     * @param key  加密键byte数组     * @return     * @throws Exception     */    private static byte[] encrypt(byte[] data, byte[] key) throws Exception {        // 生成一个可信任的随机数源        SecureRandom sr = new SecureRandom();        // 从原始密钥数据创建DESKeySpec对象        DESKeySpec dks = new DESKeySpec(key);        // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);        SecretKey securekey = keyFactory.generateSecret(dks);        // Cipher对象实际完成加密操作        Cipher cipher = Cipher.getInstance(DES);        // 用密钥初始化Cipher对象        cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);        return cipher.doFinal(data);    }    /**     * Description 根据键值进行解密     * @param data     * @param key  加密键byte数组     * @return     * @throws Exception     */    private static byte[] decrypt(byte[] data, byte[] key) throws Exception {        // 生成一个可信任的随机数源        SecureRandom sr = new SecureRandom();        // 从原始密钥数据创建DESKeySpec对象        DESKeySpec dks = new DESKeySpec(key);        // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);        SecretKey securekey = keyFactory.generateSecret(dks);        // Cipher对象实际完成解密操作        Cipher cipher = Cipher.getInstance(DES);        // 用密钥初始化Cipher对象        cipher.init(Cipher.DECRYPT_MODE, securekey, sr);        return cipher.doFinal(data);    }}

这里采用DES算法进行加密,可以设置自己的加密算法

接下来看看我们如何使用自定义注解和配置过滤字段,在UserController中的两个API我们的变化为

@RequestMapping("/{id}")    @SerializedField(includes = {"id", "email"}, encode = false)    public User findUserById(@PathVariable("id")int id){        return userService.getUserById(id);    }    @RequestMapping("/all")    @SerializedField(excludes = {"id","password"})    public List<User> findAllUser(){        return userService.getAllUser();    }

查找单个user对象的api我们配置了应该包含id和email两个字段并不加密,访问http://localhost:8080/user/1得到的结果为

{"id":"1","email":"email1"}

可以看出结果中只有配置的两个字段id和email。

在查找所有user对象api中我们配置了去除id和password两个字段,加密使用默认的配置true,访问http://localhost:8080/user/all得到的结果为

[{"email":"VF+mPHf1EuI="},{"email":"EOJMFSFgsps="},{"email":"f4R+0CfsxMw="},{"email":"Yc9Q0i1HgII="},{"email":"/LByd7J+cF0="},{"email":"sZgJ3ylyxg0="},{"email":"hjYKo1ceg6U="},{"email":"l3TMFt0j+Kw="},{"email":"lkQICJp377U="},{"email":"/eSKhBhKP8w="}]

可以看出返回的数据中只有user的email字段,并且数据已经经过加密

最后来一张整体的文件结构图
这里写图片描述

所有代码均已在文中出现,包括一些注释

源代码地址: git://code.csdn.net/CYXLZZS/sprint-boot-responsebodyadvice.git

0 0
原创粉丝点击