Spring Boot应用之数据加密以及字段过滤
2017-09-14 18:16
726 查看
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); }1
2
3
4
5
[/code]
从中可以看出我们着重需要对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>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
[/code]
主要是添加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; } }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
[/code]
Application启动类
package org.cyxl; import org.springframework.boot.SpringApplication; import or 1b8fe g.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); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[/code]
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(); } }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
[/code]
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(); } }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
[/code]
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; } }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
[/code]
此处没有用数据库,采用模拟数据
当前访问http://localhost:8080/user/2的结果是
{"id":2,"email":"email2","password":"password2"}1
[/code]
访问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"}]1
[/code]
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; }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
[/code]
实现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; } }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
[/code]
在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; } }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
[/code]
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); } }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
[/code]
这里采用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(); }1
2
3
4
5
6
7
8
9
10
11
[/code]
查找单个user对象的api我们配置了应该包含id和email两个字段并不加密,访问http://localhost:8080/user/1得到的结果为
{"id":"1","email":"email1"}1
[/code]
可以看出结果中只有配置的两个字段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="}]1
[/code]
可以看出返回的数据中只有user的email字段,并且数据已经经过加密
最后来一张整体的文件结构图
所有代码均已在文中出现,包括一些注释
源代码地址: git://code.csdn.net/CYXLZZS/sprint-boot-responsebodyadvice.git
原文地址:http://blog.csdn.net/cyxlzzs/article/details/49357197
相关文章推荐
- Spring Boot应用之数据加密以及字段过滤
- Spring Boot 数据响应之数据加密以及字段过滤
- Spring-Boot+Solr搜索应用(索引数据创建+关键字高亮+thymeleaf静态html模板渲染跳转)
- 【云星数据---mesos实战系列003】:marathon实战009--marathon部署一个springboot应用
- redis中的基本数据类型,以及在Spring-Boot对Redis的基本使用
- idea+Spring Boot的第一个应用,以及热部署的配置
- spring boot最新教程(四):返回json数据以及集成fastjson的使用
- SpringBoot项目总结--(1)字段校验与加密
- spring boot新手教程之使用FastJson解析JSON数据以及解决返回中文乱码问题
- Spring Boot: 加密应用配置文件敏感信息
- Spring Boot: 加密应用配置文件敏感信息
- Jasypt : 整合spring boot加密应用配置文件敏感信息
- SpringBoot+MyBatis简单数据访问应用的实例代码
- springBoot的返回code数据的处理以及统一异常处理
- springboot源码分析14-事件发布机制以及应用监听器
- spring boot JPA加密解密字段实践记录
- SpringBoot-Actuator应用监控以及优雅停机
- redis中的基本数据类型,以及在Spring-Boot对Redis的基本使用