接口安全设计
1、防伪装攻击(接口防刷)
设计初衷:不法分子伪装成正常用户,对接口进行超频访问,对应需要对接口设计防刷,即限制某段时间内访问次数。
q:服务器如何区分非正常用户?
a:对不同性质的用户不同处理,这里的性质就是“访问频率”,让服务器限制超频访问的用户持续调用。
技术方案:
限制某段时间说明对应限制措施,或者说相关技术应该具备时效性;访问次数对应的计数就行,那对应的用户可能很多,我们需要不同对待用户,也就是每个用户都需要计数措施,相对应的这种数据不敏感但数据量又可能很大的话可以使用redis;
限制访问涉及技术就是filter过滤器和interceptor拦截器,对于springboot项目的话选择springmvc提供的interceptor拦截器即可。
q:决定使用redis的话,就需要考虑key的设计,以及value的类型选择?
a:解决接口防刷,涉及的角色有两个,用户和接口,对应的计数措施value就选择String类型即可。将关注点放回key的设计,我们相应的就是关联用户和接口并做到唯一性,最简单的就是将用户标识和接口标识进行拼接设计为key即可。而其中用户标识就是ip地址(登录用户也可以),接口标识就是对应的接口访问路径URL,所以我们对应key设计就是:ip:url。
代码实现:
创建拦截器并配置
/**
* 防刷拦截器
*/
public class BrushProofInterceptor implements HandlerInterceptor {
@Autowired
private ISecurityRedisService securityRedisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(!(handler instanceof HandlerMethod)){
return true;
}
//防刷验证
String url = request.getRequestURI().substring(1);
String ip = RequestUtil.getIPAddress();
String key = RedisKeys.BRUSH_PROOF.join(url, ip);
if(!securityRedisService.isAllowBrush(key)){ //超过十次,true,拦截
response.setContentType("text/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(JsonResult.error(500, "请勿频繁访问","谢谢咯")));
return false;
}
return true;
}
}
redis业务层
@Service
public class SecurityRedisServiceImpl implements ISecurityRedisService {
@Autowired
private StringRedisTemplate template;
@Override
public boolean isAllowBrush(String key) {
//redis setnx操作,如果存在对应key则不做任何操作,如果没有则添加,对应就是一分钟内限制访问十次
template.opsForValue().setIfAbsent(key, "10", RedisKeys.BRUSH_PROOF.getTime(), TimeUnit.SECONDS);
Long decrement = template.opsForValue().decrement(key); //自减1,看个人判定
return decrement >= 0; //超过十次,也即计数10归0,返回true
}
}
启动类中配置拦截器和拦截规则
@Bean
public BrushProofInterceptor brushProofInterceptor(){
return new BrushProofInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//防刷
registry.addInterceptor(brushProofInterceptor())
.addPathPatterns("/**");
}
注意:如果是针对某个或者部分请求进行拦截防刷的话,就对应自定义注解结合拦截器使用即可。
2、接口防篡改(参数篡改)
设计初衷:用户正常访问的请求,可能会被不法分子拦截,对应篡改参数后放行。例如,某用户在浏览器发起转账请求,不法分子通过拦截用户请求,然后将转账对象改成自己,钱就被不法分子收取了(当然现实中不是这么简单的拦截、篡改、放行就行了,这里仅举例说明)。
q:接口如何鉴别参数被篡改了?
a:拿到最初请求前的参数,与服务器接收到的参数做对比,通过某种判断依据判断参数是否被改变。
技术方案:
使用参数签名,所谓参数签名就是对请求参数的整体描述。
①客户端发起请求之前,将所有参数使用某种算法拼接成字符串,然后对应将字符串进行加密算法加密,得到一个参数签名(sign_client);
②客户端正式发起请求,将所有参数和整理得到的参数签名一并传到服务器;
③服务器获取所有参数包括参数签名,用相同的算法将参数拼接成字符串,再用相同的加密算法加密,得到另一个参签名(sign_server);
④判断两个参数签名是否一致,一致则放行,否则拦截。
代码实现:
前端js代码,封装参数签名,该方法是自定义ajaxGet请求中定义的,目的是将每个请求中的参数都同一使用参数签名
//数据防篡改
function getSignStr(param) {
var sdic=Object.keys(param).sort(); //对请求参数排序
var signStr = "";
for(var i in sdic){ //自定义拼接字符串
if(i == 0){
signStr +=sdic[i]+"="+param[sdic[i]];
}else{
signStr +="&"+sdic[i]+"="+param[sdic[i]];
}
}
console.log(hex_md5(signStr)); //使用md5加密
return hex_md5(signStr).toUpperCase(); //转换成大写字母
}
创建拦截器
/**
* 签名拦截(防篡改)
*/
public class SignInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(!(handler instanceof HandlerMethod)){ //放行如跨域类型、静态资源等请求
return true;
}
//签名验证
Map<String, String[]> map = request.getParameterMap(); //使用String[]是因为一个参数的参数值可能不止一个
Set<String> keys = map.keySet();
Map<String, Object> param = new HashMap<>();
for (String s : map.keySet()) {
if("sign".equalsIgnoreCase(s)){
continue;
}
param.put(s, arrayToString(map.get(s))); //自定义将数组转成字符串的方法
}
String signatures = Md5Utils.signatures(param); //自定义signatures拼接、加密成sign_client
String sign = request.getParameter("sign"); //sign_server
if(sign == null || !sign.equalsIgnoreCase(signatures)){
response.setContentType("text/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new JsonResult(501,"签名校验失败","不好意思咯")));
return false;
}
return true;
}
}
测试的话可以发起一次成功的AjaxGet请求,然后在地址栏将方法签名保存下来,然后更改任意参数值,看对应的请求是否被拦截
注意:
既然我们通过算法设计参数签名,因此对应的算法不能暴露出去。但是前端控制台中是可以找到对应的js文件,然后找到对应的js代码,算法一览无余。因此我们需要对js文件进行加密操作,可以使用js在线加密工具或者其他加密工具对整个js文件加密(该加密工具不能完全解密原js代码),注意不要部分加密,防止整个js无法运行。
演示中的设计方式不适用于文件上传操作,因为文件上传会转换成流,request.getParameterMap()方法是无法识别流的,因此文件上传需要区别处理,如果参数很多,特别是参数体积很大时(比如参数传了一篇文章内容),拼接字符串和加密过程非常麻烦,对应的出现的误差就很多,匹配参数签名的时候就不准确了。
3、接口时效性
设计初衷:一般是业务需求和安全起见,在约定时间内请求有效,超时无效。也即前端发起请求到后台接收请求的某个时间段内,放行用户访问,超时之后拦截请求。
使用场景:微信支付二维码、地铁乘车码等
实现技术:参数签名+有效时间
实现方案就是综合接口防刷和参数篡改方案,使用redis计时拦截器拦截,参数签名是防止用户拦截请求篡改时间
技术方案:
①客户端发起请求之前,另外再添加设计一个时间参数,参数值就是当前时间(从前端开始发起请求的时间),将所有参数使用某种算法拼接成字符串,然后对应将字符串进行加密算法加密,得到一个参数签名(sign_client);
②客户端正式发起请求,将所有参数和整理得到的参数签名一并传到服务器;
③服务器获取所有参数包括参数签名,用相同的算法将参数拼接成字符串,再用相同的加密算法加密,得到另一个参签名(sign_server);
④判断两个参数签名是否一致,一致则放行,否则拦截;
⑤拿到时间参数,跟后端接收到时间(就是当前时间new Date()的结果),计算两个时间差,如果超过约定时间就拦截请求,否则放行。
4、接口加密(https协议)
所谓接口加密即对请求参数和重要资源标识符URI进行加密,一般我们不自行对参数、URI进行封装加密,而是使用https加密协议。
q:如何在项目中使用https?
a:①申请SSL证书,SSL证书需要向SSL证书颁发机构申请(交钱就行),申请需要提供相关资料(如公司名称、域名、ip信息等)。申请通过之后,机构会“颁发"SSL证书,该证书内容包含加密算法、公钥、秘钥等;
②jdk自带keytool,它可以在生成本地局域网使用的SSL证书,详见keytool生成证书,其证书就是一个tomcat.keystore文件;
③向阿里云申请SSL证书,性质跟SSL证书颁发机构差不多。
具体流程:
①将SSL证书配置到项目的服务器中(如springboot项目就配置到application.properties文件中);
②浏览器第一次发起请求,服务器接收到请求后,将证书副本(不具有原件的私钥)响应到服务器;
③浏览器接收证书副本后,对证书进行识别和认证(浏览器无法认证的证书对应会有警告图标),确认证书有效后,发起信任通道建立请求,服务器根据浏览器的各种请求,对请求接收并建立互信通道(就是建立TCP的三次握手过程);
④互信通道建立后,浏览器发起请求时,使用证书公钥对参数加密,然后携带到服务器,服务器接收参数使用私钥解密,响应数据给浏览器时同样使用私钥加密,浏览器接收数据后使用公钥进行解密,然后渲染数据到页面。(该过程就是对应的浏览器使用公钥加密/解密进行请求和接收响应,服务器使用私钥加密/解密进行接收请求和响应)。
5、接口文档
q:什么是接口文档
a:在项目开发汇总,web项目的前后端是分离开发的。应用程序的开发,需要由前后端工程师共同定义接口,编写接口文档,之后大家都根据这个接口文档进行开发,到项目结束前都要一直维护(如果不是一次性交付的项目后续还得一直维护)。
q:为什么使用接口文档
a:①项目开发过程中前后端工程师有一个统一的文件进行沟通交流开发;
②项目维护中或者项目人员更迭的时候,方便后期人员查看、维护;
③后端提供了接口、接口参数、接口描述,前端直接在接口文档中看到,给什么要什么则自己决定。
接口文档类型
第一代:最简单的纯文档类型
第二代:如showdoc官网在线文档(不仅仅是文档)
第三代:如swagger2官网在线文档(二代上的优化)
springboot整合swagger2接口文档
依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
配置类配置
@Configuration
@EnableSwagger2
public class SwaggerConfig implements WebMvcConfigurer {
@Bean
public Docket productApi() {
//添加head参数start
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
pars.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2).select()
// 扫描的包路径
.apis(RequestHandlerSelectors.basePackage("cn.wolfcode.luowowo.controller"))
// 定义要生成文档的Api的url路径规则
.paths(PathSelectors.any())
.build()
.globalOperationParameters(pars)
// 设置swagger-ui.html页面上的一些元素信息。
.apiInfo(metaData());
}
private ApiInfo metaData() {
return new ApiInfoBuilder()
// 标题
.title("SpringBoot集成Swagger2")
// 描述
.description("骡窝窝项目接口文档")
// 文档版本
.version("1.0.0")
.license("Apache License Version 2.0")
.licenseUrl("https://www.apache.org/licenses/LICENSE-2.0")
.build();
}
//ui页面
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
访问
http://localhost:8080/swagger-ui.html(或者http://localhost:8080/doc.html)
Swagger常见注解
//@Api:用在类上,说明该类的作用
@Api(value = "用户资源",description = "用户资源控制器")
//@ApiOperation:用在方法上,说明方法的作用
@ApiOperation(value = "注册功能",notes = "其实就是新增用户")
//@ApiImplicitParams:用在方法上包含一组参数说明
//@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
//paramType:参数放在哪个地方
//header-->请求参数的获取
//query-->请求参数的获取
//path-->请求参数的获取(用于restful接口):
//body-->请求实体中
@ApiImplicitParams({
@ApiImplicitParam(value = "昵称",name = "nickName",dataType = "String",required = true),
@ApiImplicitParam(value = "邮箱",name = "email",dataType = "String",required = true),
@ApiImplicitParam(value = "密码",name = "password",dataType = "String",required = true)
})
//@ApiModel:描述一个Model的信息
//(这种一般用在post创建的时候,使用@RequestBody这样的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候)
//@ApiModelProperty:描述一个model的属性
@ApiModel(value="用户",description="平台注册用户模型")
@ApiModelProperty(value="昵称",name="nickName",dataType = "String",required = true)
//@ApiResponses:用于表示一组响应
/**@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息(200相应不写在这里面)
code:数字,例如400
message:信息,例如"请求参数没填好"
response:抛出的异常类**/
@ApiResponses({
@ApiResponse(code=200,message="用户注册成功")
})
//@ApiIgnore:贴在类上则将该接口对外隐藏
这是一个基于JeecgBoot(低代码平台)开发的web后台系统,其使用了Swagger2,这是后台的接口文档