后端开发
# PO、BO、DO、VO、DTO、DAO、POJO
PO(Persistent Object,持久化对象)
定义:PO是与数据库表直接映射的对象,每个字段通常与数据库表的列一一对应。
用途:通常用于ORM(对象关系映射)框架(如Hibernate、MyBatis)中,主要用于数据库操作(增删改查)。
特点:一般不包含业务逻辑。
BO(Business Object,业务对象)
定义:BO是封装了业务逻辑的对象,通常用于业务逻辑层。
用途:用于处理复杂的业务逻辑。
特点:可能由多个DO或PO组合而成,包含业务逻辑和数据。
DO(Data Object / Domain Object,数据对象 / 领域对象)
阿里巴巴的DO(Data Object):
定义:与数据库表直接映射的对象,与数据库表一一对应,类似于PO。
特点:不包含业务逻辑。
DDD的DO(Domain Object):
定义:业务领域中的核心对象,类似于BO。
用途:用于业务逻辑层。
特点:可能由多个PO组成,包含业务逻辑和数据。
VO(View Object,视图对象)
定义:用于展示数据的对象,通常用于表现层(如前端页面)。
用途:通常用于返回给前端的数据封装。
特点:字段可以根据展示需求定制,不一定与数据库表或领域对象完全一致。不包含业务逻辑。
DAO(Data Access Object,数据访问对象)
定义:用于访问数据库的对象,通常封装了对数据库的操作。
用途:提供对数据库的增删改查操作。
特点:不包含业务逻辑。
DTO(Data Transfer Object,数据传输对象)
定义:用于在不同层之间传输数据的对象,通常用于服务层与表现层之间的数据传输。
用途:常用于微服务之间的数据传输、远程调用(如RPC)或前后端交互。
特点:不包含业务逻辑,仅用于数据传输。字段可以根据需要定制,不一定与数据库表或领域对象完全一致。
POJO(Plain Old Java Object,普通Java对象)
定义:一个普通的Java对象,不依赖于任何框架或接口。
- 用途:通常用于表示简单的数据对象。
特点:不继承特定的类或实现特定的接口,灵活且通用。
# 怎么组织上面这些对象的转换,有没有遇到什么挑战
手动转换
定义:通过手动编写代码实现对象之间的字段赋值。
优点:灵活性高,完全可控。
缺点:当对象字段较多时,手动编写转换代码会变得繁琐且容易出错。
使用工具类(如 Apache Commons BeanUtils)
- 利用反射机制自动完成对象属性的拷贝。 BeanUtils.copyProperties(userVo, userBo);
- 优点:简单快捷,适合字段名称一致的情况。
- 缺点:性能较低,无法处理复杂字段映射(如类型转换)。
- 利用反射机制自动完成对象属性的拷贝。 BeanUtils.copyProperties(userVo, userBo);
使用映射框架⭐
定义:通过映射框架(如MapStruct、ModelMapper、Dozer)自动完成对象之间的转换。
MapStruct
基于注解的代码生成工具
@Mapper public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); @Mapping(source = "name", target = "userName") @Mapping(source = "email", target = "userEmail") UserVo toVo(UserBo userBo); } // 使用 UserVo userVo = UserMapper.INSTANCE.toVo(userBo);
优点:
- 性能高(生成的是静态代码,无反射开销)。
- 支持复杂字段映射和类型转换。
- 提高代码的可读性和可维护性。
缺点:需要引入额外的依赖。
ModelMapper
ModelMapper 是一个智能对象映射库,能够自动推断字段映射关系。
ModelMapper modelMapper = new ModelMapper(); UserVo userVo = modelMapper.map(userBo, UserVo.class);
优点:简单易用,支持复杂映射。
缺点:性能略低于 MapStruct,可能需要额外配置。
使用Builder工厂模式
- 定义:通过Builder模式逐步构建目标对象。
- 优点:
- 支持复杂的对象构建逻辑。
- 代码可读性高,适合字段较多的情况。
- 缺点:需要为每个对象编写Builder类。
挑战
最大的挑战是在复杂业务场景下,各种对象之间的转换关系变得非常复杂。比如一个VO可能需要组合多个DO的数据,或者需要进行一些业务计算才能得到。
还有一个挑战是性能问题,特别是在高并发场景下,频繁的对象转换会带来一定的性能开销。我们曾经遇到过因为对象转换导致的性能瓶颈。
为了解决这些问题,我们采取了几种策略:
- 一是优化对象设计,减少不必要的转换;
- 二是使用缓存机制;
- 三是在某些关键路径上使用更轻量级的转换方式,比如直接使用Map而不是完整的对象。
# 为什么pojo类布尔类型不要用is开头
Java Bean规范要求getter和setter方法遵循特定的命名规则:
对于布尔类型的字段,getter方法以is或get开头。
setter方法必须以set开头。
比如isActive的getter方法是isActive(),setter方法是setActive()
如果布尔字段以
is
开头,可能会导致字段名与getter方法名看起来过于相似,容易引起混淆。而且许多序列化框架(如Jackson、Gson、Fastjson等)和工具(如Spring、Hibernate等)依赖于Java Bean规范来访问属性。
- 比如Jackson、Fastjson序列化时会使用Java Bean规范根据getter方法把字段解析为active而不是isActive
- 而Gson默认不依赖Java Bean规范,会直接基于字段名序列化为isActive
- 这时如果我们用Fastjson序列化用Gson反序列化就会出错
# 扫码登录的实现原理
扫码登录就是将手机端已经登录的状态同步到PC端或Web端
其主要分为三大步骤:生成二维码、扫码、确认登录
生成二维码:
用户打开PC端登录页面,PC端向后端发送请求,后端就会生成一个唯一的二维码ID,
后端将二维码ID会与二维码的状态(“待扫描”)一起保存在redis中,并生成二维码的图片,二维码图片和二维码ID一对一绑定
PC端显示二维码,等待用户的扫描,同时会与后端建立轮询的请求,定期根据二维码ID向后端查询二维码状态,如果二维码状态改变,PC端会同步更新页面。(当然也可以通过长连接的方式,当二维码状态改变时后端会主动通知PC端,比如淘宝就是用的轮询方式,抖音用的长连接方式。)
用户扫码:
用户扫码后,手机端解析二维码中的二维码ID,携带手机端的用户token、二维码ID向后端发送请求,
后端校验token后会变更二维码状态为已扫描,同时后端会生成一个临时token和二维码ID进行关联并存入redis中,防止二维码重复扫描,并将临时token返回给手机端,
此时PC端轮询到二维码状态变更后,PC端会显示待确认。手机端等待用户确认登录。
确认登录:
用户点击确认登录,手机端就会携带临时token向后端发送请求,
后端根据临时token获取二维码ID,并将二维码状态更新为“已登录”,
后端生成一个PC端Token,并与用户信息关联存储在Redis中。此时可以将临时token删除,
PC端通过轮询获取到二维码状态更新(“已登录”)和PC端Token,完成登录流程
扫码登录的核心, 是通过二维码作为一个桥梁, 将用户的身份信息从移动端传递给PC端, 整个过程呢可以分为生成二维码, 扫描二维码确认登录这三个步骤,
首先二维码的图片呢可以由前端或者后端生成, 但关键是要在二维码里面切入一个临时的token, 用来标记这次的扫描的绘画, 这个token会存到REDIS里面, key是我们token, value呢是二维码的一个状态, 比如说未扫描, 已扫描, 已确认等等,同时啊设置一个有效期, token过期以后二维码也就失效了, 来保证我们扫码的一个安全性, 前端跟后端可以通过轮询, 或者像WEBSOCKET这种长链接的方式进行通信, 实时从后端获取二维码的状态,
接下来就是用移动端扫描二维码, 移动端解析出二维码里面的token, 并向后端发送扫描的请求, 将移动端的token和二维码的token传给后端, 后端根据token将二维码的状态更新为已扫描, 让移动端确认是不是要登录,
用户在移动端点击确认, 登录以后, 手机端会再次发送请求给后端, 后端呢会把二维码的状态更新为已确认, 生成一个PC端Token,并与用户信息关联存储在Redis中。 完成登录,临时的token呢就可以从redis里面删除了, 那么这个呢就是整个扫码的一个登录流程
# 多端登陆踢人下线应该如何实现
对于传统基于session的单体应用架构,实现方案相对简单。这种方案依赖浏览器cookie和Tomcat session管理会话状态,核心在于sessionid的控制。具体实现可分为三个关键步骤:
- 创建一个全局的ConcurrentHashMap,以sessionid为key存储session信息。用户登录成功后,将其sessionid和session信息存入这个Map。
- 通过拦截器拦截所有请求,检查当前请求的session是否存在于全局Map中若存在,说明该用户已在其他地方登录,立即移除并销毁该session原登录用户再次访问时会被拦截
- 最后还需要一个session监听器,通过HttpSessionListener来监听session的创建和销毁,一旦检测session被销毁,需要把我们构建的全局ConcurrentHashMap的session,也需要移除,
对于现代分布式架构的token方案,实现原理有所不同:
- 用户登录成功后,服务端生成唯一token
- 将token与用户信息关联存储到Redis(比如记录token与用户ID)
- 服务端返回token给客户端存储
而要实现强制下线,就可以从Redis中清除token与用户id的映射关系,这时当原用户携带token请求时,因token已失效,系统会要求重新登录
除此以外,有的是使用JWT来生成token的,由于JWT本身是无状态的,所有用户信息都编码在token中,一个会话是否有效,看这个会话携带的token能不能正常解析出数据,这也就意味着令牌的合法性是由令牌决定的,对于这种场景的设计方案来说,单纯从JWT层面来去考虑,不是很好实现,可能最后还是得把JWT生成的token,和用户id的关系存储一份到REDIS里面,在强制下线的时候直接删除这个映射关系,这种方案就违背了JWT的设计初衷
# 如何使用redis记录上亿用户连续登录天数?
可以使用redis的bitmap数据结构
可以从两个维度来记录
如果要统计所有用户的连续登录天数
可以以日期为bitmap的key,用户id映射到bitmap的位上
有用户登录就将对应的bitmap位置为1,统计所有用户的连续登录天数时,就从当前的日期往前统计,遇到1 就将该用户的连续登录天数加1,遇到0就停止统计
用户id需要唯一
适合于用户量大的情况,而且不建议统计超过30天,可以设置key的过期时间为30天
如果要统计单个用户的连续登录天数,需要遍历每一天的bitmap,不方便
可以用另一种维度的bitmap,以用户id位bitmap的key,将日期映射到bitmap的位上
登录就置为1,统计单个用户的连续登录天数时,只需要遍历该用户的bitmap,从当前日期往前统计,遇到1计数加1,遇到0停止
这两种存储方式都可以用于统计用户登录天数,具体使用哪种需要结合具体实际情况
# session、token、redis、nginx
# session
Session 有状态认证机制,服务器存储用户状态信息。
优点
- 安全性高:Session 数据存储在服务器端,客户端只保存 Session ID,不易被篡改。
- 灵活性高:可以存储任意类型的数据(如用户信息、权限等)。
缺点
- 服务器压力大:Session 数据存储在服务器端,用户量较大时可能占用大量内存。
- 扩展性差:在分布式系统中,需要额外的机制(如 Session 复制或集中存储)来共享 Session 数据。
- sessioID通过cookie传递,需要设置httpOnly 标志保护cookie来受到xss脚本攻击,不会被js访问到
适用场景
- 传统的单体应用。
- 需要存储复杂用户状态的场景。
# session是如何工作的,生命周期是怎么样的
工作原理:
- 用户登录后,服务器创建 Session来保存用户的登录状态 并生成一个唯一的 Session ID。
- 服务器将 Session ID存入响应头的set-cookie属性中 返回给客户端(通常通过 Cookie)。
- 客户端会根据 set-cookie 属性自动在cookie中存入 Session ID。
- 服务器根据 Session ID 查找对应的 Session 数据,验证用户身份。
生命周期:
- 创建:用户登录时创建。
- 销毁:用户登出或 Session 过期时销毁。
# session+redis
使用场景
- 需要存储用户的状态信息(如权限、临时数据)。
- 需要支持分布式系统,确保 Session 数据在多个服务节点之间共享。
实现方式
- 用户登录:
- 用户登录后,服务器生成一个唯一的 Session ID,并将用户状态信息(如用户 ID、权限等)存储到 Redis 中。
- Session ID 返回给客户端(通常通过 Cookie)。
- 请求处理:
- 客户端每次请求时携带 Session ID。
- 服务器根据 Session ID 从 Redis 中获取用户状态信息,完成业务逻辑。
- 用户登出:
- 客户端删除本地 Session ID。
- 服务器从 Redis 中删除对应的 Session 数据。
优点
- 数据共享:Redis 作为集中存储,支持分布式系统。
缺点
- 性能依赖 Redis:Redis 的性能和可用性直接影响系统表现。
- 扩展性有限:Session 数据量较大时,Redis 内存占用较高。
# 为什么没有使用nginx+session的方案
虽然nginx可以通过配置 ip_hash
或者 sticky 模块来确保用户的请求被路由到同一个服务器。
但是在高并发场景下(如抢优惠券),如果恰好用户会话都在同一个服务器,大量用户同时发起请求都被路由到同一个服务器,会导致该服务器过载,而其他服务器可能处于空闲状态,无法有效利用集群资源。
此外,如果系统向微服务架构演进,传统的Session机制存在一些不足。Session数据需要存储在共享存储(如Redis或数据库)中,以便不同服务能够访问用户的会话信息。这种方式增加了系统的复杂性和维护成本,尤其是在分布式环境中,还需要考虑数据一致性、容错性等问题。
基于上述情况,我们还是选择了基于Token的认证机制(如 JWT), 因为它每个请求都携带完整的身份验证信息,不需要依赖于共享Session存储,可以很好地支持微服务架构中的跨服务调用,容易扩展和管理
# token
Token 是无状态认证机制,用户信息存储在 Token 中(如 JSON Web Token(JWT))
优点
- 无状态:服务器不需要存储 Token,适合分布式系统和微服务架构。
- 灵活性:可以在不同服务之间自由传递,适合微服务架构中的跨服务调用。
- 跨域支持:Token 可以轻松实现跨域认证。
- 可扩展性:由于是无状态的,容易扩展和管理
缺点
- 安全性依赖实现:Token 存储在客户端,可能被窃取或篡改(需使用 HTTPS 和签名机制增强安全性)。
- 无法中途撤销:Token 一旦签发,在有效期内无法撤销(除非引入黑名单机制)。
- 数据膨胀:Token 包含用户信息,可能导致数据量较大。
适用场景
- 分布式系统和微服务架构。
- 跨域认证和单点登录(SSO)。
# token是如何保证无状态认证的
工作原理
- 用户登录后,服务器生成 Token 并返回给客户端。
- 客户端保存 Token(通常存储在 LocalStorage 或 Cookie 中)。
- 客户端每次请求时携带 Token。
- 服务器验证 Token 的签名和有效期,并从中提取用户信息。
这样服务器就不用存储token,只需验证用户请求中的token是否有效,签名是否验证通过,并从中获取用户信息就能验证用户的身份。
# JWT
JWT是一种开放标准,通常用于身份验证和信息交换,又三个部分组成:
- Header:包含算法和类型(如
{"alg": "HS256", "typ": "JWT"}
)。 - Payload:包含用户信息和其他数据(如
{"sub": "123", "name": "John"}
)。 - Signature:对 Header 和 Payload 的签名,用于验证 Token 的完整性。
缺点:
jwt是无状态的,没有办法取消令牌强制用户下线,只能把用户状态保存下来,但也改变了它的作用
jwt是不加密的,不能传输敏感信息
占用大小要比session大很多
# 如何实现双 Token 机制?它的优点是什么?
- 实现:
- 用户登录后,生成 Access Token 和 Refresh Token。
- Access Token 用于访问资源,Refresh Token 用于刷新 Access Token。
- 优点:
- 提高安全性:Access Token 有效期短,减少泄露风险。
- 提升用户体验:用户无需频繁登录。
# 如何实现单点登录(SSO)?Session 和 Token 在 SSO 中分别如何应用?
SSO 实现:
- 用户登录认证中心,生成 Token。
- 认证中心将 Token 返回给客户端。
- 客户端携带 Token 访问其他系统。
- 其他系统验证 Token 的合法性。
Session:认证中心存储用户状态,其他系统通过共享 Session 实现 SSO。
Token:认证中心生成 Token,其他系统通过验证 Token 实现 SSO。
# JWT的滑动过期机制
每当用户发起请求时,如果当前的Token即将过期,则生成一个新的Token,以延长其有效期。这样,只要用户保持一定的活动频率,其会话就不会过期;但如果用户在一定时间内没有进行任何操作,则会话将过期,需要重新登录。
性能考虑:频繁地生成新Token可能会对性能造成影响。
用户体验:确保良好的用户体验,让用户在无需手动干预的情况下保持登录状态,
潜在的安全风险:如果Token泄露且没有其他安全措施(如黑名单机制),攻击者可以无限期地保持登录状态。
# 在分布式系统中如何处理JWT的滑动过期
token+redis
使用场景
- 需要无状态认证,支持跨域和分布式系统。
- 需要存储 Token 的附加信息(如黑名单、用户权限)。
实现方式
- 用户登录:
- 用户登录后,服务器生成一个 Token(如 JWT),并将其返回给客户端。
- 同时,将 Token 的附加信息(如用户权限、黑名单状态)存储到 Redis 中。
- 请求处理:
- 客户端每次请求时携带 Token。
- 服务器验证 Token 的合法性,并从 Redis 中获取附加信息(如权限)。
- 用户登出:
- 客户端删除本地 Token。
- 服务器将 Token 加入 Redis 黑名单(或直接删除附加信息)。
优点
- 无状态:Token 自包含用户信息,适合分布式系统。
- 扩展性好:Redis 存储附加信息,支持复杂业务逻辑。
缺点
- 实现复杂:需要结合 Token 和 Redis 的逻辑。
- 性能依赖 Redis:Redis 的性能和可用性直接影响系统表现。
# Redis + Token 和 JWT 的区别
Redis + Token 方案
- 生成Token:当用户登录成功后,服务器生成一个唯一的Token(通常是一个随机字符串),并将其存储在Redis中,同时将Token返回给客户端。
- 存储Token:Redis作为内存数据库,能够高效地存储和检索Token。每个Token可以关联用户的会话信息或其他必要数据。
- 验证Token:每次请求到来时,服务器从请求头中提取Token,并在Redis中查找该Token。如果找到,则认为请求合法,并继续处理;否则拒绝请求。
- 管理Token生命周期:通过设置Redis中的Key的过期时间来控制Token的有效期。也可以手动删除或更新Token。
优点
- 状态化管理:由于Token存储在服务器端的Redis中,服务端可以完全控制Token的状态,便于管理和撤销。
- 高性能:Redis是内存数据库,读写速度非常快,适合高并发场景。
- 灵活控制:可以根据需要动态调整Token的有效期或立即失效(如用户登出时)。
缺点:额外依赖:需要引入Redis等外部存储系统,增加了系统的复杂性和运维成本。
JWT 方案
工作机制
生成JWT:当用户登录成功后,服务器使用密钥对包含用户信息的Payload进行签名,生成JWT,并将其返回给客户端。
传递JWT:客户端收到JWT后,通常将其存储在LocalStorage或Cookie中,并在后续请求中通过HTTP Header(如
Authorization: Bearer <token>
)传递给服务器。验证JWT:每次请求到来时,服务器从请求头中提取JWT,并使用相同的密钥对其进行验证。如果验证通过,则解码Payload获取用户信息;否则拒绝请求。
管理JWT生命周期:JWT自带有效期(
exp
字段),一旦生成无法直接修改其内容或撤销。可以通过结合Refresh Token机制延长用户会话。
优点
无状态:JWT包含了所有必要的信息,服务器无需存储任何会话状态,易于扩展和维护。
减少网络开销:不需要额外查询数据库或缓存,减少了网络延迟和负载。
跨域友好:由于JWT是自包含的,非常适合前后端分离和跨域应用场景。
缺点
不可撤销:一旦签发,除非过期,否则无法直接撤销特定用户的Token。需要额外机制(如黑名单)来处理这种情况。
大小限制:由于 JWT需要在网络上传输,其大小不宜过大,特别是当需要携带大量用户信息时。
安全性风险:如果密钥泄露,攻击者可以伪造任意有效的 JWT。
# Session 和 Token 可以结合使用吗?如果可以,如何实现?
- 可以结合使用:
- 使用 Token 进行无状态认证。
- 将敏感数据(如权限)存储到服务器端的 Session 中。
- 通过 Redis 共享 Session 数据。
# Cookie、Session、JWT
Cookie
工作流程
用户登录后服务器后验证用户名和密码,
验证通过将用户名放入响应头的set-cookie属性中
客户端发出请求就会根据set-cookie属性自动将用户名放入cookie中
服务器根据cookie中的用户名进行身份验证
缺点:
- cookie中的用户名容易被篡改
- cookie的容量时优先的
- cookie可能被用户禁用
Session
- 工作流程
- 用户登录后服务器会将用户信息存放在session中并生成一个唯一的sessionID与之对应
- 将SessionID放入响应头的set-cookie属性中
- 客户端根据set-cookie属性会自动把sessionID放入cookie中
- 服务器根据cookie中的sessionID对应到session中获取用户信息进行身份验证
- 缺点:
- 高并发下,服务器会占用大量资源
- 扩展性差,分布式环境下需要进行集群间session同步或者进行session共享存储
- 依然需要使用cookie,在前后端分离架构下会有跨域限制,需要进行响应的CORS配置
JWT
工作流程
- 用户登录后服务器将用户信息保存在JWT中返回被客户端
- 客户端可以通过cookie或者将JWT放入请求头中发送给服务器
- 服务器对JWT进行验证,保证JWT有效且没有被篡改,从中获取用户信息
优点:
- 由于JWT是无状态的,扩展性好,适合微服务和分布式系统
缺点
- 相较于简单的SessionID,JWT比较大,
- 一旦签发,除非过期,否则无法撤销
# Session 和 Token 分别有哪些安全性问题?如何解决?
- Session:
- 问题:Session 劫持(如网络监听、xss脚本攻击)
- 解决:
- 使用 HTTPS防止网络监听、使用Secure 标记确保Cookie只能通过HTTPS传输,防止通过HTTP明文传输时被截获。
- 使用HttpOnly标志可以防止 JavaScript访问Cookie,有效防御XSS攻击
- Token:
- 问题:Token截取、Token 泄露、重放攻击、CSRF(跨站请求伪造)。
- 解决:
- 使用 HTTPS防止中间人攻击和网络监听
- 设置较短的 Token 有效期、使用 Refresh Token(双Token机制)
- 使用CSRF Token,设置
SameSite
标志属性为Strict
或Lax
。
# 在选择将JWT存储在LocalStorage还是Cookie时,你认为哪种方式更好?为什么?
LocalStorage
优点
存储容量大:LocalStorage 可以存储较大数据(通常为 5MB),适合存储较长的 JWT。
前端控制:前端可以完全控制 JWT 的存储和读取,灵活性高。
跨域支持:LocalStorage 不受同源策略限制,适合前后端分离的架构。
缺点
安全性较低:LocalStorage 容易受到 XSS(跨站脚本攻击)的影响,攻击者可以通过注入恶意脚本窃取 JWT。
手动管理:需要手动将 JWT 添加到请求头中(如
Authorization: Bearer <token>
)。
安全性增强:
使用 HTTPS 加密传输。
对 JWT 进行加密或签名。
设置较短的 Token 有效期。
Cookie
优点
安全性较高:
- 可以设置
HttpOnly
标志,防止 JavaScript 访问 Cookie,减少 XSS 攻击的风险。 - 可以设置
Secure
标志,确保 Cookie 仅通过 HTTPS 传输。 - 可以设置
SameSite
标志,防止 CSRF(跨站请求伪造)攻击。
- 可以设置
自动管理:浏览器会自动将 Cookie 附加到每个请求中,无需手动管理。
缺点
存储容量小:每个 Cookie 的大小通常限制在 4KB 左右,不适合存储较长的 JWT。
跨域限制:受同源策略限制,跨域请求需要额外配置 CORS。
高安全性场景:
优先选择 Cookie,并严格设置安全标志。
结合双 Token 机制(Access Token 和 Refresh Token),进一步提高安全性。