0%

vhr人力资源管理系统面经

Spring框架面经。包括mubatis、spring、springMVC、springBoot,以理解为主。

1 介绍一下vhr人力资源管理系统

vhr 是一个前后端分离的人力资源管理系统,包括人事管理、系统管理等模块,基于 Restful 风格开发后端接口、使用 Swagger 进行接口测试;

技术栈:Spring Boot、Spring Security、MyBatis、MySQL、Redis、RabbitMQ

  • 1、使用 Spring Security 基于角色的动态访问控制,实现用户认证和用户授权;即根据用户的角色赋予不同的操作权限和显示菜单。
  • 2、将邮件服务划分为单独的模块、引入消息中间件 RabbitMQ,实现异步发送欢迎新员工的入职邮件;
  • 3、实现前后端分离,后端不再处理页面跳转仅传递 JSON 字符串,实现了基于 JWT 的无状态登录;

2 vhr的数据库是怎么设计的?

面向的用户是公司的管理人员,然后实现了基于角色的动态动态访问控制,基本的就有管理人员的hr表(用户表)、role表(角色表)、menu表(菜单表),然后就是实现用户、角色和菜单之间连接关系的hr_role表(一对多的关系)、menu_role表(多对多的关系)。然后就是其他一些公司的employee表(员工表)、departmen表(部门表)、joblevel表(职称表)等。

  • hr用户表,公司的管理人员也就是该系统的使用用户。。一个用户一般拥有一种或者多种角色
  • role角色表,不同的角色对用有不同的权限,比如人事专员只管人事、部门经理管理该部门的员工等
  • hr表和role表通过 一个hr_role表相联系,即指定一个用户具有哪几种角色
  • 还有一个menu表,相当于是一个资源表,menu中的数据会以json返回给前端,然后vue动态更新。其中里面最重要的字段就是url、路径匹配规则,所有的请求会被拦截然后根据这个url去查询访问这个url需要什么角色的权限,然后看当前用户是否具备相应的角色。
  • menu_role表将menu和role表相联系,即指定某个url会对哪些角色可见(menu和role是多对多的关系)

3 RESTful风格,介绍一下

3.1 RESTful风格是什么?

URI统一资源标识符

URL是统一资源定位符

REST(Representational State Transfer) 指 表现层状态转化。

  • 在web应用中,我们一般用URI统一资源标识符来表示一种资源

  • 表现层指的是一个”资源“的具体表现形式,比如一个html文本、一个xml或者一个图片都是表现层

  • 状态转换就是客户端通过http的几种请求方式(get\post\put\delete)对服务器进行访问,完成资源的状态转换

    • GET 用来获取资源、POST 用来新建资源(也可以用于更新资源)、PUT 用来更新资源、DELETE 用来删除资源

REST的核心在于,当你设计一个系统的时候,资源是第一位的考虑,你首先从资源的角度进行系统的拆分、设计,而不是像以往一样以操作为角度来进行设计。

总的来说就是,RESTful定义了一种客户端和服务器交互的风格,使URL更简洁、更有层次感。

3.2 RESTful的使用示例

SpringMVC 对 RESTful 提供了非常全面的支持,主要有如下几个注解:

  • @PathVariable注解:提取请求地址中的参数
  • @PostMapping注解:映射一个POST请求
  • @GetMapping注解:映射一个GET请求
  • @PutMapping注解:映射一个PUT请求
  • @DeleteMapping注解:映射一个DELETE请求

1 使用@PathVariable 注解可以将Controller下方法的参数绑定到URL的模板变量上,使url的访问更灵活。反之如果不使用的话,就需要在URL手动用=赋值参数

2 使用不同的注解组合(不同的请求方式)可以实现 相同的URL实现不同的效果!!使显示的url更简洁和有层次感

3.3 RESTful风格的设计原则

  • URL尽量使用名词复数, 不使用动词

  • 访问同一个URL地址, 采用不同的请求方式, 代表执行不同的操作(get获取, post新增等)

  • 服务器返回的响应格式数据,尽量通过XML JSON进行数据传递;

  • 无状态连接,服务器端不应保存过多上下文状态,即每个请求都是独立的;

4 前后端分离时,前后端是如何交互的

4.1 后台如何处理前端传递过来信息,并返回数据的过程

其实就是问springMVC的原理和执行流程

用户发出请求后,前端传过来的实际上是一个url,后台接受请求,返回数据给前端。接受请求并返回数据其实就是springMVC的整个处理流程。

SpringMVC的执行过程是围绕着前置(调度)控制器DispatcherServlet的调度来设计的。

  • 第一步,DispatcherServlet拦截请求,并调用 处理器映射HandlerMapping 和 解析控制器映射HandlerExecution。
    • 目的是根据url查找对应控制器,找到是哪个Controller应该处理这个请求,找到之后返回给DispatcherServlet
  • 第二步,DispatcherServlet调用HandlerAdapter处理器适配器,并让他调用那个Controller。
    • 接下来就是我们编写的代码逻辑,后端Controller调Service、Service调Mapper接口,mapper.xml做持久层的增删改查,得到一个JSON对象传递给前端
    • 前端解析JSON对自己的js对象,然后得到视图和模型,通过HandlerAdapter处理器适配器传递给DispatcherServlet
  • 第三步,DispatcherServlet调用视图解析器(ViewResolver)来解析逻辑视图,最后得到视图,再呈现给用户!!

总结:简单来讲的话就是三步:1通过url找到对应的Controller、然后2调用执行持久化层的查询获得逻辑视图或者model,最后3解析视图、并呈现给用户。

4.2 后端返回值有字符串、JSON、实体对象,如何统一的传给前端的?

做一个删除操作——返回的是RespBean对象

查询所有员工 返回的是一个RespPageBean对象

查询部门的时候返回的是一个list集合 List

@ResponseBody是作用在方法上的,@ResponseBody 表示该方法的返回结果直接写入 HTTP response body 中

其实就是SpringBoot框架的功能,使用了@ResponseBody注解,所有的java对象都转换为了JSON格式写入Http response body中。

所以我后端不用管其他,你要集合我返回集合、要msg我返回msg、要page返回page,反正最后都变成JSON给前端了。

5 RBAC基于角色的权限访问控制

5.1 基于角色的动态访问控制逻辑

使用springScurity完成用户认证和用户授权。@EnableWebSecurity //开启WebSecurity模式,然后编写一个SecurityConfig类继承WebSecurityConfigurerAdapter,重写里面的configure方法给用户设置角色和权限。

因为是前后端分离的项目,基本的登录逻辑就是:用户(hr表)登录成功之后,可以通过hr_role表查询到用户的角色,再根据用户角色去(meau_role中)查出来用户可以操作的菜单(资源),然后把这些可以操作的资源,组织成一个 JSON 数据,返回给前端,前端再根据这个 JSON 渲染出相应的菜单。

这样就可以根据数据库中的表信息,动态的配置资源-角色以及用户-角色之间的关系,进而调整用户可以操作的资源(菜单)。

5.2 有状态登录和无状态登录

传统的有状态登录Session

  • 服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理。典型的就是Session。

  • 例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:

    • 服务端保存大量数据,增加服务端压力

    • 服务端保存用户状态,不支持集群化部署

img

RESTful 风格的无状态服务

微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:

  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

好处:

  • 多次请求不需要必须访问到同一台服务器,

  • 服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)

  • 减小服务端存储压力

使用JWT实现无状态服务

  • 首先客户端发送账户名/密码到服务端进行认证登录
  • 认证通过后,服务端将用户信息加密并且编码成一个 token,返回给客户端
  • 以后客户端每次发送请求,都需要携带认证的 token Json web token (JWT)
  • 服务端对客户端发送来的 token 进行解密,判断是否有效,并且获取用户登录信息
jwt

5.3 Token如何保存状态

如何处理注销账户之后token还有效的问题?

相关的有:退出登录、修改密码、修改权限、注销账户之后,token还有效

Redis用来储存token,账户注销或者退出登录之后就删除Redis中的token

这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。

token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。

  • 解决方案一:将 token 存入内存数据库:将 token 存入 DB 中,redis 内存数据库在这里是是不错的选择。如果需要让某个 token 失效就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则。

token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

设置token的有效期为30分钟,服务器每次校验时,检查token的有效期,如果快过期了,就重新生成一个token给客户端。客户端请求之前先检查更新自己的token

先来看看在 Session 认证中一般的做法:假如 session 的有效期30分钟,如果 30 分钟内用户有访问,就把 session 有效期被延长30分钟。

  1. 类似于 Session 认证中的做法:这种方案满足于大部分场景。假设服务端给的 token 有效期设置为30分钟,服务端每次进行校验时,如果发现 token 的有效期马上快过期了,服务端就重新生成 token 给客户端。客户端每次请求都检查新旧token,如果不一致,则更新本地的token。这种做法的问题是仅仅在快过期的时候请求才会更新 token ,对客户端不是很友好。
  2. 每次请求都返回新 token :这种方案的的思路很简单,但是,很明显,开销会比较大。
  3. token 有效期设置到半夜 :这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
  4. 用户登录返回两个 token :第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为1天。客户端登录后,将 accessToken和refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。该方案的不足是:1⃣️需要客户端来配合;2⃣️用户注销的时候需要同时保证两个 token 都无效;3⃣️重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当accessToken 快过期的时候,提前去通过 refreshToken 获取新的accessToken)。

==5.4 JWT的格式??==

6 邮件服务原理

vhrserver相当于只是发送了一个消息给中间件

然后真正发送邮件的功能都在mailserver中,JavaMailSender接口

将邮件服务划分为单独的模块、引入消息中间件 RabbitMQ,实现异步发送欢迎新员工的入职邮件;即当管理人员比如hr在界面上操作新增了一名员工到数据库中,就会自动的向中间件RabbitMQ发送一条消息,然后mailServer会自动异步的订阅消息,向新入职的员工发送一封入职邮件,里面包含姓名、职位、职称和部门等信息。

vhrServer中的业务逻辑:

  • 新增员工的命令返回值为1时,进入邮件发送的代码中
  • 指定消息的唯一UUID,获取一个新的employee对象用于传递
  • 一个MailSendLog类用来记录消息的发送日志,里面实例变量有消息的id、员工id、投递状态、投递规则key exchage等
  • 配置RabbitMQ的配置文件,声明队列、交换机、绑定交换机等

mailServer中的业务逻辑:

  • 配置RabbitMQ的配置文件,然后消费消息就ok了
  • 消息确认机制改为手动,只要消费成功一条消息,就将消息id记录在Redis上,避免消息的重复消费

7 RabbitMQ如何保证消息的可靠性

1 保证我们的消息能够成功到达队列

  • 消息传递、要经过三个流程:

    • 1 消息到达交换机

      • 第一个流程可能由于网络的波动、延迟等原因,消息没有准确达到交换机
    • 2 交换机把消息路由到队列中

      • 第二个流程可能routing key错误等原因,消息没有准确到达队列
    • 3 消息持久化写入日志

      • 第三个流程可能在消息写入日志的过程中,broker crash掉了,重启后消息丢失
  • 解决方案一:事务控制,但大幅度降低了效率不推荐使用

  • 解决方案二:发送者确认模式:即消息发送失败回调、路由失败回调

    • 1 通过实现ConfirmCallBack接口,消息发送到交换器Exchange后触发回调

    • 2 通过实现ReturnCallback接口,如果消息从交换器发送到对应队列失败时触发(比如根据发送消息时指定的routingKey找不到队列时会触发)

    • 总结:

      • 如果成功入列,单纯给客户端发送”ack”信号

      • 如果消息无法路由到队列则是发送 “消息” + “ack”,且”消息”比ack信号先到

2 rabbitmq的持久化(解决消息到达队列之后的可靠性)

  • 三个持久化:交换机持久化、队列持久化、消息持久化

  • 持久化的消息在进入持久化队列之后会写入到rabbitmq的持久性日志文件中,消息被消费掉后,会把持久性日志文件中该消息标记为等待垃圾收集

  • 三个持久化很大程度上已经解决了rabbitmq这一端因为宕机而导致消息丢失的问题。。。如果在broker在把消息写入日志文件的时候崩掉了(极端情况),这时可以借助mirror queue来处理

3 确保一条消息只消费一次

  • 首先将 RabbitMQ 的消息自动确认机制改为手动确认,然后每当有一条消息消费成功了,就把该消息的唯一 ID 记录在 Redis 上,然后每次收到消息时,都先去 Redis 上查看是否有该消息的 ID,如果有,表示该消息已经消费过了,不再处理,否则再去处理。

  • 这里使用的是Token的思想,核心就是每个操作都有一个唯一凭证 token,一旦执行成功,对于重复的请求,总是返回同一个结果。

  • 每一个邮件消息都有唯一的uuid,作为唯一的凭证

-------------感谢阅读没事常来-------------