Spring Boot应用之数据加密以及字段过滤
2015-10-23 09:38
645 查看
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. */ @SpringBootApplication public 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. */ @Service public 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. */ @Repository public 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
相关文章推荐
- 深度优先实现拓扑排序--java
- 在Servlet中获取Spring的指定bean
- java keytool证书工具使用小结
- SpringMVC+MyBatis+EasyUI 实现分页查询
- eclipse项目没错但有红叉
- JAVA初始化过程
- 完成这个例子,说出java中针对异常的处理机制。
- java高精度开平方
- Java基本数据类型
- java开源微博系统weibo4j分享
- 使用JavaMail碰到的几个问题
- eclipse中AndroidA工程依赖B工程设置
- java web项目防止多用户重复登录解决方案
- 如何切换不同版本的JDK
- struts2 拦截器和actioninvocation
- java 桌面路径
- Eclipse包结构显示调整
- java实现文件上传--flash上传
- spring概述以及优点
- java正则表达式学习