# 安全
# 总览
基础平台框架已支持或者计划支持的安全措施包含如下:
安全措施 | 完成度 | 安全措施 | 完成度 | 安全措施 | 完成度 |
---|---|---|---|---|---|
跨域 | 已完成 | SQL注入攻击 | 已完成 | 防重放重刷 | 已完成 |
XSS攻击 | 已完成 | CSRF攻击 | 已完成 | 接口签名 | 已完成 |
接口加解密 | 已完成 | 数据库加解密 | 已完成 | 配置文件脱敏 | 已完成 |
登录授权 | 已完成 | 账号登录 | 已完成 | 动态口令登录 | 已完成 |
第三方平台登录 | 已完成 | 刷新令牌登录 | 已完成 | 扩展认证 | 已完成 |
OAuth2对接 | 已完成 | 密码安全策略 | 已完成 | https访问 | 已完成 |
登陆日志 | 已完成 | 系统日志 | 已完成 | 数据日志 | 已完成 |
api日志 | 已完成 | 防盗链 | 规划中 | url重定向(配置管理) | 规划中 |
ip黑白名单 | 规划中 | mac黑白名单 | 规划中 |
# 跨域
# 概述
为什么会出现跨域问题?
出于浏览器的同源策略(same origin policy)限制。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)。同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。 虽然同源策略的限制是必要的,但是有时很不方便,合理的用途也受到影响。下面介绍跨域请求的解决方案。
# 解决方案
CORS
是跨域资源共享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。springmvc或springboot项目实现跨域请求有多种解决方案,比较常见的方案有使用注解 @CrossOrigin
方式、有重写WebMvcConfigurer
方式、有使用过滤器
方式等等,开发框架的hos-framework-security-starter
模块集成了spring security
也是支持配置跨域请求。下面着重介绍下使用注解 @CrossOrigin
和使用本框架的hos-framework-security-starter
模块解决跨域问题。
使用注解@CrossOrigin(推荐)
@CrossOrigin
是Spring 从4.2版本后开始支持实现跨域的注解,一般放在controller的类或方法上。
@CrossOrigin
具有多个属性,如允许来源域名列表、跨域请求中允许的请求头中的字段类型、跨域HTTP请求中支持的HTTP请求类型(GET
、POST
...)等等,这些都可灵活配置。
@CrossOrigin
属于局部跨域,作用域局限于你所加注解的类或方法,所以它比全局配置的跨域更加灵活、细粒度更高。
@CrossOrigin
作用在类上示例:在StaffController
类上添加
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/user/staff")
public class StaffController extends BaseController<Staff> {
...
}
@CrossOrigin
作用在方法上示例:在SraffController
类selectPageStaff
方法上添加
@CrossOrigin(origins = "*")
@GetMapping("/selectPageStaff")
public BaseResponse<IPage<Staff>> selectPageStaff(@RequestBody Staff staff) {
return BaseResponse.success(staffService.selectPageStaff(staff));
}
使用框架hos-framework-security-starter模块跨域
hos-framework-security-starter模块集成了spring security并进行封装处理,下面介绍下使用过程。
1.添加hos-framework-security-starter
依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-security-starter</artifactId>
</dependency>
2.在项目配置文件application-dev.yml
(此名字为示例,具体请按照自己项目配置文件修改)中添加配置
#安全攻击防护相关配置
framework:
security:
#跨域支持,默认关闭
cors:
enable: true
#allowed:
#跨域允许来源,可配置多个,多个之间用逗号分隔,在framework.security.cors.enable=true时有效,不配置默认允许所有来源
#origins: https://www.baidu.com,https://www.taobao.com
#跨域允许方法类型,可配置多个,多个之间用逗号分隔,在framework.security.cors.enable=true时有效,不配置默认允许所有方法类型
#methods: POST,PUT,DELETE
此处配置的为全局跨域策略。支持配置跨域启动开关、跨域允许来源和跨域允许方法,具体说明参考配置上注释即可。
# SQL注入攻击
# 概述
SQL注入攻击(SQL Injection),简称为注入攻击。SQL注入,被广泛用于非法获取网站控制权,这是在应用程序的数据库层中发生的安全漏洞。在设计程序中,忽略了对输入字符串中包含的SQL命令的检查,并且将数据库误认为是要运行的常规SQL命令,这导致数据库受到攻击,从而可能导致盗窃,修改和删除数据,并进一步导致网站嵌入恶意代码,植入后门程序的危害等。
# 解决方案
1.对于使用Mybatis作为ORM框架,可采用#{parameterName}来接收参数方式。
众所周知,在Mybatis的xml中接收参数有#{parameterName}
和${parameterName}
两种方式。滥用${parameterName}
可能会出现SQL注入,而使用#{parameterName}
会避免SQL注入,原因如下:
- Mybatis使用
${parameterName}
接收参数是直接把参数拼装在SQL中,也就是未经过预编译,仅仅是取变量的值 - Mybatis使用
#{parameterName}
接收参数会先预编译SQL再填充参数
例如存在如下SQL:
<select id="selectUser2" parameterType="java.lang.String" resultMap="userMap">
select id,name,password,age
from dmhr.t_user
where name = ${name}
and password = ${password}
</select>
如果传入参数name的值为’admin’ or 1 = 1
,password的值为’1’
,那么使用$
接收参数后拼接的SQL就变成了下边示例:
这样查询该用户数据时即使不知道用户密码,也会查询出用户名为admin
的用户信息,也就是遭到了SQL注入攻击。那如果使用#{parameterName}
示例如下:
<select id="selectUser2" parameterType="java.lang.String" resultMap="userMap">
select id,name,password,age
from dmhr.t_user
where name = #{name}
and password = #{password}
</select>
在传入同样的参数时日志打印SQL如下:
会发现变量的位置采用占位符?
占位,之后再组装2个参数,组装后的SQL如下:
select id,`name`,password,age from t_user where `name` = "'admin' or 1=1" and password = "'1'"
这个SQL在数据库中执行会查不到结果,也就防止了SQL注入。
2.自定义过滤器
除了使用Mybatis自带功能外,也可以自定义过滤器来达到避免SQL注入的效果。自定义过滤器会对接口传入参数进行非法字符校验,如含有’
、,
、&
、<
、select
、delete
等一些特殊字符或字符串,过滤器会进行拦截。关于自定义过滤器平台后续可能会提供具体解决方案,敬请关注。
# XSS攻击
# 概述
XSS(跨站脚本攻击)是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、 VBScript、ActiveX、 Flash 或者甚至是普通的HTML。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。 形成XSS漏洞的主要原因是程序中输入和输出的控制不够严格。
# 解决方案
基础平台定义了XSS过滤器针对特殊字符进行过滤,目前过滤器配置的正则表达式。
使用HOS基础平台的过滤器可防止XSS攻击,使用步骤如下
1.添加hos-framework-security-starter依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-security-starter</artifactId>
</dependency>
2.在项目配置文件application-dev.yml(此名字为示例,具体请按照自己项目配置文件修改)中添加配置
#安全攻击防护相关配置
framework:
security:
#防xss攻击,默认关闭
xss:
enable: true
开启XSS过滤器开关即可,默认为关闭状态。
# CSRF攻击
# 概述
CSRF(跨站请求伪造)是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。CSRF 攻击之所以能够成功,是因为攻击者可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。
# 解决方案
CSRF攻击出现的本质是服务端对身份验证过于简单,用户信息都是存在于 cookie 中,如果身份验证不是采用这种机制(如使用token令牌的方式)可不用考虑该类攻击问题。如果确实使用cookie验证机制,可使用集成平台后端工程防CSRF攻击,步骤如下
1.添加hos-framework-security-starter依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-security-starter</artifactId>
</dependency>
2.在项目配置文件application-dev.yml(此名字为示例,具体请按照自己项目配置文件修改)中添加配置
#安全攻击防护相关配置
framework:
security:
#防csrf攻击,默认关闭。此处需要注意:csrf开关开启后,除"GET", "HEAD", "TRACE", "OPTIONS"四类请求可以直接通过外,其他请求都会拦截
#由于防csrf采用了token机制,所以要求前端页面传入csrfToken,传入方式有两种:
#方式一:隐藏域 <input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}'>写法固定;
#方式二:header中传入,如果token保存在session中(默认),则header-name为 X-CSRF-TOKEN,header-value为token具体值,具体值需要前端从session中获取;
#如果token保存在cookie中,则header-name为 X-XSRF-TOKEN,header-value为token具体值,具体值需要前端从cookie中获取
csrf:
enable: true
需要注意的是,防止CSRF攻击需要前后端配合进行。后端需要开启防CSRF攻击开关,默认为关闭状态;前端需要上送csrfToken,如果没有上送或前端上送csrfToken与后端存储的csrfToken不一致则认为可能受到了csrf攻击,就会执行进行后续的异常策略。前端上送csrfToken有两种方式,具体见配置注释。
还有一点值得注意,"GET"、 "HEAD"、"TRACE"和"OPTIONS"这四类请求会直接放行即前端即使没有传入csrfToken也不会拦截。集成平台后端工程集成的spring security认为如果是通过上述方式发起的请求,意味着它只是访问服务器的资源,没有对服务器的资源进行更新,所以对于这类请求,spring security的防御策略是允许的。
# 防重放重刷
# 概述
重放攻击的基本原理就是把以前窃听到的数据原封不动地重新发送给接收方。很多时候,网络上传输的数据是加密过的, 此时窃听者无法获悉数据的准确意义。但如果他知道这些数据的作用,就可以在不知道数据内容的情况下通过 再次发送这些数据达到欺骗接收端的目的。
# 解决方案
本功能针对不同的使用场景,提供 全局 以及 注解接口 两种模式
具体实现: 后端接收到请求,提取请求接口地址以及传递的参数,使用MD5加密,之后检验缓存中是否已经存在该MD5加密串, 若缓存中已经存在即判定该请求为重复请求,直接进行拦截,若不存在将MD5加密串存放到缓存中
注意:防重放依赖缓存功能,如果要使用防重放,请先引入HOSCache缓存组件,详情见缓存功能说明
# 使用说明
1.添加hos-framework-security-starter依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-security-starter</artifactId>
</dependency>
2.在项目配置文件application-dev.yml(此名字为示例,具体请按照自己项目配置文件修改)中添加配置
framework:
interface-rebrush:
#是否全局进行接口验证,开启之后所求的请求都会做重放验证
isGlobal: true
#防止重新提交的时间参数,单位秒,默认为5s
second: 5
#全局模式下需要排除的请求
excludeUrl:
- /testRequest/*
根据framework.interface-rebrush.isGlobal
的配置对应不同的处理方式:
全局模式:
配置项 framework.interface-rebrush.isGlobal
值为 true
该模式下除excludeUrl配置项匹配的请求,其他请求均会重放验证
注意:在开启全局模式的情况下,重放注解 @InterfaceRebrush 功能失效
注解模式:
配置项 framework.interface-rebrush.isGlobal
值为false
该模式下,只有带有注解 @InterfaceRebrush 的控制层方法所对应的请求会进行重放验证
例如:
@InterfaceRebrush
@PostMapping("/ceshipost001/{testName}")
public BaseResponse<String> ceshiLianjiePost(@PathVariable() String testName){
return BaseResponse.success(testName);
}
# 接口签名
# 概述
接口开发是各系统之间对接的重要方式,其数据是通过开放的互联网传输,对数据的安全性要有一定要求。 为了提高传输过程参数的防篡改性,签名是目前比较常用的方式。
# 解决方案
本功能针对不同的使用场景,提供全局接口签名验证以及注解接口签名验证两种模式
具体实现:
1.接口签名流程
前端将请求的参数,按照接口生成规则,生成接口签名signature,并和 时间戳timestamp(用于验证签名的时效性) 一起放入请求头中。 后端接收到请求之后,先通过时间戳timestamp验证请求的时效性。验证通过之后, 提取请求参数并根据签名生成规则生成新的签名,并与前端请求中所携带的签名signature进行对比校验
2.签名生成规则:
1、
PathVariable:将Key进行排序,按照key=value顺序进行拼接,使用#分割
Parameter:将Key进行排序,按照key=value顺序进行拼接,使用#分割
Body:从request inputstream中获取保存为String形式
如果存在多种数据形式,则按照body、parameter、path variable的顺序进行再拼接,
得到所有数据的拼接值,值记作 字符串Y。
2、
签名秘钥secret拼接:
secret=xxx#值,记作 字符串X
3、
最终拼接值=XY,最后使用MD5加密
key的排序方式:数字-字母-中文
例如:01,0C,10,11,2,a,A,AA,C,shen,z,什么
字符按照数字0~9,字母(aA~zZ),中文。按照该顺序,先排序首个字符再排第二个字符,依次类推
3.调用说明
调用验证接口签名的接口时,需要在请求头header中传入两个必须的参数:signature 与 timestamp
signature:按照上述签名生成规则生成接口签名 ,timestamp:时间戳
postman的调用示例如下:
*Vue端签名处理在HOS前端框架中已集成完成可以直接使用,其他端请参考上述签名规则实现
# 使用说明
1.添加hos-framework-security-starter依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-security-starter</artifactId>
</dependency>
2.在项目配置文件application-dev.yml(此名字为示例,具体请按照自己项目配置文件修改)中添加配置
framework:
interface-signature:
#是否开启全局模式,开启之后所求的请求都会验证签名
isGlobal: false
#签名的有效时长(通过时间戳验证),单位分钟,默认为5分钟
minute: 5
#秘钥,用于签名生成
secret: 11111
# 需要排除的链接
excludeUrl:
- /ceshipost002/*
根据framework.interface-signature.isGlobal
的配置对应不同的处理方式:
全局模式:
配置项 framework.interface-signature.isGlobal
值为true
该模式下除了excludeUrl项中匹配的请求其他所有请求都会经过签名验证
应用场景:安全性要求高,所有接口都需要做接口签名
注意:在开启全局模式的情况下,签名注解 @InterfaceSign 功能失效
注解模式:
配置项 framework.interface-signature.isGlobal
值为false
该模式下,添加了注解 @InterfaceSign 控制层方法才会验证签名
应用场景:安全要求的接口,或者对外提供的接口
例如:
@InterfaceSign
@PostMapping("/ceshipost001/{testName}")
public BaseResponse<String> ceshiLianjiePost(@PathVariable() String testName){
return BaseResponse.success(testName);
}
# 接口加解密
# 概述
为保证请求过程中传输数据的安全,提高系统的保密性,需要对接口进行加密。
本组件功能支持AES(对称加密)、RSA(非对称加密)、SM4(国密)三种加密方式, 支持后端接口的入参解密以及出参加密。
# 解决方案
加密传输流程
本组件功能主要包含两个方面: 接口入参的解密操作 以及 接口出参的加密操作。
接口入参的解密:分为全参数解密以及部分参数的解密。
接口出参加密:对接口出参(BaseResponse的data字段)加密,由前端接收到之后做解密
关于前端的加密与解密请参考前端文档
名词解释:
param参数:
param参数为请求地址后携带的参数,例如请求地址为:
http://localhost:8367/ceshipost001/001?Param012=731328dc0bddf59f128b496c31049170&Param010=1222
其中Param012和Param010为param参数
body参数: 放到RequestBody里面的参数,例如常规的ajax请求内容就存放到body里面
全参数解密:
前端对所有业务参数进行加密,后端接收到请求后进行解密,将解密出来的参数以及数据,重新放入请求中的原本位置
固定参数解密:
只针对请求中重要参数字段(password等)进行加密传输,后端接收到请求之后,只对这些固定的参数进行解密。
# 使用说明
1.添加hos-framework-security-starter依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-security-starter</artifactId>
</dependency>
2.在项目配置文件application-dev.yml(此名字为示例,具体请按照自己项目配置文件修改)中添加配置
framework:
interface-encryption:
#加密方式,支持的方式有 AES、RSA、国密(SM4)
decryptType: AES
# RSA公钥,用于加密解密
publicKey:
# RSA私钥,用于加密解密
privateKey:
# SM4或者AES秘钥,用于接口数据加解密
secret: 1234567890123456
#是否开启全局模式
#开启后,除excludeUrl项匹配的请求地址,对其他请求进行均解密或加密操作
#未开启,只对添加了加解密注解的控制层方法进行加解密操作
#在全局模式下,isDecrypt控制是否进行接口入参解密,isEncrypt控制是否对接口出参进行加密
isGlobal: true
##以下配置项均在全局模式下使用
#是否对接口的入参进行解密,全局模式下使用
isDecrypt: false
#是否对接口返回的结果进行加密,默认情况只加密data字段的数据
isEncrypt: false
#对body里面的数据单个解密,还是作为一个整体解密
decryptAll:
# RequestParam(param参数)的参数进行全参数解密
params: true
# RequestBody(body参数)的参数进行全参数解密
body: false
# 解密的参数配置,isGlobal为ture(全局解密),encrypAll为false的情况下,只对单个参数进行解密
decryptParam:
# 对请求RequestParam(param参数)中附加的参数进行解密
params:
- Param012
- param01
# 对RequestBody(body参数)里面的参数进行解密,只支持键值对的JSON
body:
- param01
# 全局情况下,排除的请求
excludeUrl:
- /ceshipost001/*
注意:加密方式为AES时,秘钥为32长度的字符串,加密方式为SM4时,秘钥为16长度字符串
# 全局模式
framework.interface-encryption.isGlobal
值为true
时开启。
除framework.interface-encryption.excludeUrl
配置项中匹配的链接,
对其他所有的请求接口进行解密操作。
注意:在开启全局模式的情况下,对应注解 @InterfaceDecrypt、@InterfaceEncrypt 功能均会无效
# 接口入参解密
framework.interface-encryption.isDecrypt
值为true
时,对接口的入参进行解密。
接口入参的解密分为两种情况,全参数解密以及固定参数解密。
# 全参数解密
前端加密之后的将密文需要放入encrypt的参数中,后端获取到encrypt中的密文, 将其解密之后再将各个入参放到原本的位置。 若设置为全参数解密模式,encrypt为必须的参数。
framework.interface-encryption.decryptAll.params
的值为true
,启动param参数全参数解密
framework.interface-encryption.decryptAll.body
的值为true
,启动body参数全参数解密
如果是param参数全参数解密,请求例子如下: http://localhost:8367/ceshipost001/001?encrypt=86836161de9d1ecb29a04ce00e8b224d594620283a78a40d08ca53239090dc47 获取到encrypt中的密文并解密之后为数据为 Param01=011 ,之后会将Param01放到请求的 param参数中
如果是body参数全参数解密,requestBody中的数据格式为JSON格式,并需要encrypt参数,例如:
{
"encrypt":"731328dc0bddf59f128b496c31049170"
}
解密后的数据须为JSON格式,例如:JSON键值对
{
"param01":"001",
"param02":"002"
}
或者 JSON数组
["param01","param02"]
之后,解密的数据会放入RequestBody(body参数)中
# 固定参数解密
从请求中固定的参数中获取密文,分别进行解密,并将明文放入原本的位置
framework.interface-encryption.decryptAll.params
的值为false
,启动param参数固定参数解密
framework.interface-encryption.decryptAll.params
的值为false
,启动body参数固定参数解密
param参数会根据framework.interface-encryption.decryptParam.params
中匹配参数名称进行解密,
body参数会根据framework.interface-encryption.decryptParam.body
中匹配参数名称进行解密
如果param参数解密,请求例子如下:
http://localhost:8367/ceshipost001/001?password=731328dc0bddf59f128b496c31049170,
根据framework.interface-encryption.decryptParam.params
中的值
匹配到password字段需要解密,获取到password中的密文并解密,将明文放入password,解密后的完整的请求为
http://localhost:8367/ceshipost001/001?password=20200101
如果是body参数,例如:RequestBody中的请求参数为
{
"password":"731328dc0bddf59f128b496c31049170"
}
根据framework.interface-encryption.decryptParam.body
配置项,
匹配到password参数需要解密,获取password存放的密文并解密得到明文为 001 ,最终RequestBody中的请求参数为:
{
"password":"001"
}
# 接口出参加密
framework.interface-encryption.isEncrypt
值为true
时开启。
对于返回的数据,只对BaseResponse中的data字段中的数据进行加密,加密后的密文会重新放入到data字段中,
并返回响应给前端。
例如:
方法返回的原数据为:
{
"code": "200",
"msg": "",
"data": {
"name":"测试",
"value":"test"
},
"success": true
}
对data字段中的数据进行加密,加密之后的结果为:
{
"code": "200",
"msg": "",
"data": "731328dc0bddf59f128b496c31049170",
"success": true
}
此时data中存放的是加密后的密文
# 注解模式
不开启全局模式,即framework.interface-encryption.isGlobal
值为false
时,使用的就是注解模式。
此时只针对添加了加解密注解的控制层方法进行加解密操作。
# 接口入参解密
功能注解为 @InterfaceDecrypt。 注解模式接口入参解密同样支持两种方式:全参数解密以及固定参数的解密。
@InterfaceDecrypt注解说明:
功能描述:
根据bodyParams、keyParams两个属性的值,对控制层方法所对应的请求进行解密操作。 具体的解密操作与要求与全局模式中相同。
若bodyParams中不为空,匹配该请求中body参数中参数名称并对匹配到的参数进行固定参数解密; 若bodyParams中不指定任何参数或者为空,代表对body参数进行全参数解密;
若keyParams中不为空,匹配该请求中的param参数中参数名称并对匹配到的参数进行固定参数解密; 若keyParams中不指定任何参数或者为空,代表对param参数进行全参数解密。
使用位置:方法
属性:
属性 | 功能描述 |
---|---|
bodyParams | body参数中需要解密的参数名称,可以为空。未指定值时,代表对body参数全参数解密 |
keyParams | param参数中需要解密的参数名称,可以为空。未指定值时,代表对param参数全参数解密 |
使用方式例如:
@InterfaceDecrypt(bodyParams = {"ceshiParam01"},keyParams = {"param01"})
@PostMapping("/ceshipost002/001")
public BaseResponse<String> ceshiLianjiePost(
@PathVariable String ceshiParam01,
@RequestBody String bodyStr
){
return BaseResponse.success("successTest001");
}
# 接口出参加密
功能注解为 @InterfaceEncrypt,使用方式:
@InterfaceEncrypt
@PostMapping("/ceshipost001/{testName}")
public BaseResponse<String> ceshiLianjiePost(@PathVariable() String testName){
return BaseResponse.success(testName);
}
只对BaseResponse中的data字段进行加密操作,之后返回给前端,例如
返回的原数据为:
{
"code": "200",
"msg": "",
"data": {
"name":"测试",
"value":"test"
},
"success": true
}
加密之后的结果为:
{
"code": "200",
"msg": "",
"data": "731328dc0bddf59f128b496c31049170",
"success": true
}
此时data中存放的是加密后的密文
# 数据库加解密
# 概述
- 数据库加解密可以实现对数据库中的敏感数据加密存储及明文展示,能有效防止明文存储引起的数据泄密问题,可极大地提高数据安全性。
- 集成平台后端工程提供了数据库加解密的解决方案,大体流程为数据入库前可对指定数据加密处理以密文存储,解密查询结果并以明文展示。在整个过程中用户操作的均为明文数据,无需关心底层的数据加解密过程。本方案兼容Mybatis与MybatisPlus(使用上略有区别),同时加密方式上支持
AES
、SM4
(国密)两种加密算法,使用者可根据自己的业务进行选择,通过简单的配置即可完成两种算法的切换。
# 配置
- 引入依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-database-encryption-starter</artifactId>
</dependency>
- 配置文件
framework:
datasource-encryption:
enable: true
encryptType: AES
secret: 12345678123456781234567812345678
enable:数据库加解密开关,默认为false关闭
encryptType:加密算法,可选的方式有AES、SM4,默认为SM4
secret:密钥,默认为1234567812345678(对应SM4),注意AES密钥长度为32,SM4密钥长度为16
# 使用介绍
使用Mybatis和MybatisPlus操作数据库加解密时在使用方式上会有一些差异,下面介绍下各自的使用方式。
# 使用Mybatis
1.SQL参数加密
如果SQL语句(无论增删改查)的参数含有需要加密的数据,那么需要在mapper.xml中在参数#{xxx}
后添加typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler
这项配置,使用示例如下所示
<update id="testMybatisUpdate">
update staff
set phone = #{staff.phone,typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler},
email = #{staff.email},
org_id = #{staff.orgId}
where name = #{staff.name,typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler}
</update>
示例中字段phone
和字段name
的参数上都添加了typeHandler
,意味着在执行数据库操作之前这两个字段需要加密。
2.SQL的执行结果解密
如果SQL语句的查询结果中含有需要解密的数据,则需要在select
标签上指定resultMap
(不能指定为resultType
),同时需要在resultMap
内的相应column
后面添加typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler
,示例如下
<resultMap id="BaseResultMap" type="com.mediway.oa.user.model.entity.Staff">
<id column="id" jdbcType="VARCHAR" property="id" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="phone" jdbcType="VARCHAR" property="phone" typeHandler="com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="gender" jdbcType="VARCHAR" property="gender" />
<result column="description" jdbcType="VARCHAR" property="description" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="name" jdbcType="VARCHAR" property="name" typeHandler="com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler"/>
<result column="org_id" jdbcType="VARCHAR" property="orgId" />
</resultMap>
示例中字段phone
和字段name
添加了typeHandler
,意味着他们从数据库中查询出结果后需要解密。
# 使用MybatisPlus
1.操作的实体类上添加注解
@TableName(autoResultMap = true)
2.实体类的字段上添加注解
@TableField(typeHandler = DataBaseEncryptTypeHandler.class)
# 注意事项
使用Mybatis的查询SQL时,返回结果类型必须为
resultMap
,返回其它形式加解密不生效。在使用MybatisPlus的条件构造器(各种Wrapper)进行查询时,如果查询条件中含有敏感字段,则只支持使用
Wrapper.setEntity(T entity)
的方式构造查询条件,其它如eq、gt、ne等方式一律无效(这些方式敏感字段不加密)。如果使用setEntity的方式无法满足业务中的需求,则建议在Mybatis的mapper.xml中书写SQL语句。使用MybatisPlus的有关map操作的相关接口(如BaseMapper.selectByMap、BaseMapper.selectMaps、BaseMapper.selectMapsPage)时加解密不生效。
# 使用示例
下边分别以Mybatis和MybatisPlus两种方式演示员工的新增和查询步骤。
- 添加依赖
<dependency>
<groupId>com.mediway.hos</groupId>
<artifactId>hos-framework-database-encryption-starter</artifactId>
</dependency>
- 配置文件中开启开关、指定加密算法和密钥
framework:
datasource-encryption:
enable: true
encryptType: AES
secret: 12345678123456781234567812345678
3.演示mybatis,StaffMapper.java中分别创建新增和查询接口
/**
* 新增员工
*
* @param staff 员工对象
* @return
*/
int testMybatisInsert(@Param("staff") Staff staff);
/**
* 查询员工
*
* @param name 员工姓名列表
* @param phone 员工手机号
* @return 员工列表
*/
List<Staff> testMybatisSelect(@Param("name") String name, @Param("phone") String phone);
StaffMapper.xml中分别书写对应的SQL语句,name
和phone
作为敏感字段
<!--新增员工-->
<insert id="testMybatisInsert" parameterType="com.mediway.oa.user.model.entity.Staff">
insert into staff(id,name,gender,age,email,phone,org_id,description) values(
#{staff.id},
#{staff.name,typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler},
#{staff.gender},
#{staff.age},
#{staff.email},
#{staff.phone,typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler},
#{staff.orgId},
#{staff.description})
</insert>
<!--自定义resultMap,敏感字段需要指定typeHandler-->
<resultMap id="BaseResultMap" type="com.mediway.oa.user.model.entity.Staff">
<id column="id" jdbcType="VARCHAR" property="id" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="phone" jdbcType="VARCHAR" property="phone" typeHandler="com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="gender" jdbcType="VARCHAR" property="gender" />
<result column="description" jdbcType="VARCHAR" property="description" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="name" jdbcType="VARCHAR" property="name" typeHandler="com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler"/>
<result column="org_id" jdbcType="VARCHAR" property="orgId" />
</resultMap>
<!--查询员工-->
<select id="testMybatisSelect" resultMap="BaseResultMap">
select * from staff
where phone = #{phone,typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler}
and name = #{name,typeHandler=com.mediway.hos.database.encryption.handler.DataBaseEncryptTypeHandler}
</select>
StaffController中分别创建新增和查询接口
/**
* 新增员工
*
* @param staff 员工对象
* @return
*/
@PostMapping("/testMybatisInsert")
public BaseResponse testMybatisInsert(@RequestBody Staff staff) {
return BaseResponse.success(staffMapper.testMybatisInsert(staff));
}
/**
* 查询员工
*
* @param name 员工姓名
* @param phone 员工手机号
* @return 员工列表
*/
@GetMapping("/testMybatisSelect")
public BaseResponse testMybatisSelect(@RequestParam("name") String name, @RequestParam("phone") String phone) {
List<Staff> staff = staffMapper.testMybatisSelect(name, phone);
return BaseResponse.success(staff);
}
4.请求新增员工接口
url:http://localhost:8367/user/staff/testMybatisInsert
method:POST
请求参数
{
"id":"e2ab1960cd737111154d46878b5bccbb",
"name":"丽丽",
"gender":"女",
"age":39,
"email":"lili@qq.com",
"phone":"13201122222",
"orgId":"22",
"description":"lili"
}
请求结果
{
"code": "200",
"msg": "success",
"data": 1,
"success": true
}
数据库中效果,可以看到phone和name已经是密文存储
5.请求员工查询接口
url:http://localhost:8367/user/staff/testMybatisSelect?name=丽丽&phone=13201122222
method:GET
请求结果
{
"code": "200",
"msg": "success",
"data": [
{
"id": "e2ab1960cd737111154d46878b5bccbb",
"createTime": "2022-04-14 16:09:14",
"updateTime": "2022-04-14 16:09:14",
"current": null,
"size": null,
"name": "丽丽",
"gender": "女",
"age": 39,
"orgId": "22",
"email": "lili@qq.com",
"phone": "13201122222",
"description": "lili",
"isDeleted": null
}
],
"success": true
}
可以看到成功查询到了该条数据,并且返回的是解密后的明文数据。
6.演示MybatisPlus,在员工实体类上添加注解@TableName(autoResultMap = true)
@ApiModel("员工信息")
@TableName(value = "`staff`", autoResultMap = true)
@Data
public class Staff extends BaseEntity {
}
在姓名和手机号字段上分别添加注解@TableField(typeHandler = DataBaseEncryptTypeHandler.class)
/**
* 姓名
*/
@ApiModelProperty(value = "`姓名`", dataType = "`String`", example = "")
@TableField(value = "`name`",typeHandler = DataBaseEncryptTypeHandler.class)
private String name;
/**
* 手机号
*/
@ApiModelProperty(value = "`手机号`", dataType = "`String`", example = "")
@TableField(value = "`phone`",typeHandler = DataBaseEncryptTypeHandler.class)
private String phone;
7.请求新增员工接口
url:http://localhost:8367/user/staff/insert
method:POST
请求参数
{
"name":"苗苗",
"gender":"女",
"age":23,
"email":"miaomiao@qq.com",
"phone":"13201121111",
"description":"miaomiao"
}
请求结果
{
"code": "200",
"msg": "success",
"data": null,
"success": true
}
数据库中效果,可以看到phone和name已经是密文存储
5.请求员工查询接口
url:http://localhost:8367/user/staff/selectOne?name=苗苗
method:GET
请求结果
{
"code": "200",
"msg": "success",
"data": {
"id": "f1817fbe37fbeb8888f30e058e9b25da",
"createTime": "2022-04-14 16:27:27",
"updateTime": "2022-04-14 16:30:40",
"current": null,
"size": null,
"name": "苗苗",
"gender": "女",
"age": 23,
"orgId": null,
"email": "miaomiao@qq.com",
"phone": "13201121111",
"description": "miaomiao",
"isDeleted": 0
},
"success": true
}
可以看到成功查询到了该条数据,并且返回的是解密后的明文数据。
# 配置文件脱敏
# 概述
对yml或properties文件中的敏感配置项(如数据库用户名密码、redis密码等),进行脱敏处理,可以有效防止企业内部对隐私数据的滥用,防止隐私数据在未经脱敏的情况下从企业流出,满足企业既要保护隐私数据,同时又保持监管合规,满足企业合规性。
# 使用步骤
- 引入依赖
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
</dependency>
- 在配置文件中设置加解密秘钥
jasypt:
encryptor:
password: qsakjdnfij234234sdf67
algorithm: PBEWITHHMACSHA512ANDAES_128
在开发环境中,可以将秘钥放在配置文件中,
在生产环境中,建议通过环境变量或启动参数等动态方式传入秘钥,否则依然存在泄漏风险。
- 通过jasypt-1.9.3.jar进行加解密, cd到jasypt-1.9.3.jar的所在目录,执行下面示例中的命令即可完成加解密。
参数解释:algorithm
为加密算法,必须为PBEWITHHMACSHA512ANDAES_128
password
为加解密时的钥匙,必须和配置文件中的jasypt.encryptor.algorithm
保持一致input
输入要加解密的字符串
命令行加密:
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI ivGeneratorClassName=org.jasypt.iv.RandomIvGenerator algorithm="PBEWITHHMACSHA512ANDAES_128" password="qsakjdnfij234234sdf67" input="root"
命令行解密:
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringDecryptionCLI ivGeneratorClassName=org.jasypt.iv.RandomIvGenerator algorithm="PBEWITHHMACSHA512ANDAES_128" password="qsakjdnfij234234sdf67" input="c/TmGK59nvSmDeZ/C98m0dxuYqvyrOdmAQCkILghYclEqE8SdNO5ouape0unGO9V"
运行示例图如下:
该加密工具对同一明文加密的结果时不固定的,
- 在配置文件中将需要脱敏的value值替换成预先经过加密的内容ENC(XXX)
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql://Ip:port/demo
username: ENC(P6KG/VZpFh3FhL/hMKpKz67k9ct13xi9uIOW6Y65x3SKiCdCfkD6Tp04sOvCROJq)
password: ENC(b+gymNI26uhK0o1ReQfKpZPeuKqEU5OeX4GxGziP02nOWTtKcHgOauVVcFf6C+61)
ENC(XXX)格式主要为了便于识别该值是否需要解密,如不按照该格式配置,在加载配置项的时候将保持原值,不进行解密。