您的位置:首页 > 编程语言 > Java开发

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