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

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: