Spring Boot應用之資料加密以及欄位過濾
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
從中可以看出我們著重需要對beforeBodyWrite這個方法進行實現,supports方法的話可以根據自己的需求來確定是否需要使用這個攔截處理
2)資料欄位的過濾
對於資料欄位的過濾我們這裡有兩種需求。第一是每個API返回某個物件的資料欄位是相同的,比如User物件,每個API需要返回的都是去掉password這個欄位,那這種情況我們可以採用JsonView的方式,具體網上可以找到解決方案。第二種需求是對於每一個API返回的某個物件的資料欄位不一定相同,都可以通過配置的方式,簡單而靈活的達到過濾資料的目的。這時我們的解決方案是在每一個API方法上自定義一個註解,可以配置返回的物件應該包含或者去除哪些欄位 ,基於這樣的思考我們也可以通過ResponseBodyAdvice中的beforeBodyWrite方法來實現
3、實施方案
1)新建maven專案,新增依賴
<?xml version="1.0" encoding="UTF-8"?>
<projectxmlns="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
主要是新增spring boot的支援
2) 搭建基本框架
具體各個檔案程式碼如下
User模型
package org.cyxl.model;
/**
* Created by jeff on 15/10/23.
*/
public classUser {
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
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 classApplication {
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
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 classUserController {
@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
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 classUserService {
@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
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 classUserRepository {
//模仿資料
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
此處沒有用資料庫,採用模擬資料
{"id":2,"email":"email2","password":"password2"}
- 1
[{"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
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 @interfaceSerializedField {
/**
* 需要返回的欄位
* @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
實現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 classMyResponseBodyAdviceimplementsResponseBodyAdvice {
//包含項
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
在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 classHelper {
private static String key = "[email protected]#$%";
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
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 classDesUtil {
private final static String DES = "DES";
public static void main(String[] args) throws Exception {
String data = "123 456";
String key = "[email protected]#$%";
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 根據鍵值進行解密
*