java常见bugs与安全漏洞
欢迎你来读这篇博客,这篇博客主要是关于java常见bug与安全漏洞及避免方法的介绍。
其中包括了关于我的见解和收集的知识分享。
序言
Bug
NPE 场景
- 调用了空对象的实例方法
- 访问空对象的属性
- 数组为空的时候,取长度
- 不能抛出空异常对象
- 方法的返回值是 null,调用方法直接使用
- 自动拆箱导致 NPE
- 空包装对象赋值给基本数据类型时
- 方法传参时
- 空对象进行比较大小
避免方法
- 使用之前初始化
- 避免返回 null
- 外部传值,一定要及时判断
- 基本数据类型优于包装类型,优先使用基本数据类型
- 不确定的包装类型,先校验后使用
- 对于 Null 值的包装类型,赋值为 0
字符串、数组、集合场景
- null 字符串调用 equals 方法
- 对象数组 new 出来,但元素没有初始化
- list 的 addAll 方法,传递 null
Optional
- 不能作为类字段使用,没有实现序列化接口
- 容器类,代表存在和不存在
- orElse(new Object()) 存在返回,空提供默认值
- orElseGet() 存在返回,空由函数去产生 // 配合单例
- orElseThrow 存在返回,空抛出异常
- ifPresent 存在处理
- map 执行操作后返回一个 optional 对象
- 解决了什么问题
- 使代码更加优雅简洁
- 减少 npe
- 解决不了什么问题
- 不具备传导性
- 何时使用
- Optional 的预期用途主要是作为返回类型。
- 何时不要使用
- 不要将其用作类中的字段,因为它不可序列化。如果确实需要序列化包含 Optional 值的对象,则 Jackson 库提供了将 Optionals 视为普通对象的支持。这意味着 Jackson 将空对象视为空,将具有值的对象视为包含该值的字段。
- 不要将其用作构造函数和方法的参数,因为这会导致不必要的复杂代码。
1 | |
Optional 源码与注释
1 | |
正确处理异常
- 使用异常,而不是返回码(或类似),因为异常会更加的详细
- 主动捕获检查性异常,并对异常信息进行反馈(日志或标记)
- 保持代码整洁,一个方法中不要有多个 try catch 或者嵌套的 try catch
- 捕获更加具体的异常,而不是通用的 Exception
- 合理的设计自定义的异常类
常见异常
- 并发修改:可迭代对象在遍历的同时做修改,则会报并发修改异常
- 类型转换:类型转换不符合 Java 的继承关系,则会报类型转换异常
- 枚举查找:枚举在查找时,如果枚举值不存在,不会返回空,而是直接抛出异常
资源管理
try-with-resources
java 计算、接口、集合
BigDecimal 精度问题
- 初始化设置精度需要匹配
- bigDecimal.setScale(2) 可大不能小
- bigDecimal.setScale(2,BigDecimal.ROUND_HALF_UP)
- 除法结果需要精度
- 数值比较需要精度匹配
日期
SimpleDataFormat
- 可以解析大于或等于它定义的时间精度,但不能解析小于它定义的精度
- 线程不安全,多线程会抛出异常
- 原因:内部有一个 calendar
- 解决方法:定义为局部变量、使用 ThreadLoacl、Synchronize
迭代问题
- for-each 优于 for
集合判等问题
- equals 和 hashcode 与其可能带来的潜在问题
- 类实现了 compareTo 方法,就需要实现 equals 方法
- compareTo 与 equals 的实现过程需要同步
Lombok
- @EqualsAndHashCode(callSuper=true)
- 命名问题
抽象类和接口
抽象类、接口的含义和特性
- 抽象类是子类的通用特性,包含了属性和行为;接口是定义行为,并不关心谁去实现
- 抽象类是对类本质的抽象,表达的是 is a 的关系;接口是对行为的抽象,表达的是 like a 的关系
抽象类、接口的相同点
- 接口中的方法(java8 改变了这一语法)和抽象类中的抽象方法都不能有方法体,并且必须在子类中实现
- 都可以被继承,但是不能被实例化
抽象类、接口的不同点
- 使用时语法不同,抽象类使用 extends,接口则使用 implements
- 接口中只能定义常量,所以,不能表达对象状态,而抽象类可以
- 接口中的方法必须是 public 类型的,而抽象类则没有限制
- 类可以同时实现多个接口(间接解决了 Java 不支持多继承的 we 难题),但是只能继承一个抽象类
默认方法与静态方法
lambda 与函数式接口
Stream 和 lambda 真的高效吗
迭代过多可能会导致计算低效
序列化
- 父类不可序列化,子类实现了序列化接口也不行
- 需要提供父类的默认无参构造器
- 类中存在引用对象
- 所有属性都是可序列化的才可以序列化
- 同一个对象多次序列化,直接有更新
- 不会重复序列化,会影响结果
泛型、反射、编译优化
泛型
玩转泛型的前提是理解泛型擦除。
泛型的特性
- 先检查再编译
- 泛型不支持继承
- 泛型的类型变量不能是基本类型
- 泛型的类型参数只能是类类型,不能是简单类型
使用原始类型,可能会带来灾难性后果 不声明泛型类型。主要是为了兼容旧代码。
反射
- 应用场景
- 开发通用框架
- 动态代理
- 注解
- 可扩展性功能
- 缺点
- 性能开销
- 破坏封装性
- 内部曝光
什么情况下反射获取不到 Method
- 当方法是基本类型时,反射获取 Method 的参数类型也必须一致
- 如果调用的方法属于当前对象的父类,那么 getDetlaredMethod 获取不到 Method
字符串
可以看我的Java日志和Java·奇技淫巧这两篇文章,有关于字符串拼接的讨论。
深拷贝与浅拷贝
Object 类的 clone()是浅拷贝
线程安全
synchronized
- 不会被继承,子类需要重新指定
- 标记位置:方法声明、方法体
- JDK 实现:对象头标记,字节码标记 monitor
- JDK 优化:偏向锁-轻量级锁-重量级锁
多线程原子更新变量值
- countDownLatch
- Atomic 系列
阻塞队列
阻塞队列:支持两个附加操作的队列。队列空则等待、队列满则等待。
| 方法 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
|---|---|---|---|---|
| 插入 | add | offer | put | offer(time) |
| 移除 | remove | poll | take | poll(time) |
| 检查 | element | peek | - | - |
copy-on-write 容器
主要目的是解决并发读写 List 异常
优点:并发读不需要加锁,提高进程的并发度
缺点:内存占用问题,一致性问题
应用场景:读多写少,如黑名单,白名单。
使用的时候,需要注意减少扩容开销,使用批量添加
线程池
TODO:自定义线程池,相关文章太多了,不再这里写了,有时间了,补充。
ThreadLocal
threadlocal 并不是来解决并发或者共享问题的。
使用问题:
- 不支持继承
- 如果不及时清理现场,会造成数据混乱。
Spring 常见的坑
Spring Bean 默认名称生成策略导致空指针
Spring 对 Class 前两个字母都是大写的,进行了特殊处理,把大写变成小写。但是默认策略是第一个字母会变成小写。
如果类名是 QWERService。GetBean(qWERService) 就找不到。
解决方案:
- 使用类型获取
- getBean 传正确
- 类名规避这种情况
- 根据类型获取 Bean getBean(QWERService.class)
使用@Autowired 依然空指针
- 属性对象注入了,但是对象没被注册成 bean
- 虽然标记成 SpringBean,属性也注入了,但是使用 new 去获取类对象。
- 正确做法应该是,Bean 的整个生命周期都应该被 Spring 容器管理
- Spring 的包扫描机制,没扫描到
不使用自动注入,如何获取上下文
- ApplicationContextInitializer
- ApplicationListener
- SpringApplication.run()的返回值
什么场景需要使用获取并使用上下文?
多线程下 SpringBean 的数据不符合预期怎么办
- 默认单例 Bean
- 如果有全局属性,造成线程间通信,如果不做线程安全,就会有问题。
- 优势
- 减少新生成实例的小号
- 减少 JVM 垃圾回收
- 快速获取 Bean
- 劣势
- 线程不安全
- 默认单例的理由
- 少创建实例
- 垃圾回收便捷
- 使用缓存快速获取
- 优势
- 如果有全局属性,造成线程间通信,如果不做线程安全,就会有问题。
- 原型
报错:存在多个 Bean 异常
- @Autowired:属于 Spring 框架,默认使用类型(byType)进行注入
- @Qualifier:结合 @Autowired 一起使用,自动注入策略由 byType 变成 byName
- @Resource:JavaEE 自带的注解,默认按 byName 自动注入。JDK9-11 某个版本已经移除。目前遵循 JakartaEE 标准,而非 JavaEE 标准。
- @Primary:存在多个相同类型的 Bean m @Primary 用于定义首选项
Bean 注入常见异常
- 只定义了接口,没有具体实现
- 解决方法 require 参数设置为 false
- 定义了接口的多个实现类,只使用@Autowire 注入
循环依赖怎么办
解决方式,三级缓存。只能解决单例模式下的循环依赖。
- field 的方式
- set 方式
循环依赖问题最终解决方案,还是要通过避免循环依赖进行解决。出现循环依赖是设计的问题。
循环依赖通常意味着类之间的耦合过强,可能需要重新审视和调整设计。例如:
- 单一职责原则:确保每个类只负责一个职责,避免一个类同时依赖多个功能,这样可以降低类之间的相互依赖。
- 依赖反转:如果类 A 依赖类 B,而类 B 又依赖类 A,考虑引入接口或抽象类,反转依赖方向。通过接口使类之间的依赖关系更加灵活。
- 抽象工厂模式:引入工厂类来管理对象的创建,而不是直接在类之间相互依赖。这样可以让依赖关系更加清晰。
- 事件驱动:如果两个组件之间的依赖是为了处理某些事件或操作,可以考虑使用事件驱动的机制,解耦它们之间的关系。
如何利用 Bean 生命周期
@Transactional
- 捕获异常,标记回滚(编程式事务)
- 捕获异常,抛出 Check 异常,不能回滚
- 捕获异常,抛出 UnCheck 异常,可以回滚
- 捕获特定异常,根据异常,可以选择性回滚
- 一个没标注事务的方法,调用一个标注事务的方法,事务失效
Spring MVC 的坑
自定义返回
- 使用 ResponseEntity 类:标识整个 HTTP 响应(状态码、头部信息、响应体
- 异常类或 Controller 方法上标识 @ResponseStatus 注解
- 使用 @ControllerAdvice(@RestControllerAdvice)和 @ExceptionHandler 注解
时间序列化和反序列化的问题
- @DateTimeFormat
- FE2BE
- Get 可以正常,Post 不行
- @JsonFormat
- 自定义实现 Json 序列化格式转换
日志放在拦截器还是过滤器
- Filter
- Interceptor
读取 Request 输入流,请求数据就不见了
SpringBoot 中的坑
配置出错
- 配置文件加载顺序
- 配置文件优先级
- 推荐使用同一种格式 yml
定时任务
- @EnableScheduling
- @Scheduled
- fixedDelay
- fixedRate
- initialDelay
- cron
- spring.task.scheduling
- ScheduleConfig
线程池
- @EnableAsync
- @Async
- 有没有返回值
异步任务如果抛出异常,spring 框架只打印了一行日志,并没有做处理。开发中需要捕获异常,并处理
异步任务如果超时了。一直阻塞不太好。选择超时时间,超时不做处理。
自定义线程池,配置自定义异常处理。
Jackson
- ObjectMapper 线程安全
- 尽量注入使用,尽可能重用。
- 注解
- JsonIgnore
- JsonProperty
- JsonFormat
- JsonInclude
- JsonIgnoreProperties
- JsonSerialize
SQL
NULL
为什么很多人用 NULL 呢?前情提要,以下是 MySQL 数据库的问题。
- NULL 是默认行为,如果不指定列是 NOT NULL,那么它就是 NULL 的
- 严重误区:NULL 不占用存储空间的(一种优化行为)
- 数据库每一行记录的大小,在一开始定义表结构的时候就确定了。
- NULL 属性非常方便,SQL 语句或者代码不需要额外的填充或判断
MySQL难以优化引用可空列查询,它会使索引、索引统计和值更加复杂。可空列需要更多的存储空间,还需要MySQL
内部进行特殊处理。可空列被索引后,每条记录都需要一个额外的字节,还能导致MyISAM中固定大小的索引变成可变大小的索引
含 Null 列的 where 条件,默认优化为 and where col is not null 或者 is null。
而且,由于索引的构建及优化。只有 is null 或者 is not null 才会走索引。严重影响查询效率。
导致唯一索引约束失败。
影响计算和聚合函数,计算错误。如 Count(col) 这个不会计算 null 值,
影响排序。NUll 值判断会在所有值最前面。
解决方法:
- 特殊值填充。
- 对于已经存在数据的表,填充特殊值到 NULL 列,再去修改表结构。
随意设置数据类型
- 主键
- mysql 允许创建表时不指定主键,但是一定要指定一个主键。InnoDB 自动添加隐士主键。
- 主键不具备任何业务含义,只是一个唯一的值。
- 字符串
- char
- varchar
- tinytext
- text
- mediumtext
- longtext
- 日期
- date
- time
- datetime
- timestamp
- 数值
- tinyint
- int
- bigint
- flot
- double
- decimal
- 二进制
- tityblob
- blob
- mediumblob
- longblob
- 可以使用 help 查看 help char
- 枚举
- mysql 做的并不好,不推荐用
建议
- 使用存储所需最小的数据类型
- 选择简单的数据类型
- 存储小数直接选择 decimal
- 避免使用 text 和 blob
索引
- 索引加的对,但是查询的有问题
- 字符串类型查询时没使用引号
- 左边的属性列,参与了数学或者函数运算,即便是满足按照最左前缀的顺序,也不会走索引。
- 联合索引最左前缀顺序不匹配,不会使用表索引
- 索引加的不对
- 不再使用的索引没有及时删除:空间浪费、插入删除更新性能受影响、MySQL 维护索引也需要消耗资源
- 索引选择性太低。所以选择性=不重复的索引值/表记录数
- 列值过长,可以选择部分前缀索引(区分度高的情况下),而不是整列加上索引
MySQL 自动断开了连接
8 小时限制连接,自动断开
autoReconnect 的副作用
- 原有连接上的事务将会被回滚,事务的提交模式将会丢失
- 原有连接中持有的表的锁将会全部释放原有连接关联的会话 session 将会丢失,重新恢复的连接关联的将是一个全新的会话 session
- 原有连接中定义的用户变量将会丢失
- 原有连接中定义的预编译 SQL 将会丢失
- 原有连接失效、新的连接恢复之后,MySQL 将使用新的记录行来存储连接中的性能数据
事务出错,可能是锁用的不对
- 颗粒度
- 行锁
- 表锁
- 数据锁定方式
- 乐观锁
- 悲观锁
- 排他锁
- 共享锁
Redis
正确选择数据类型
- string 几乎可以存储所有数据
- 浪费存储空间,key 也需要空间
- 管理、维护噩梦,redis 存在大量 kv 对象
- key 冲突几率变高 业务线名+工程名+模块名+键名
- hash
- list 队列、栈、有界队列
- set 去重、无序的数据集合 去重,共同关注
- sortedset 带有权重的集合,排行榜
使用了事务,怎么没回滚
redis 事务的基础,四个指令,multi、exec、discard、watch
两类错误
- 事务执行 exec 之前,入队命令错误(命令语法错误,服务器内存不足)
- 命令在 exec 调用之后失败(事务不停止,命令继续执行)
Big Key 影响性能
- 数据量大的 key,比如:字符串 value 值非常大,哈希、列表、集合、有序集合元素多
- 危害
- 内存不均
- 超时阻塞
- 网络流量拥塞
- 国企删除
- 迁移困难
如何发现 Big key
redis-cli 使用 –bigkeys 命令
key * 发现 key name
memory usage name 查看内存占用
删除 big key
- string del 命令
- dict 使用 hscan 每次获取部分 field value 使用 hdel 删除每个 field
- list 使用 ltrim 命令渐进式删除
- set 使用 sscan 每次获取部分,再使用 srem 删除每个元素
- sortedset 使用 zscan 命令,每次获取部分元素,再使用 zremrangebyrank 命令删除元素
redis 内存耗尽
- expire key seconds 设置生存时间,把 key 标记为易失的(volatile)
- 对 key 值修改,不改 key,不会刷新过期时间
redis 过期删除策略
- 定时删除
- 惰性删除
- 定期删除
有了过期机制以后内存还不够用
- 业务需求量大
- 不能及时失效
配置 maxmemory maxmemory-policy
常见的内存淘汰策略
- noevication
- allkeys-lru
- allkeys-random
- volatile-lru
- volatile-random
- volatile-ttl
volatile 策略只会对带过期时间的 key 进行淘汰
allkeys 策略会对所有的 key 进行淘汰
如果只是缓存,使用 allkeys。如果需要 redis 持久化,选择 volatile
频繁命令往返,造成性能瓶颈
redis pipline
- 非原子的
- 命令不能太多,不然会一直阻塞客户端
持久化
RBD
| 配置指令 | 配置含义 | 配置示例 |
|---|---|---|
| save | 时间策略 | save 900 1 save 300 10 |
| dbfilename | 文件名称 | dbfilename dump.rdb |
| dir | 文件保存路径 | dir /home/work/redis/ |
| stop-writes-on-bgsave-error | 如果持久化出错,主进程是否停止写入 | stop-writes-on-bgsave-error yes |
| rdbcompression | 是否压缩 | rdbcompression yes |
| rdbchecksum | 导入时是否检查 | rdbchecksum yes |
AOF
| 配置指令 | 配置含义 | 配置示例 |
|---|---|---|
| appendonly | 是否开启 aof | appendonly yes |
| appendfilename | 文件名称 | appendfilename “appendonly.aof” |
| appendfsync | 同步方式 | appendfsync everysec/always/no |
| no-appendfsync-on-rewrite | aof 重写期间是否同步 | no-appendfsync-on-rewrite no |
| auto-aof-rewrite-percentage auto-aof-rewrite-min-size aof-rewrite-incremental-fsync |
重写触发配置、文件重写策略 | auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb aof-rewrite-incremental-fsync yes |
| aof-load-truncated | 加载 aof 时如果有错如何处理 | aof-load-truncated yes |
缓存穿透
- 缓存穿透
- 空的数据缓存默认值
- 布隆过滤器
缓存雪崩
缓存集中失效
- 错开过期时间
- 限流&降级
- 本地缓存
Security
xss
主要思路
- 严格校验长度
- 严格根据需求校验 DTO
- 长文本必须转义
1 | |
SQL Inject
目前此漏洞很少了,如果代码中有涉及到硬编码 sql 的情况,使用#{},而非${}。
防盗链
目前云厂商的 OSS 基本上都送防盗链防护或者卖这个服务。接入就行了,便宜又方便。
底层实现原理:根据黑白名单,判断 HTTP 请求头中的 Referer 字段。
CSRF
- Spring Security CSRF Token
幂等
- Token 令牌
- 获取 token,判断是否缓存中有,没有直接报错,有则执行,执行完业务逻辑后删除。
- MVCC
- 去重表
- 悲观锁
防止伪造 Token 请求
核心接口做多因子认证
- 短信验证码
- 人脸识别
防止机器人
- 图形验证码,限流,黑白名单
忘记密码漏洞
- 多因子验证
- 多因子验证码接口调用前,图形验证码拦截,防止机器模拟
- 重试次数限制
- 防止 DDos
- 黑白名单
隐藏域漏洞
- 值传递不要用隐藏域
任意文件上传漏洞
- 文件格式限制
- 文件格式校验
- 前端校验扩展名
- 服务器校验扩展名
- content-type 校验
- 文件上传目录限制,权限限制
- 文件重命名
其他漏洞
- 直接异常信息,会给攻击者以提示。要做隔离
- 上线去除所有注释
网站安全漏洞扫描
信息加密与密钥管理
接口幂等
通过 token 机制进行幂等和防重。
接口安全
- 搭建 API 网关控制接口访问权限
- Oauth2
- HTTPS
- API 接口数字签名 非对称加密 RSA
- 基于令牌方式实现 API 接口调用
- 与 IP 绑定
- 与 USERID 绑定
Oauth2
todo 太复杂了,写简单了没意义,详细的,写不出来。研究研究先。
接口加密
- URL 特殊字符转码
- 对称加密和非对称加密
- 细谈 DES、RSA 加密原理
- 移动 APP 接口安全加密设计
- HTTPS
- 令牌
- 非对称加密
- 基于令牌方式实现接口参数安全传输
- 验签单向加密 MD5(加盐)
- 防止抓包篡改数据
网关
- 黑白名单
- 日志
- 协议适配
- 身份认证
- 计流限流
- 路由
HTTPS
参考资料
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。