上次 《前后端 API 交互如何保证数据安全性?》 文章中, 我们介绍了如何在 Spring Boot 框架中去统一处理数据的加解密. 对于请求的加密也只做了 POST 请求的自动加密, 今天接着上文来继续介绍 GET 请求的安全性如何保证?
首先我们来看一个简单的 GET 请求:
http://cxytiandi.com/user?name=yinjihuan
首先很明显的是我们可以看到 name 参数是明文的, 如果对安全性要求很高, 建议查询也用 POST 请求, 前面我们对所有 POST 请求的参数都做了加密操作.
无论是 GET 还是 POST 都可以做签名
明文没关系, 关键是这个请求我复制到浏览器中打开, 把 name 改成别的值, 如果真的存在的话也是能返回结果的. 问题就在这, 参数被修改了, 后端无法识别, 这是第一个问题.
第二个问题是这个请求可以无限的使用, 就是你明天去请求这个地址它还能返回结果, 这个其实也需要控制下, 当然控制的方式有很多种, 今天我们会介绍一种比较简单的方式来控制.
第一种方式
参数中加签名, 前后端约定一个 key, 将参数按照字母排序拼接成一个字符串, 然后拼接上 key, 最后用 MD5 或者 SHA 进行加密, 最后得到一个加密的签名, 作为参数传到后端进行验证.
比如:
name=yinjihuan&sign=MD5(name=yinjihuan+key)
后端我们可以统一在过滤器中进行验证, 取得参数 sign 的值, 取得请求的所有参数, 同时也按照前端生成 sign 的方式生成一个新的 sign, 对两个 sign 进行比较, 如果相等, 就证明参数没有被篡改.
为了防止一个请求被多次使用, 我们通常会再 sign 中加上请求那刻的时间戳, 服务器这边会判断时间差, 如果在 10 分钟内可以让它继续执行, 当然这个 10 分钟你可以自己去调整, 长一点主要是为了方式客户端和服务器时间不一样的问题, 当然这种情况不能完全避免.
第二种方式
第二种方式比较简单, 因为我们前面讲过了请求的数据加解密, 既然我们有了加密的 key 和加密算法, 其实完全可以将签名的内容用我们的加密算法进行加密, 上面用的 md5 方式不是很安全, md5 是可以被破解的.
同时因为我这边用的 axios 来请求数据, 可以使用请求拦截器, 在请求之前统一对请求进行签名操作, 不用在每个地方单独去处理.
在使用 get 请求时, 我们用下面的方式:
- axios.get('/user', {
- params: {
- ID: 12345
- }
- })
- .then(function (response) {
- console.log(response);
- })
- .catch(function (error) {
- console.log(error);
- });
然后在请求拦截器中我们可以通过 params 就可以获取当前请求的所有参数信息, 这边我们不采用拼接的方式, 直接往 params 中添加一个 signTime(签名时间), 然后用对整个 params 进行加密得到一个 sign, 通过请求头传递到后台.
此时到后台的数据就是参数信息 + 签名时间, 比如:{name:"yjh",signTime:19210212121212}, 签名就是 {name:"yjh",signTime:19210212121212} 加密的内容.
- // 添加请求拦截器
- axios.interceptors.request.use(function (config) {
- // 在发送请求之前做些什么
- if (config.method == "get"){
- var newParams = config.params;
- console.log(newParams);
- if (newParams == undefined) {
- newParams = new Object();
- }
- newParams.signTime = new Date().getTime();
- config.headers.sign = EncryptData(JSON.stringify(newParams));
- console.log(JSON.stringify(config));
- }
- if (config.method == "post"){
- var newParams = new Object();
- newParams.signTime = new Date().getTime();
- config.headers.sign = EncryptData(JSON.stringify(newParams));
- }
- return config;
- }, function (error) {
- // 对请求错误做些什么
- return Promise.reject(error);
- });
后端可以在过滤器中进行签名校验, 代码如下:
- /**
- * 请求签名验证过滤器 < br>
- *
- * 请求头中获取 sign 进行校验, 判断合法性和是否过期 < br>
- *
- * sign = 加密({参数: 值, 参数 2: 值 2, signTime: 签名时间戳})
- * @author yinjihuan
- *
- * @about http://cxytiandi.com/about
- *
- */
- public class SignAuthFilter implements Filter {
- private EncryptProperties encryptProperties;
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- ServletContext context = filterConfig.getServletContext();
- ApplicationContext ctx = webApplicationContextUtils.getWebApplicationContext(context);
- encryptProperties = ctx.getBean(EncryptProperties.class);
- }
- @SuppressWarnings("unchecked")
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- HttpServletRequest req = (HttpServletRequest) request;
- HttpServletResponse resp = (HttpServletResponse) response;
- if (req.getMethod().equals("OPTIONS")) {
- chain.doFilter(request, response);
- return;
- }
- resp.setCharacterEncoding("UTF-8");
- String sign = req.getHeader("sign");
- if (!StringUtils.hasText(sign)) {
- PrintWriter print = resp.getWriter();
- print.write("非法请求: 缺少签名信息");
- return;
- }
- try {
- String decryptBody = AesEncryptUtils.aesDecrypt(sign, encryptProperties.getKey());
- Map<String, Object> signInfo = JsonUtils.getMapper().readValue(decryptBody, Map.class);
- Long signTime = (Long) signInfo.get("signTime");
- // 签名时间和服务器时间相差 10 分钟以上则认为是过期请求, 此时间可以配置
- if ((System.currentTimeMillis() - signTime)> encryptProperties.getSignExpireTime() * 60000) {
- PrintWriter print = resp.getWriter();
- print.write("非法请求: 已过期");
- return;
- }
- // POST 请求只处理时间
- // GET 请求处理参数和时间
- if(req.getMethod().equals(HttpMethod.GET.name())) {
- Set<String> paramsSet = signInfo.keySet();
- for (String key : paramsSet) {
- if (!"signTime".equals(key)) {
- String signValue = signInfo.get(key).toString();
- String reqValue = req.getParameter(key).toString();
- if (!signValue.equals(reqValue)) {
- PrintWriter print = resp.getWriter();
- print.write("非法请求: 参数被篡改");
- return;
- }
- }
- }
- }
- } catch (Exception e) {
- PrintWriter print = resp.getWriter();
- print.write("非法请求:" + e.getMessage());
- return;
- }
- chain.doFilter(request, response);
- }
- @Override
- public void destroy() {
- }
- }
首先我们会对签名信息进行判断, 没有则拦截掉, 然后进行解密操作, 得到签名时间, 判断有效期, 最后再根据解密得到的参数信息, 循环去和当前请求中的参数进行比较, 只要有一个对不上, 那就是参数被篡改了, 这边我做的比较简单, 对值的判断都转成字符串来比较, 不确定在一些特殊数据类型是否有问题, 大家可以自己去改.
验证的代码我也封装到了我的那个 spring-boot-starter-encrypt 中(欢迎 Star):https://github.com/yinjihuan/spring-boot-starter-encrypt
只需要配置过滤器即可:
- /**
- * 注册签名验证过滤器
- * @return
- */
- @Bean
- public FilterRegistrationBean signAuthFilter() {
- FilterRegistrationBean registrationBean = new FilterRegistrationBean();
- registrationBean.setFilter(new SignAuthFilter());
- registrationBean.setUrlPatterns(Arrays.asList("/rest/*"));
- return registrationBean;
- }
以上代码大家不一定能用到, 我个人认为没必要复制我的代码去实验, 理解思路才是你最佳的选择. 简单分享, 勿喷哈...........
来源: http://www.tuicool.com/articles/Bv2eYzu