在以SpringBoot开发后盾API接口时,会存在哪些接口不平安的因素呢?通常如何去解决的呢?本文次要介绍API接口有不平安的因素以及常见的保障接口平安的形式,重点实际如何对接口进行签名。@pdai
SpringBoot接口 - API接口有哪些不平安的因素?如何对接口进行签名?
筹备知识点
- API接口有哪些不平安的因素?
常见的保障接口平安的形式?
- AccessKey&SecretKey
- 认证和受权
- https
- 接口签名(加密)
实现案例
- 定义注解
- AOP拦挡
- Request封装
- 实现接口
- 接口测试
- 示例源码
- 更多内容
筹备知识点
倡议从接口整体的平安体系角度来了解,比方存在哪些不平安的因素,加密解密等知识点。
API接口有哪些不平安的因素?
这里从体系角度,简略列举一些不平安的因素:
开发者拜访凋谢接口
- 是不是一个非法的开发者?
多客户端拜访接口
- 是不是一个非法的客户端?
用户拜访接口
- 是不是一个非法的用户?
- 有没有权限拜访接口?
接口传输
- http明文传输数据?
其它方面
- 接口重放,上文介绍的接口幂等
- 接口超时,加timestamp管制?
- ...
常见的保障接口平安的形式?
针对上述接口存在的不平安因素,这里向你展现一些典型的保障接口平安的形式。
AccessKey&SecretKey
这种设计个别用在开发接口的平安,以确保是一个非法的开发者。
- AccessKey: 开发者惟一标识
- SecretKey: 开发者密钥
以阿里云相干产品为例
认证和受权
从两个视角去看
- 第一: 认证和受权,认证是访问者的合法性,受权是访问者的权限分级;
- 第二: 其中认证包含对客户端的认证以及对用户的认证;
- 对于客户端的认证
典型的是AppKey&AppSecret,或者ClientId&ClientSecret等
比方oauth2协定的client cridential模式
https://api.xxxx.com/token?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
grant_type参数等于client_credentials示意client credentials形式,client_id是客户端id,client_secret是客户端密钥。
返回token后,通过token拜访其它接口。
- 对于用户的认证和受权
比方oauth2协定的受权码模式(authorization code)和明码模式(resource owner password credentials)
https://api.xxxx.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID&scope=read
grant_type参数等于password示意明码形式,client_id是客户端id,username是用户名,password是明码。
(PS:password模式只有在受权码模式(authorization code)不可用时才会采纳,这里只是举个例子而已)
可选参数scope示意申请的权限范畴。(相干开发框架能够参考spring security, Apache Shiro,SA-Token等)
https
从接口传输平安的角度,避免接口数据明文传输, 具体能够看这里
HTTP 有以下安全性问题:
- 应用明文进行通信,内容可能会被窃听;
- 不验证通信方的身份,通信方的身份有可能遭逢假装;
- 无奈证实报文的完整性,报文有可能遭篡改。
HTTPs 并不是新协定,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPs 应用了隧道进行通信。
通过应用 SSL,HTTPs 具备了加密(防窃听)、认证(防假装)和完整性爱护(防篡改)。
接口签名(加密)
接口签名(加密),次要避免申请参数被篡改。特地是平安要求比拟高的接口,比方领取畛域的接口。
- 签名的次要流程
首先咱们须要调配给客户端一个私钥用于URL签名加密,个别的签名算法如下:
1、首先对申请参数按key进行字母排序放入有序汇合中(其它参数请参看后续补充局部);
2、对排序完的数组键值对用&进行连贯,造成用于加密的参数字符串;
3、在加密的参数字符串后面或者前面加上私钥,而后用加密算法进行加密,失去sign,而后随着申请接口一起传给服务器。
例如:
https://api.xxxx.com/token?ke...
服务器端接管到申请后,用同样的算法取得服务器的sign,比照客户端的sign是否统一,如果统一申请无效;如果不统一返回指定的错误信息。
- 补充:对什么签名?
- 次要包含申请参数,这是最次要的局部,签名的目标要避免参数被篡改,就要对可能被篡改的参数签名;
- 同时思考到申请参数的起源可能是申请门路path中,申请header中,申请body中。
- 如果对客户端调配了AppKey&AppSecret,也可退出签名计算;
- 思考到其它幂等,token生效等,也会将波及的参数一并退出签名,比方timestamp,流水号nonce等(这些参数可能来源于header)
- 补充: 签名算法?
个别波及这块,次要蕴含三点:密钥,签名算法,签名规定
- 密钥secret: 前后端约定的secret,这里要留神前端可能无奈妥善保留好secret,比方SPA单页利用;
- 签名算法:也不肯定要是对称加密算法,对称是反过来解析sign,这里是用同样的算法和规定计算出sign,并比照前端传过来的sign是否统一。
- 签名规定:比方屡次加盐加密等;
PS:有读者会问,咱们是可能从有些客户端获取密钥,算法和规定的(比方前端SPA单页利用生成的js中获取密钥,算法和规定),那么签名的意义在哪里?我认为签名是伎俩而不是目标,签名是加大攻击者攻打难度的一种伎俩,至多是能够抵御大部分简略的攻打的,再加上其它防备形式(流水号,工夫戳,token等)进一步晋升攻打的难度而已。
- 补充:签名和加密是不是一回事?
严格来说不是一回事:
- 签名是通过对参数依照指定的算法、规定计算出sign,最初前后端通过同样的算法计算出sign是否统一来避免参数篡改的,所以你能够看到参数是明文的,只是多加了一个计算出的sign。
- 加密是对申请的参数加密,后端进行解密;同时有些状况下,也会对返回的response进行加密,前端进行解密;这里存在加密和解密的过程,所以思路上必然是对称加密的模式+工夫戳接口时效性等。
- 补充:签名放在哪里?
签名能够放在申请参数中(path中,body中等),更为优雅的能够放在HEADER中,比方X-Sign(通常第三方的header参数以X-结尾)
- 补充:大厂开放平台是怎么做的呢?哪些能够借鉴?
以腾讯开放平台为例,请参考腾讯开放平台第三方利用签名参数sig的阐明
实现案例
本例子采纳AOP拦挡自定义注解形式实现,次要看实现的思路而已(签名的目标要避免参数被篡改,就要对可能被篡改的参数签名)。@pdai
定义注解
package tech.pdai.springboot.api.sign.config.sign;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * @author pdai */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Signature {}
AOP拦挡
这里能够看到须要对所有用户可能批改的参数点进行按规定签名
package tech.pdai.springboot.api.sign.config.sign;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.util.Map;import java.util.Objects;import javax.servlet.http.HttpServletRequest;import cn.hutool.core.text.CharSequenceUtil;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import org.springframework.util.CollectionUtils;import org.springframework.web.context.request.RequestAttributes;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.context.request.ServletWebRequest;import org.springframework.web.servlet.HandlerMapping;import org.springframework.web.util.ContentCachingRequestWrapper;import tech.pdai.springboot.api.sign.config.exception.BusinessException;import tech.pdai.springboot.api.sign.util.SignUtil;/** * @author pdai */@Aspect@Componentpublic class SignAspect { /** * SIGN_HEADER. */ private static final String SIGN_HEADER = "X-SIGN"; /** * pointcut. */ @Pointcut("execution(@tech.pdai.springboot.api.sign.config.sign.Signature * *(..))") private void verifySignPointCut() { // nothing } /** * verify sign. */ @Before("verifySignPointCut()") public void verify() { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); String sign = request.getHeader(SIGN_HEADER); // must have sign in header if (CharSequenceUtil.isBlank(sign)) { throw new BusinessException("no signature in header: " + SIGN_HEADER); } // check signature try { String generatedSign = generatedSignature(request); if (!sign.equals(generatedSign)) { throw new BusinessException("invalid signature"); } } catch (Throwable throwable) { throw new BusinessException("invalid signature"); } } private String generatedSignature(HttpServletRequest request) throws IOException { // @RequestBody String bodyParam = null; if (request instanceof ContentCachingRequestWrapper) { bodyParam = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), StandardCharsets.UTF_8); } // @RequestParam Map<String, String[]> requestParameterMap = request.getParameterMap(); // @PathVariable String[] paths = null; ServletWebRequest webRequest = new ServletWebRequest(request, null); Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (!CollectionUtils.isEmpty(uriTemplateVars)) { paths = uriTemplateVars.values().toArray(new String[0]); } return SignUtil.sign(bodyParam, requestParameterMap, paths); }}
Request封装
package tech.pdai.springboot.api.sign.config;import java.io.IOException;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import lombok.NonNull;import lombok.extern.slf4j.Slf4j;import org.springframework.web.filter.OncePerRequestFilter;import org.springframework.web.util.ContentCachingRequestWrapper;@Slf4jpublic class RequestCachingFilter extends OncePerRequestFilter { /** * This {@code doFilter} implementation stores a request attribute for * "already filtered", proceeding without filtering again if the * attribute is already there. * * @param request request * @param response response * @param filterChain filterChain * @throws ServletException ServletException * @throws IOException IOException * @see #getAlreadyFilteredAttributeName * @see #shouldNotFilter * @see #doFilterInternal */ @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { boolean isFirstRequest = !isAsyncDispatch(request); HttpServletRequest requestWrapper = request; if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) { requestWrapper = new ContentCachingRequestWrapper(request); } try { filterChain.doFilter(requestWrapper, response); } catch (Exception e) { e.printStackTrace(); } }}
注册
package tech.pdai.springboot.api.sign.config;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class FilterConfig { @Bean public RequestCachingFilter requestCachingFilter() { return new RequestCachingFilter(); } @Bean public FilterRegistrationBean requestCachingFilterRegistration( RequestCachingFilter requestCachingFilter) { FilterRegistrationBean bean = new FilterRegistrationBean(requestCachingFilter); bean.setOrder(1); return bean; }}
实现接口
package tech.pdai.springboot.api.sign.controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import tech.pdai.springboot.api.sign.config.response.ResponseResult;import tech.pdai.springboot.api.sign.config.sign.Signature;import tech.pdai.springboot.api.sign.entity.User;/** * @author pdai */@RestController@RequestMapping("user")public class SignTestController { @Signature @PostMapping("test/{id}") public ResponseResult<String> myController(@PathVariable String id , @RequestParam String client , @RequestBody User user) { return ResponseResult.success(String.join(",", id, client, user.toString())); }}
接口测试
body参数
如果不带X-SIGN
如果X-SIGN谬误
如果X-SIGN正确
示例源码
https://github.com/realpdai/t...
更多内容
辞别碎片化学习,无套路一站式体系化学习后端开发: Java 全栈常识体系(https://pdai.tech)