# security-integration **Repository Path**: linshj/security-integration ## Basic Information - **Project Name**: security-integration - **Description**: 安全框架学习 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-07-15 - **Last Updated**: 2021-09-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [TOC] # 后台管理系统 ## 前端(common-security-vue) ### 技术栈 - vue2.x脚手架 - axios - element-ui - jsonwebtoken ### 设计思路 #### 简要说明 ​ 初学vue,不是很熟悉,该前端项目是参考某些实战项目进行开发的,本项目从git上拉取下来后,进入vue的项目路径,使用npm install命令即可把相关的依赖信息下载下来即可运行 ​ 具体的页面模块功能其实很简单,都是vue和element-UI的基本使用,具体参考官网即可。这里对项目中参考其他实战项目中的一些关键点进行说明,该部分内容都是通用的,可用于到其他vue项目上 - route - 前置导航守卫 通过是否存在token信息来进行判定,对没有登录的用户进行拦截并重定向到登录页面 - 路由信息模块化 对路由信息进行模块化,这样做的好处是易于管理,对每个模块的路由单独建立一个js进行管理,此后台管理系统只有一个管理模块,因此使用了“/”作为根路径 - store - 对store信息进行模块化,这样做的好处是易于管理,将菜单信息和枚举信息放到store中,可以避免多次重复加载后台数据吗,此处将后台部分使用到的枚举值存写死在store,正常的业务系统通过actions进行异步加载 - lib - constant.js 定义系统中所有页面会用到的常量和方法,方便所有页面引用 - QueryPageUtil.js 封装查询翻页列表信息时使用到的参数信息,封装返回查询翻页列表时的处理方法 - request.js 本项目设计最复杂的js,通过封装axios,来完成前后端的通讯请求 - 封装axios作为工具类 参考其他实战项目,将使用的axios进行封装,配置一系列通用的配置信息,如请求头、返回结果的处理等,减少实际调用传递的参数信息,达到统一处理的目的 - 添加请求时拦截器 本项目是前后端分离项目,在向后端进行每个请求时,均需要带上jwt来作为用户凭证,因此在请求前将jwt信息写入请求通中。另外在此处开启loading动画,不允许用户在数据请求期间再做其他操作 - 添加返回时拦截器 配置返回时的拦截器,主要处理两个方面的问题。 第一个是数据层面的问题,后端返回固定的数据结构,包含本次数据请求的状态标识,通过判定该状态标识,来决定调用Promise的resolve或reject,这样就不用再在每个请求中单独判定,达到减少重复代码的功能。另外在请求时开启了loading动画,需要在此处进行关闭 第二个就是系统级别的错误问题,在请求失败的情况下,通过判定error.response.status的状态码(因部分状态码由后端返回,所以需与后端约定哪种情况返回哪种状态码)来判定相关的请求失败信息,并展示到页面上。另外在请求时开启了loading动画,需要在此处进行关闭 - 添加loading信息 添加loading动画信息,避免用户在请求数据的情况下,再做其他操作。 - 处理token超时续签 token续签是本前端项目最难处理的问题,token过期的情况下,需要进行两个动作,一个是重新续约,另外一个就是将本次失败的请求再次进行请求并返回到页面上 token续签和本次失败数据再次请求都是需要与后端进行通信的异步动作,而js的顺序执行并不会等待异步结果的返回,因此此处需要使用到await和async方法,才能完成整个流程,具体的方法查看refreshTokenAndReRequest()。 对await和async的理解,可以参考[这里](https://segmentfault.com/a/1190000007535316) - api - index.js 因为前端项目可能存在多个后端系统需要进行通信,实例化httpInstance时统一定义后端系统的地址,这样在每个具体调用时就可以省略这部分重复的代码 - components - SysDictSelect.vue element-ui的select控件提供了比较详尽的方案,但是在实际使用中仍旧需要比较多的配置,因此在项目中再次对select控件进行封装,形成一个通用的component,是非常有必要的 该component的关键点在于要将prpos.dictValue和selectValue实现双向同步,因此对dictValue进行了watch实现父组件向该子组件传递数据,而changeVal实现了子组件向附件组散发更新值(父组件的value要添加.sync后缀),从而实现双向同步 ## 后端(common-security) ### 技术栈 - springboot - spring-cache - redis - mybatis-plus - spring-security - knife4j ### 设计思路 #### 登录设计 ​ 采用spring security+Jwt的方式,实现了前后端分离,对相关的实现类进行简要说明,具体思路可参考[spring security认证及授权1](http://www.macrozheng.com/#/architect/mall_arch_04)和 [spring security认证及授权2](http://www.macrozheng.com/#/architect/mall_arch_05) - SecurityConfig ​ spring security的配置类,完成seciruty的总体配置工作,其中**configure(HttpSecurity http)**用于实现登录认证和资源访问权限的控制,本项目主要做了以下的配置 1. 关闭csrf(暂时不实现) 2. 开启cors跨域访问(跨域配置在另外一个类),一定要进行开启,否则就算配置了跨域类,也无法生效 3. 禁止session使用,因为本项目是前后端分离项目,所以无需使用session 4. 对部分关键接口放行(登录,刷新token),无需登录即可实现请求 5. 对资源访问进行权限控制(具体的权限配置在RbacService) 6. 设置登录和权限异常处理(MyAuthenticationEntryPoint、MyAccessDeinedHandler),因为是前后端分离的项目,在此处对未登录(401)及权限不足(403)时进入到对应类中,进行响应状态码及对应失败信息的返回 7. 增加jwt认证过滤器,用于实现jwt的登录认证 - JwtAuthenticationFilter ​ jwt认证过滤器,实现从请求头中获取token信息,验证token有效性及加载用户权限信息并设置到SecurityContextHolder中 - MyRbacService ​ 对已登录的用户进行资源鉴权,根据用户包含的权限信息与请求的uri资源进行比较,判断当前用户是否包含对应权限,若权限不足时,将会进入MyAccessDeinedHandler中 - MyAccessDeinedHandler ​ 资源鉴权失败的处理,本项目为前后端分离项目,因为此处的做法是修改请求头的状态响应码并返回权限不足的提示 - MyAuthenticationEntryPoint ​ 未登录时的处理,本项目为前后端分离项目,因为此处的做法是修改请求头的状态响应码并返回未登录的提示 - MyUserDetailService ​ 实现了security的UserDetailsService#loadUserByUsername,加载用户基本信息、权限信息及关联的组织机构信息,因为jwt的性质,所以每个请求都会从数据库中加载完整的用户权限信息,此处增加了用户信息的缓存,避免频繁查询数据库 - MyUserDetail ​ 该类为实体类,继承了数据库的用户实体及security,以及实现了security的UserDetails,包含了用户基本信息、权限信息及机构信息等。 ​ MyUserDetailService返回的就是该实现类,上述提到MyUserDetailService会将信息缓存到redis中,此处在反序列化时会出现错误。原因在于GrantedAuthority类中只有get方法,该类属性的赋值是通过构造方法传入的,针对该类需要在redis的反序列化解析器中注册一个专门针对的该类的的解析器(SimpleGrantedAuthorityDeserializer) ​ 该类最终会被设置成UsernamePasswordAuthenticationToken.principal,作为登录凭证,存放于SecurityContextHolder中 - CorsConfig ​ 解决前后端分离的跨域问题,除了该类外,需要在security配置中开启cors配置,否则无法生效 #### 参数说明 本工程中使用了很多的配置信息,其中很多都存放在application.yml中,部分固定的内容存放于枚举类 - application.yml > #mybatis-plus代码生成路径,如果本工程是module时,需要进行设置 > project-location: /common-security > > #jwt配置,head名称、加密签名的密钥,过期时间等,被JwtTokenUtil读取 > jwt: > header: JWTHeaderName > secret: lsj&cyb > expiration: 3600000 > refreshExpiration: 7200000 > > #security相关配置,被SecurityProp读取 > security: > #配置放行路径 > ignoreUrls: > - /authentication > - /refreshToken > #配置放行资源 > ignoreResources: > 放行静态资源 > - /css/** > - /fonts/** > - /img/** > - /js/** > #knife4j放行 > - /webjars/** > - /doc.html > - /v2/** > - /swagger-ui.html > - /swagger-resources/** - 枚举类 > RedisKey--redis缓存的键值名称前缀 #### 分布式锁 ​ 虽然后台管理系统大多数的情况下都是少部分人员来使用的,不过为了完善数据一致性问题,因此在部分关键的数据处理中,使用了redis的分布式锁,用于保证数据一致性 ​ 通过使用Aop(DistributedLockAop)+注解(DistributedLock)的方式,实现了分布式锁的功能(自己简单写的,可能会有一定缺陷),用于以下场景的时候,保证数据的一致性: - 菜单 - 菜单插入 - 菜单删除 - 菜单更新 - 菜单排序更新 - 组织机构 - 组织机构新增 - 组织机构删除 - 组织机构更新 - 组织机构排序更新 - 角色 - 角色新增 - 角色删除 - 角色更新 - 角色与菜单 - 角色与菜单关系更新 - 用户与角色 - 用户与角色关系更新 - 用户与组织机构 - 用户与组织机构关系更新 #### 缓存设置及清除 ##### 缓存信息 - 用户登录时缓存用户信息(user::xxx) 具体对象是MyUserDetail,包括用户的基本信息、权限集合、组织机构等信息 该对象缓存时遇到了无法反序列化的问题,原因是因为GrantedAuthority接口只有get方法,没有set方法,解决的办法是为该类添加反序列化转换器(SimpleGrantedAuthorityDeserializer),然后注册到redis反序列化的配置上,这样就能解决反序列化的问题 - 字典信息缓存(dict::xxx),在后台中没有使用到的地方不需要缓存,前台业务系统,获取存取缓存需要遵照dict::xxx格式 ##### 清除时机 当更新了后台信息后,有些信息需要及时更新,所以需要在操作的同时清空缓存信息,以下都是使用了@CacheEvit来进行部分或全部缓存删除,如果是需要按需删除,可以在本代码基础上进行更改 - 菜单 - 菜单删除 - 菜单更新 - 菜单排序更新 - 组织机构 - 组织机构删除 - 组织机构更新 - 角色 - 删除角色 - 角色与菜单 - 更新角色与菜单关系 - 用户与机构 - 更新用户与机构关系 - 用户与角色 - 更新用户与角色关系 #### 后台管理设计 ##### 后台菜单管理(AdminMenuController) 1. 返回后台管理系统(前端)的菜单信息,内容固定为以下信息,该部分菜单信息存放于sys_admin_menu中。 2. 可以按照实际情况进行更改,但要记住具体的url要与前端设置的路由信息一致 ##### 登录管理(JwtController) 1. 提供登录和续签(刷新token)的功能 2. 登录和续签返回的信息都会返回两个jwt token,分别是access_token和refresh_token,两者除了过期时间之外并无其他区别,用于实现前端无感续签。 无感续签原理: 1. 当access_token过期后,前端向后端发起第一次请求时,将会校验到token失效,此时将会返回http状态码为401的信息到前端,告知未登录,本次请求是失败的 2. 当前端收到401的状态时,在有token的情况下,会使用refresh_token向后端发起刷新token的请求 3. 若refresh_token未过期,后端将会返回新的access_token和refresh_token,此时前端将返回的access_token和refresh_token重新设置,然后将失败的请求重新再发送一次并返回请求结果,实现无感续签 4. 若refresh_token已过期,此时将会返回失败信息,前端就要清空token信息并跳转到登录页面 ##### 用户管理(UserController) 1. 提供增删改查和修改密码等基本功能 - 用户新增时,要检查是否存在相同账户数据,存在则不允许新增 - 用户删除时,要将用户关联的角色关系也删除 ##### 用户与角色管理(UserRoleController) 1. 用户与角色更新的功能 - 清除缓存中关联的用户信息,实现实时更新权限信息 2. 根据用户Id查找关联角色功能(用于展示在页面上) ##### 角色管理(RoleController) 1. 提供增删改查等基本功能 - 角色新增 - 检查是否存在相同中文名称角色或相同角色代码数据,存在则不允许新增 - 为数据设置设置默认顺序和默认可用功能 - 加入redis分布式锁(key=role_key),因为存在排序字段,所以需要加入分布式锁控制 - 角色删除 - 删除用户与角色的关联关系 - 删除角色与菜单的关联关系 - 清除缓存中所有的用户信息(这里因为使用cacheEvict,只存入了roleId,所以只能全部删除。如果要实现精确删除,需要先查询出该角色关联的用户,然后逐一删除,此处可按需更改) - 删除时,更新所有角色的排序 - 加入redis分布式锁(key=role_key),因为存在排序字段,所以需要加入分布式锁控制 - 角色更新 - 当角色状态不可用时,需要删除用户与角色关联关系 - 每次更新时不能更新角色代码(因为每个角色代码是唯一的) - 加入redis分布式锁(key=role_key),因为存在排序字段,所以需要加入分布式锁控制 2. 提供列出所有角色的功能(用于为用户授权时一次性带出所有的角色信息,包括不可用角色,由前端控制不能选择不可用角色) ##### 角色与菜单管理(RoleMenuController) 1. 更新角色与菜单关系 - 清除缓存中所有的用户信息(这里因为使用cacheEvict,只存入了roleId,所以只能全部删除。如果要实现精确删除,需要先查询出该角色关联的用户,然后逐一删除,此处可按需更改) 2. 根据角色编码查找对应的菜单(权限)信息(用于展示在页面上当前角色存在哪些权限) ##### 菜单管理(MenuController) 1. 提供增删查改等基本功能 - 菜单新增 - 检查父级菜单是否存在 - 检查父级菜单为叶子节点时,更新为非叶子节点 - 为新增的菜单计算排序(在当前父节点下),默认是最后一名 - 为新增的菜单插入默认信息,默认菜单名称(可在更新菜单时修改)、父节点集合信息、默认可用、默认为叶子节点、层级信息等 - 加入redis分布式锁(key=menu_tree_key),因为存在排序字段,所以需要加入分布式锁控制 - 菜单删除 - 查出当前被删除节点的所有子节点 - 删除角色与菜单的关联关系,包括所有的子节点,同时删除该子菜单信息 - 删除当前菜单后,更新当前父节点下的剩余菜单节点排序 - 删除菜单后,判断当前父节点是否还有其他子菜单,没有则更新父菜单为叶子节点 - 加入redis分布式锁(key=menu_tree_key),因为存在排序字段,所以需要加入分布式锁控制 - 清除缓存中所有的用户信息,这个是必须的,因为要实时更新菜单信息 - 菜单更新 - 检查是否存在相同名称或url的菜单,存在则不允许更新 - 若当前菜单状态不可用,需要删除角色与菜单关联关系(包括子菜单),更新所有子菜单的状态不可用 - 设置菜单状态可用时,先检查上级菜单是否可用,上级菜单不可用的情况下,此次更新失败并返回提示到前端 - 加入redis分布式锁(key=menu_tree_key),因为存在排序字段及可用状态等信息,所以需要加入分布式锁控制 - 清除缓存中所有的用户信息,这个是必须的,因为要实时更新菜单信息 2. 提供单个菜单更新排序功能(在页面上可以修改菜单排序,仅限于当前父节点下的菜单排序) - 菜单在当前层级排在最前面或最后面时,不允许更新排序 - 当前菜单排序更新后,对其他同层级被影响到排序的菜单,也要更新信息 - 加入redis分布式锁(key=menu_tree_key),因为存在排序字段,所以需要加入分布式锁控制 - 清除缓存中所有的用户信息(这个只是单纯的排序,如果觉得问题不大,可以不加) 3. 提供菜单树结构的接口(用于在页面上的多个地方展示,菜单管理页面展示、角色包含的菜单权限等) 4. 根据用户Id获取菜单树信息(在后台管理中没用) ##### 用户与组织管理(UserOrgController) 1. 更新用户与组织关系 - 清除缓存的用户信息 - 加入分布式锁(org_tree_key),防止更新关系时被删除了也不知道 2. 根据用户Id找出关联的机构信息(用于展示在页面上当前用户所关联的机构信息) ##### 组织机构管理(OrgController) 1. 提供增删查改等基本功能 - 组织机构新增 - 检查父级菜单是否存在 - 检查父级菜单为叶子节点时,更新为非叶子节点 - 为新增的组织机构计算排序(在当前父节点下),默认是最后一名 - 为新增的组织机构插入默认信息,默认组织名称(可在更新组织时修改)、父节点集合信息、默认可用、默认为叶子节点、层级信息等 - 加入redis分布式锁(key=org_tree_key),因为存在排序字段,所以需要加入分布式锁控制 - 组织机构删除 - 查出当前被删除节点的所有子节点 - 删除用户与机构的关联关系,包括所有的子节点,同时删除该子机构信息 - 删除当前机构后,更新当前父节点下的剩余机构节点排序 - 删除机构后,判断当前父节点是否还有其他子机构,没有则更新父机构为叶子节点 - 加入redis分布式锁(key=org_tree_key),因为存在排序字段,所以需要加入分布式锁控制 - 清除缓存中所有的用户信息,这个是必须的,因为要实时更新机构信息 - 组织机构更新 - 检查是否存在相同组织名称或相同组织代码,存在则不允许更新 - 若当前机构状态不可用,需要删除用户与机构关联关系(包括子菜单),更新所有子菜单的状态不可用 - 设置机构状态可用时,先检查上级机构是否可用,上级机构不可用的情况下,此次更新失败并返回提示到前端 - 加入redis分布式锁(key=org_tree_key),因为存在排序字段及可用状态等信息,所以需要加入分布式锁控制 - 清除缓存中所有的用户信息,这个是必须的,因为要实时更新机构信息 2. 提供单个组织更新排序功能(在页面上可以修改菜单排序,仅限于当前父节点下的组织机构排序) 3. 提供组织机构树结构的接口(用于在页面上的树形展示) ##### 字典管理(DictController) 1. 提供增删查改等基本功能 - 新增或更新时,不允许保存在库内已存在的相同字段名称和字段值的数据