栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java业务开发之安全问题

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java业务开发之安全问题

安全问题

任何客户端传过来的数据都是不能直接信任的

HTTP请求,任何客户端传过来的数据都是不能直接信任的,不能信任请求头与请求体里的任何内容。客户端传给服务端的数据只是信息收集,数据需要经过有效性验证、权限验证等后才能使用,并且这些数据只能认为是用户操作的意图,不能直接代表数据当前的状态。

如果接口面向内部服务,由服务调用方传入用户 ID 没什么不合理,但是这样的接口不能直接开放给客户端或 H5 使用。如果你的接口直面用户(比如给客户端或 H5 页面调用),那么一定需要用户先登录才能使用。登录后用户标识保存在服务端,接口需要从服务端(比如 Session 中)获取(sessionId)。

Spring Validation

@Validated

public class TrustClientParameterController {

  @PostMapping("/better")

    @ResponseBody

    public String better(

            @RequestParam("countryId")

            @Min(value = 1, message = "非法参数")

            @Max(value = 3, message = "非法参数") int countryId) {

        return allCountries.get(countryId).getName();

    }

}

@PostMapping("/orderRight")

public void right(@RequestBody Order order) {

    //根据ID重新查询商品

    Item item = Db.getItem(order.getItemId());

    //客户端传入的和服务端查询到的商品单价不匹配的时候,给予友好提示

    if (!order.getItemPrice().equals(item.getItemPrice())) {

        throw new RuntimeException("您选购的商品价格有变化,请重新下单");

    }

    //重新设置商品单价

    order.setItemPrice(item.getItemPrice());

    //重新计算商品总价

    BigDecimal totalPrice = item.getItemPrice().multiply(BigDecimal.valueOf(order.getQuantity()));

    //客户端传入的和服务端查询到的商品总价不匹配的时候,给予友好提示

    if (order.getItemTotalPrice().compareTo(totalPrice)!=0) {

        throw new RuntimeException("您选购的商品总价有变化,请重新下单");

    }

    //重新设置商品总价

    order.setItemTotalPrice(totalPrice);

    createOrder(order);

}

Spring Wed技巧

自定义注解 @LoginRequired

@GetMapping("right")

public String right(@LoginRequired Long userId) {

    return "当前用户Id:" + userId;

}

注解:

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.PARAMETER)

@documented

public @interface LoginRequired {

    String sessionKey() default "currentUser";

}

@Slf4j

public class LoginRequiredArgumentResolver implements HandlerMethodArgumentResolver {

    //解析哪些参数

    @Override

    public boolean supportsParameter(MethodParameter methodParameter) {

        //匹配参数上具有@LoginRequired注解的参数

        return methodParameter.hasParameterAnnotation(LoginRequired.class);

    }

    @Override

    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {

        //从参数上获得注解

        LoginRequired loginRequired = methodParameter.getParameterAnnotation(LoginRequired.class);

        //根据注解中的Session Key,从Session中查询用户信息

        Object object = nativeWebRequest.getAttribute(loginRequired.sessionKey(), NativeWebRequest.SCOPE_SESSION);

        if (object == null) {

            log.error("接口 {} 非法调用!", methodParameter.getMethod().toString());

            throw new RuntimeException("请先登录!");

        }

        return object;

    }

}

最后,实现 WebMvcConfigurer 接口的 addArgumentResolvers 方法,来增加这个自定义的处理器 LoginRequiredArgumentResolver:

SpringBootApplication

public class CommonMistakesApplication implements WebMvcConfigurer {

...

    @Override

    public void addArgumentResolvers(List resolvers) {

        resolvers.add(new LoginRequiredArgumentResolver());

    }

}

任何涉及钱与资源的代码必须要考虑防刷、限量和防重,要做好安全兜底。

短信验证码

对于短信验证码,有如下 4 种可行的方式来防刷。

第一种方式,只有固定的请求头才能发送验证码。也就是说,我们通过请求头中网页或 App 客户端传给服务端的一些额外参数,来判断请求是不是 App 发起的。其实,这种方式“防君子不防小人”。比如,判断是否存在浏览器或手机型号、设备分辨率请求头。对于那些使用爬虫来抓取短信接口地址的程序来说,往往只能抓取到 URL,而难以分析出请求发送短信还需要的额外请求头,可以看作第一道基本防御。

第二种方式,只有先到过注册页面才能发送验证码。对于普通用户来说,不管是通过 App 注册还是 H5 页面注册,一定是先进入注册页面才能看到发送验证码按钮,再点击发送。我们可以在页面或界面打开时请求固定的前置接口,为这个设备开启允许发送验证码的窗口,之后的请求发送验证码才是有效请求。这种方式可以防御直接绕开固定流程,通过接口直接调用的发送验证码请求,并不会干扰普通用户。

第三种方式,控制相同手机号的发送次数和发送频次。除非是短信无法收到,否则用户不太会请求了验证码后不完成注册流程,再重新请求。因此,我们可以限制同一手机号每天的最大请求次数。验证码的到达需要时间,太短的发送间隔没有意义,所以我们还可以控制发送的最短间隔。比如,我们可以控制相同手机号一天只能发送 10 次验证码,最短发送间隔 1 分钟。

第四种方式,增加前置图形验证码。短信轰炸平台一般会收集很多免费短信接口,一个接口只会给一个用户发一次短信,所以控制相同手机号发送次数和间隔的方式不够有效。这时,我们可以考虑对用户体验稍微有影响,但也是最有效的方式作为保底,即将弹出图形验证码作为前置。除了图形验证码,我们还可以使用其他更友好的人机验证手段(比如滑动、点击验证码等),甚至是引入比较新潮的无感知验证码方案(比如,通过判断用户输入手机号的打字节奏,来判断是用户还是机器),来改善用户体验。此外,我们也可以考虑在监测到异常的情况下再弹出人机检测。比如,短时间内大量相同远端 IP 发送验证码的时候,才会触发人机检测。总之,我们要确保,只有正常用户经过正常的流程才能使用开放平台资源,并且资源的用量在业务需求合理范围内。此外,还需要考虑做好短信发送量的实时监控,遇到发送量激增要及时报警。

资产(比如优惠券、积分)的申请需要理由,甚至需要走流程,这样才可以追溯是什么活动需要、谁提出的申请,程序依据申请批次来发放。

在业务需要发放优惠券的时候,先申请批次,然后再通过批次发放优惠券:

@GetMapping("right")

public int right() {

    CouponCenter couponCenter = new CouponCenter();

    //申请批次   

    CouponBatch couponBatch = couponCenter.generateCouponBatch();

    IntStream.rangeClosed(1, 10000).forEach(i -> {

        Coupon coupon = couponCenter.generateCouponRight(1L, couponBatch);

        //发放优惠券

        couponCenter.sendCoupon(coupon);

    });

    return couponCenter.getTotalSentCoupon();

}

可以看到,generateCouponBatch 方法申请批次时,设定了这个批次包含 100 张优惠券。在通过 generateCouponRight 方法发放优惠券时,每发一次都会从批次中扣除一张优惠券,发完了就没有了:

public Coupon generateCouponRight(long userId, CouponBatch couponBatch) {

    if (couponBatch.getRemainCount().decrementAndGet() >= 0) {

        return new Coupon(userId, couponBatch.getAmount());

    } else {

        log.info("优惠券批次 {} 剩余优惠券不足", couponBatch.getId());

        return null;

    }

}

public CouponBatch generateCouponBatch() {

    CouponBatch couponBatch = new CouponBatch();

    couponBatch.setAmount(new BigDecimal("100"));

    couponBatch.setId(1L);

    couponBatch.setTotalCount(new AtomicInteger(100));

    couponBatch.setRemainCount(couponBatch.getTotalCount());

    couponBatch.setReason("XXX活动");

    return couponBatch;

}

防重、防重,钱的进出一定要和订单挂钩并且实现幂等涉及钱的进出

需要做好以下两点。

  • 任何资金操作都需要在平台侧生成业务属性的订单,一定是先有订单再去做资金操作。同时,订单的产生需要有业务属性。业务属性是指,订单不是凭空产生的,否则就没有控制的意义。比如,返现发放订单必须关联到原先的商品订单产生;再比如,借款订单必须关联到同一个借款合同产生。
  • 一定要做好防重,也就是实现幂等处理,并且幂等处理必须是全链路的。这里的全链路是指,从前到后都需要有相同的业务订单号来贯穿,实现最终的支付防重。

//错误:每次使用UUID作为订单号

@GetMapping("wrong")

public void wrong(@RequestParam("orderId") String orderId) {

    PayChannel.pay(UUID.randomUUID().toString(), "123", new BigDecimal("100"));

}

//正确:使用相同的业务订单号

@GetMapping("right")

public void right(@RequestParam("orderId") String orderId) {

    PayChannel.pay(orderId, "123", new BigDecimal("100"));

}

//三方支付通道

public class PayChannel {

    public static void pay(String orderId, String account, BigDecimal amount) {

        ...

    }

}

SQL注入与XSS

使用sqlmap探索接口:

python sqlmap.py -u  http://localhost:45678/sqlinject/jdbcwrong --data name=test

对于 SQL 注入来说,使用参数化的查询是最好的堵漏方式;对于 JdbcTemplate 来说,我们可以使用“?”作为参数占位符;对于 MyBatis 来说,我们需要使用“#{}”进行参数化处理。

XSS:原本是让用户传入或输入正常数据的地方,被黑客替换为了 Javascript 脚本,页面没有经过转义直接显示了这个数据,然后脚本就被执行了。更严重的是,脚本没有经过转义 就保存到了数据库中,随后页面加载数据的时候,数据中混入的脚本又当做代码执行了。黑客可以利用这个漏洞来盗取敏感数据,诱骗用户访问钓鱼网站等。

第一,要从根本上、从最底层进行堵漏,尽量不要在高层框架层面做,否则堵漏可能不彻底。第二,堵漏要同时考虑进和出,不仅要确保数据存入数据库的时候进行了转义或过滤,还要在取出数据呈现的时候再次转义,确保万无一失。第三,除了直接堵漏外,我们还可以通过一些额外的手段限制漏洞的威力。比如,为 cookie 设置 Httponly 属性,来防止数据被脚本读取;又比如,尽可能限制字段的最大保存长度,即使出现漏洞,也会因为长度问题限制黑客构造复杂攻击脚本的能力。

保存和传输敏感数据

BCrypt加密算法

BCrypt保存密码,不要使用像 MD5 这样快速的摘要算法,而是使用慢一点的算法。比如 Spring Security 已经废弃了 MessageDigestPasswordEncoder,推荐使用 BCryptPasswordEncoder,也就是BCrypt来进行密码哈希。BCrypt 是为保存密码设计的算法,相比 MD5 要慢很多。

"password": "$2a$10$wPWdQwfQO2lMxqSIb6iCROXv7lKnQq5XdMO96iCYCj7boK9pk6QPC"

//格式为:$$$

  • BCrypt 把盐作为了算法的一部分,强制我们遵循安全保存密码的最佳实践。
  • 生成的盐和哈希后的密码拼在了一起:$是字段分隔符,其中第一个$后的 2a 代表算法版本第二个$后的 10 是代价因子(默认是 10,代表 2 的 10 次方次哈希),第三个$后的 22 个字符是盐,再后面是摘要。所以说,我们不需要使用单独的数据库字段来保存盐。
  • 代价因子的值越大,BCrypt 哈希的耗时越久。因此,对于代价因子的值,更建议的实践是,根据用户的忍耐程度和硬件,设置一个尽可能大的值。

对称加密算法,使用相同的密钥进行加密和解密。使用对称加密算法来加密双方的通信的话,双方需要先约定一个密钥,加密方才能加密,接收方才能解密。如果密钥在发送的时候被窃取,那么加密就是白忙一场。因此,这种加密方式的特点是,加密速度比较快,但是密钥传输分发有泄露风险。

非对称加密算法,一种密钥的保密的算法。公钥密码是由一对密钥对构成的,使用公钥或者说加密密钥来加密,使用私钥或者说解密密钥来解密,公钥可以任意公开,私钥不能公开。使用非对称加密的话,通信双方可以仅分享公钥用于加密,加密后的数据没有私钥无法解密。因此,这种加密方式的特点是,加密速度比较慢,但是解决了密钥的配送分发安全问题。

对称加密常用的加密算法有, DES、3DES 和 AES;非对称加密常用算法有,RSA。

注意:在业务代码中要避免使用 DES 加密。ECB 模式虽然简单,但是不安全,不推荐使用。

AES算法,除了 ECB 模式和 CBC 模式外,AES 算法还有 CFB、OFB、CTR 模式,你可以参考这里了解它们的区别。《实用密码学》一书比较推荐的是 CBC 和 CTR 模式。还需要注意的是,ECB 和 CBC 模式还需要设置合适的填充模式,才能处理超过一个分组的数据。对于敏感数据保存,除了选择 AES+ 合适模式进行加密外,我还推荐以下几个实践:不要在代码中写死一个固定的密钥和初始化向量,最好和之前提到的盐一样,是唯一、独立并且每次都变化的。推荐使用独立的加密服务来管控密钥、做加密操作,千万不要把密钥和密文存在一个数据库,加密服务需要设置非常高的管控标准。数据库中不能保存明文的敏感信息,但可以保存脱敏的信息。普通查询的时候,直接查脱敏信息即可。

对于数据保存:

用户密码不能加密保存,更不能明文保存,需要使用全球唯一的、具有一定长度的、随机的盐,配合单向散列算法保存。使用 BCrypt 算法,是一个比较好的实践。

诸如姓名和身份证这种需要可逆解密查询的敏感信息,需要使用对称加密算法保存。我的建议是,把脱敏数据和密文保存在业务数据库,独立使用加密服务来做数据加解密;对称加密需要用到的密钥和初始化向量,可以和业务数据库分开保存。

对于数据传输,则务必通过 SSL/TLS 进行传输。对于用于客户端到服务端传输数据的 HTTP,我们需要使用基于 SSL/TLS 的 HTTPS。对于一些走 TCP 的 RPC 服务,同样可以使用 SSL/TLS 来确保传输安全。

HTTPS

HTTPS TLS 1.2 连接(RSA 握手)的过程:

作为准备工作,网站管理员需要申请并安装 CA 证书到服务端。CA 证书中包含非对称加密的公钥、网站域名等信息,密钥是服务端自己保存的,不会在任何地方公开。

建立 HTTPS 连接的过程,首先是 TCP 握手,然后是 TLS 握手的一系列工作,包括:

  1. 客户端告知服务端自己支持的密码套件(比如

TLS_RSA_WITH_AES_256_GCM_SHA384,其中 RSA是密钥交换的方式,

AES_256_GCM是加密算法,SHA384是消息验证摘要算法),提供客户端随机数。

2.  服务端应答选择的密码套件,提供服务端随机数。

3.  服务端发送CA证书给客户端,客户端验证CA证书((后面详细说明)。

4.  客户端生成PreMasterKey,并使用非对称加密+公钥加密PreMasterKey.

5.  客户端把加密后的PreMasterKey传给服务端。

6.  服务端使用非对称加密+私钥解密得到 PreMasterKey,并使用PreMasterKey+两个

随机数,生成 MasterKey。

7.  客户端也使用PreMasterKey+两个随机数生成 MasterKey。

8.  客户端告知服务端之后将进行加密传输。

9.  客户端使用MasterKey配合对称加密算法,进行对称加密测试。

10.  服务端也使用MasterKey配合对称加密算法,进行对称加密测试。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/489619.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号