当前位置: 首页 > news >正文

医院网站建设报告青岛房产网58同城网

医院网站建设报告,青岛房产网58同城网,免费软件 全免费,网站开发 访问速度慢文章目录 用户-角色-菜单-资源 各表关系图菜单 和 路由菜单表及分析分析 /api/admin/user/menus接口MenuServiceImpl#listUserMenus接口返回示例及分析 前端代码分析menu.jsSideBar.vue 接口权限控制资源表 及 分析分析 WebSecurityConfig权限控制整体流程先说登录UserDetailsS… 文章目录 用户-角色-菜单-资源 各表关系图菜单 和 路由菜单表及分析分析 /api/admin/user/menus接口MenuServiceImpl#listUserMenus接口返回示例及分析 前端代码分析menu.jsSideBar.vue 接口权限控制资源表 及 分析分析 WebSecurityConfig权限控制整体流程先说登录UserDetailsServiceImpl 再看权限控制自定义FilterInvocationSecurityMetadataSourceImpl自定义AccessDecisionManagerImpl 会话管理限制单个用户登录人数挤掉登录 或 阻止登录踢人下线注册会话会话监听 用户-角色-菜单-资源 各表关系图 菜单 和 路由 菜单表及分析 分析 要形成右边这种菜单需要2部分来做支撑。 第一部分需要构建出菜单之间的父子级关系出来。 在上表中通过id与parent_id就可以构建出来但是应当注意到它这种似乎没有做超过2级的菜单。它的这种第一级要么是菜单要么是目录第二级只能是菜单不能是目录。也就是说目录下面不能是目录只能是菜单目前的前端vue项目里面没有对菜单做递归。 第二部分需要嵌套路由支持。 也就是要做到后台管理系统的这种布局在切换菜单的时候右侧主区域切换到不同的组件需要嵌套路由作支撑。在考虑动态路由的时候就不要考虑父子级之间的关系只需要知道要展示到主区域的组件在vue项目里面的路径vue组件所在项目路径-表中的component字段以及该路由组件的路径vue组件对应的路由路径-表中的path字段 /api/admin/user/menus接口 MenuServiceImpl#listUserMenus Override public ListUserMenuDTO listUserMenus() {// 查询用户菜单信息//仅通过tb_user_role、tb_role_menu、tb_menu连表查询 用户拥有的角色 有哪些菜单ListMenu menuList menuDao.listMenusByUserInfoId(UserUtils.getLoginUser().getUserInfoId());// 获取目录列表//仅返回上一步查询到的菜单中parent_id为null的菜单ListMenu catalogList listCatalog(menuList);// 获取目录下的子菜单//将相同parent_id的菜单进行分组以parent_id作为key放入map中// 从这里就看出来了它不支持多级菜单了MapInteger, ListMenu childrenMap getMenuMap(menuList);// 转换前端菜单格式return convertUserMenuList(catalogList, childrenMap); }private ListUserMenuDTO convertUserMenuList(ListMenu catalogList, MapInteger, ListMenu childrenMap) {// 遍历每个parent_id为null的菜单// - 如果通过上面构建的map中能找到它的子菜单// 那么就把它当作多级菜单处理(认为是目录)// 将它的子菜单排序并添加到children属性中// - 如果没有找到就作为菜单处理认为是菜单创建一个UserMenuDTO并设置component为Layoutpath为菜单的path// 将此菜单path会被置为空字符串这个设置空字符串是有意义的添加到刚刚创建的UserMenuDTO的children中// 也就是说如果一级是菜单会把它包到里面去// 整个过程没有使用递归 或者 通过构建map的方式 构建多级菜单但是如果是一级菜单它是会把它包一层的return catalogList.stream().map(item - {// 获取目录UserMenuDTO userMenuDTO new UserMenuDTO();ListUserMenuDTO list new ArrayList();// 获取目录下的子菜单ListMenu children childrenMap.get(item.getId());if (CollectionUtils.isNotEmpty(children)) {// 多级菜单处理userMenuDTO BeanCopyUtils.copyObject(item, UserMenuDTO.class);list children.stream().sorted(Comparator.comparing(Menu::getOrderNum)).map(menu - {UserMenuDTO dto BeanCopyUtils.copyObject(menu, UserMenuDTO.class);dto.setHidden(menu.getIsHidden().equals(TRUE));return dto;}).collect(Collectors.toList());} else {// 一级菜单处理userMenuDTO.setPath(item.getPath());userMenuDTO.setComponent(COMPONENT); // Layouotlist.add(UserMenuDTO.builder().path().name(item.getName()).icon(item.getIcon()).component(item.getComponent()).build());}userMenuDTO.setHidden(item.getIsHidden().equals(TRUE));userMenuDTO.setChildren(list);return userMenuDTO;}).collect(Collectors.toList()); }接口返回示例及分析 观察下面的 首页 和 个人中心 的确是被包了一层刚刚提到 前端需要 侧边栏菜单 和 添加动态路由那么这里只提供一个接口的话并且里面没有分 菜单 和 路由那么前端势必就要自己组装 出合适的数据格式了。 大致猜想下这2部分内容该如何组装出来 路由首先分析路由这个比较简单从下面的数据返回就可以看出来它实际上已经大致和vue-router所需要的路由类似了只需要把component的部分通过异步组件加载方式把它导入进去就可以了。比如下面的首页当匹配到/就会默认展示Layout然后由于里面有一个path为空字符串的子路由vue-router会把这个子路由渲染到Layout的路由出口的地方。里面还有个小问题比如说下面的文章管理它的path是/article-submenu那我直接在地址上输入这个路径的话它是会渲染一个Layout组件然后路由出口是空的也就是主区域是空白的此时也可以给文章管理加一个path为空字符串的子路由让它显示一个默认的页面当然这个目录是点击不了的只是为了防止用户输入这个路劲而已。此处可以参考vue3后台管理系统、vue2异步组件菜单 侧边栏第一层级的菜单有可能是菜单也有可能是目录目录是不能点击的只能作展开/收缩。那如何区分它们呢因为使用element-ui组件去渲染左侧菜单那么就必须知道当前这个菜单有没有子菜单如果有子菜单用的是el-sub-menu如果直接是一个菜单的话那就是el-menu-item此处可参考vue3后台管理系统 的 使用el-menu创建侧边栏菜单 部分可以通过name来进行判断因为通过包了一层的方式生成的最外面的那层菜单的name是没有赋值的因此它肯定为null也就是说碰到为null的name的一级菜单直接拿这个菜单下面的一个子菜单这种只会存在一个子菜单比如首页、个人中心就是这样的。还有的就是有name的菜单就通过el-sub-menu把它渲染出来这样他就是一个目录了。 {flag:true,code:20000,message:操作成功,data:[{name:null,path:/,component:Layout,icon:null,hidden:false,children:[{name:首页,path:,component:/home/Home.vue,icon:el-icon-myshouye,hidden:null,children:null}]},{name:文章管理,path:/article-submenu,component:Layout,icon:el-icon-mywenzhang-copy,hidden:false,children:[{name:发布文章,path:/articles,component:/article/Article.vue,icon:el-icon-myfabiaowenzhang,hidden:false,children:null},{name:修改文章,path:/articles/*,component:/article/Article.vue,icon:el-icon-myfabiaowenzhang,hidden:true,children:null},{name:文章列表,path:/article-list,component:/article/ArticleList.vue,icon:el-icon-mywenzhangliebiao,hidden:false,children:null},{name:分类管理,path:/categories,component:/category/Category.vue,icon:el-icon-myfenlei,hidden:false,children:null},{name:标签管理,path:/tags,component:/tag/Tag.vue,icon:el-icon-myicontag,hidden:false,children:null}]},{name:消息管理,path:/message-submenu,component:Layout,icon:el-icon-myxiaoxi,hidden:false,children:[{name:评论管理,path:/comments,component:/comment/Comment.vue,icon:el-icon-mypinglunzu,hidden:false,children:null},{name:留言管理,path:/messages,component:/message/Message.vue,icon:el-icon-myliuyan,hidden:false,children:null}]},{name:用户管理,path:/users-submenu,component:Layout,icon:el-icon-myyonghuliebiao,hidden:false,children:[{name:用户列表,path:/users,component:/user/User.vue,icon:el-icon-myyonghuliebiao,hidden:false,children:null},{name:在线用户,path:/online/users,component:/user/Online.vue,icon:el-icon-myyonghuliebiao,hidden:false,children:null}]},{name:权限管理,path:/permission-submenu,component:Layout,icon:el-icon-mydaohanglantubiao_quanxianguanli,hidden:false,children:[{name:角色管理,path:/roles,component:/role/Role.vue,icon:el-icon-myjiaoseliebiao,hidden:false,children:null},{name:接口管理,path:/resources,component:/resource/Resource.vue,icon:el-icon-myjiekouguanli,hidden:false,children:null},{name:菜单管理,path:/menus,component:/menu/Menu.vue,icon:el-icon-mycaidan,hidden:false,children:null}]},{name:系统管理,path:/system-submenu,component:Layout,icon:el-icon-myshezhi,hidden:false,children:[{name:网站管理,path:/website,component:/website/Website.vue,icon:el-icon-myxitong,hidden:false,children:null},{name:页面管理,path:/pages,component:/page/Page.vue,icon:el-icon-myyemianpeizhi,hidden:false,children:null},{name:友链管理,path:/links,component:/friendLink/FriendLink.vue,icon:el-icon-mydashujukeshihuaico-,hidden:false,children:null},{name:关于我,path:/about,component:/about/About.vue,icon:el-icon-myguanyuwo,hidden:false,children:null}]},{name:相册管理,path:/album-submenu,component:Layout,icon:el-icon-myimage-fill,hidden:false,children:[{name:相册列表,path:/albums,component:/album/Album.vue,icon:el-icon-myzhaopian,hidden:false,children:null},{name:照片管理,path:/albums/:albumId,component:/album/Photo.vue,icon:el-icon-myzhaopian,hidden:true,children:null},{name:照片回收站,path:/photos/delete,component:/album/Delete.vue,icon:el-icon-myhuishouzhan,hidden:true,children:null}]},{name:说说管理,path:/talk-submenu,component:Layout,icon:el-icon-mypinglun,hidden:false,children:[{name:发布说说,path:/talks,component:/talk/Talk.vue,icon:el-icon-myfabusekuai,hidden:false,children:null},{name:说说列表,path:/talk-list,component:/talk/TalkList.vue,icon:el-icon-myiconfontdongtaidianji,hidden:false,children:null},{name:修改说说,path:/talks/:talkId,component:/talk/Talk.vue,icon:el-icon-myshouye,hidden:true,children:null}]},{name:日志管理,path:/log-submenu,component:Layout,icon:el-icon-myguanyuwo,hidden:false,children:[{name:操作日志,path:/operation/log,component:/log/Operation.vue,icon:el-icon-myguanyuwo,hidden:false,children:null}]},{name:null,path:/setting,component:Layout,icon:null,hidden:false,children:[{name:个人中心,path:,component:/setting/Setting.vue,icon:el-icon-myuser,hidden:null,children:null}]}] }前端代码分析 menu.js 下面的代码只遍历了2层只处理了图标 和 路由的组件异步加载和 Layout的字符串转为实际的Layout组件这些都是vue-router的要求。路由 和 菜单 用的 是同一份数据。此处可与vue3后台管理系统 # 调整路由处作对比学习感觉的确他的更加灵活一点他的可以不同path的路径都可以用Layout作为App.vue的路由出口展示的组件。我的是直接就当作Layout的子路由了但更加简单但有一点必须作为前提那就是一点要跟着vue-router的用法走这个是大前提所以做的时候肯定需要先把静态路由搭建出来确认没问题之后再搞动态路由。 import Layout from /layout/index.vue; import router from ../../router; import store from ../../store; import axios from axios; import Vue from vue;export function generaMenu() {// 查询用户菜单axios.get(/api/admin/user/menus).then(({ data }) {if (data.flag) {var userMenuList data.data;userMenuList.forEach(item {if (item.icon ! null) {item.icon iconfont item.icon;}if (item.component Layout) {item.component Layout;}if (item.children item.children.length 0) {item.children.forEach(route {route.icon iconfont route.icon;route.component loadView(route.component);});}});// 添加侧边栏菜单store.commit(saveUserMenuList, userMenuList);// 添加菜单到路由router.addRoutes(userMenuList);} else {Vue.prototype.$message.error(data.message);router.push({ path: /login });}}); }export const loadView view {// 路由懒加载return resolve require([/views${view}], resolve); };SideBar.vue 此处可对照 vue3后台管理系统 # 使用el-menu创建侧边栏菜单下面只做了2级遍历。多级菜单实现可参考 vue3后台管理系统 # 创建TreeMenu.vue递归组件 templatedivel-menuclassside-nav-barrouter:collapsethis.$store.state.collapse:default-activethis.$route.pathbackground-color#304156text-color#BFCBD9active-text-color#409EFFtemplate v-forroute of this.$store.state.userMenuList!-- 二级菜单 --template v-ifroute.name route.children !route.hiddenel-submenu :keyroute.path :indexroute.path!-- 二级菜单标题 --template slottitlei :classroute.icon /span{{ route.name }}/span/template!-- 二级菜单选项 --template v-for(item, index) of route.childrenel-menu-item v-if!item.hidden :keyindex :indexitem.pathi :classitem.icon /span slottitle{{ item.name }}/span/el-menu-item/template/el-submenu/template!-- 一级菜单 --template v-else-if!route.hiddenel-menu-item :indexroute.path :keyroute.pathi :classroute.children[0].icon /span slottitle{{ route.children[0].name }}/span/el-menu-item/template/template/el-menu/div /template接口权限控制 资源表 及 分析 分析 将系统中的每一controller里面的接口当作一个资源接口名称就是资源名称、接口访问路径就是资源url。每一个controller类也是一个资源它用来管理内部的接口作为它们的父资源父资源的parent_id为null也就是说里面只会存在2级关系。使用角色 去 关联 资源用户 去 关联 角色因此就可以确定一个用户拥有哪些资源。一个角色如果关联了某个controller下面的某个或者某几个资源那么它一定关联了这个controller资源也就是子关联了那么父也一定要关联。那个菜单也应如此但是我发现角色分配菜单那里选择了子菜单却没有自动勾选对应的父级菜单连父级菜单都没的话返回的就是空菜单。资源那里是正常的。当确定某个用户具有哪些角色就可以确定这个用户拥有了哪些资源其实就是拥有了哪些接口的访问权限接口的访问权限是通过security这个权限框架控制的并且博客中是做到了动态权限控制即新增或者修改资源、更新角色 与 资源的关系时项目不需要重启用户也不需要退出再登录即可按修改后的接口访问权限实时的生效但是用户登录后再修改这个用户的角色这个是不能实时生效的需要退出退出再登录。 WebSecurityConfig package com.minzheng.blog.config;import com.minzheng.blog.handler.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.session.HttpSessionEventPublisher;/*** Security配置类** author yezhiqiu* date 2021/07/29*/ Configuration EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;Autowiredprivate AuthenticationSuccessHandlerImpl authenticationSuccessHandler;Autowiredprivate AuthenticationFailHandlerImpl authenticationFailHandler;Autowiredprivate LogoutSuccessHandlerImpl logoutSuccessHandler;Beanpublic FilterInvocationSecurityMetadataSource securityMetadataSource() {return new FilterInvocationSecurityMetadataSourceImpl();}Beanpublic AccessDecisionManager accessDecisionManager() {return new AccessDecisionManagerImpl();}Beanpublic SessionRegistry sessionRegistry() {return new SessionRegistryImpl();}Beanpublic HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher();}/*** 密码加密** return {link PasswordEncoder} 加密方式*/Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 配置权限** param http http* throws Exception 异常*/Overrideprotected void configure(HttpSecurity http) throws Exception {// 配置登录注销路径http.formLogin().loginProcessingUrl(/login).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailHandler).and().logout().logoutUrl(/logout).logoutSuccessHandler(logoutSuccessHandler);// 配置路由权限信息http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessorFilterSecurityInterceptor() {Overridepublic O extends FilterSecurityInterceptor O postProcess(O fsi) {fsi.setSecurityMetadataSource(securityMetadataSource());fsi.setAccessDecisionManager(accessDecisionManager());return fsi;}}).anyRequest().permitAll().and()// 关闭跨站请求防护.csrf().disable().exceptionHandling()// 未登录处理.authenticationEntryPoint(authenticationEntryPoint)// 权限不足处理.accessDeniedHandler(accessDeniedHandler).and().sessionManagement().maximumSessions(20).sessionRegistry(sessionRegistry());}} 权限控制整体流程 先说登录 用户登录是通过Security配置的formLogin()配置的UsernamePasswordAuthenticationFilter这个过滤器来登录的这个过滤器会提取登录请求中的username,password请求参数交给认证管理器作认证而认证管理器是security默认配置的具体可以看AbstractAuthenticationFilterConfigurer#configure(B http)这里是从sharedObject中拿到AuthenticationManager设置到UsernamePasswordAuthenticationFilter过滤器中。而认证管理器是security它自己会默认创建一个并且它默认会去寻找spring容器中定义的UserDetailsService这个类型的bean具体见InitializeUserDetailsManagerConfigurer#configure(AuthenticationManagerBuilder auth)里面的配置过程比较复杂见Spring Security框架配置运行流程完整分析需要花费不少的时间才能看明白配置过程但只需要知道它默认会去寻找定义的UserDetailsService实现类bean设置到AuthenticationManager认证管理中而formLogin()配置的UsernamePasswordAuthenticationFilter正是需要认证管理器因此实际的查询用户操作就交给了我们配置的UserDetailsService这个bean中。登录代码如下实际上就是查询出了当前用户拥有的角色、当前用户点赞过哪些文章、评论、说说以及ip地址和来源地和浏览器等 UserDetailsServiceImpl Service public class UserDetailsServiceImpl implements UserDetailsService {Autowiredprivate UserAuthDao userAuthDao;Autowiredprivate UserInfoDao userInfoDao;Autowiredprivate RoleDao roleDao;Autowiredprivate RedisService redisService;Resourceprivate HttpServletRequest request;Overridepublic UserDetails loadUserByUsername(String username) {if (StringUtils.isBlank(username)) {throw new BizException(用户名不能为空);}// 查询账号是否存在UserAuth userAuth userAuthDao.selectOne(new LambdaQueryWrapperUserAuth().select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType).eq(UserAuth::getUsername, username));if (Objects.isNull(userAuth)) {throw new BizException(用户名不存在!);}// 封装登录信息return convertUserDetail(userAuth, request);}/*** 封装用户登录信息** param user 用户账号* param request 请求* return 用户登录信息*/public UserDetailDTO convertUserDetail(UserAuth user, HttpServletRequest request) {// 查询账号信息UserInfo userInfo userInfoDao.selectById(user.getUserInfoId());// 查询账号角色ListString roleList roleDao.listRolesByUserInfoId(userInfo.getId());// 查询账号点赞信息SetObject articleLikeSet redisService.sMembers(ARTICLE_USER_LIKE userInfo.getId());SetObject commentLikeSet redisService.sMembers(COMMENT_USER_LIKE userInfo.getId());SetObject talkLikeSet redisService.sMembers(TALK_USER_LIKE userInfo.getId());// 获取设备信息String ipAddress IpUtils.getIpAddress(request);String ipSource IpUtils.getIpSource(ipAddress);UserAgent userAgent IpUtils.getUserAgent(request);// 封装权限集合return UserDetailDTO.builder().id(user.getId()).loginType(user.getLoginType()).userInfoId(userInfo.getId()).username(user.getUsername()).password(user.getPassword()).email(userInfo.getEmail()).roleList(roleList).nickname(userInfo.getNickname()).avatar(userInfo.getAvatar()).intro(userInfo.getIntro()).webSite(userInfo.getWebSite()).articleLikeSet(articleLikeSet).commentLikeSet(commentLikeSet).talkLikeSet(talkLikeSet).ipAddress(ipAddress).ipSource(ipSource).isDisable(userInfo.getIsDisable()).browser(userAgent.getBrowser().getName()).os(userAgent.getOperatingSystem().getName()).lastLoginTime(LocalDateTime.now(ZoneId.of(SHANGHAI.getZone()))).build();}} 再看权限控制 看这一部分之前需要先搞懂security他的工作原理它是基于filter过滤器实现的可以先看Security源码学习笔记OAuth2 # 第十节部分 关于FilterIntercetor的介绍。配置的起源在于使用http.authorizeRequests()修改了其中默认配置的组件而替换成了博客中使用的组件。 自定义FilterInvocationSecurityMetadataSourceImpl security会把当前访问的资源请求对象封装为FilterInvocation把它交给SecurityMetadataSource#getAttributes方法以获得访问当前资源请求对象所需要的权限。 下面代码的过程就是在通过ant-style的路径匹配根据配置的资源url查询到访问当前的资源可以是哪些角色也就是说用户必须要有返回中的任一角色才能访问FilterInvocation否则不允许访问。 下面还有一个返回指定“disable”固定字符串的意思是没有任何角色能够访问这个资源除非你有一个disable的角色但这个角色显然不存在也就是没人可以访问这个资源。 还有一点是在查询之前给了一个钩子如果resourceRoleList为null也就是有地方修改了这个属性为null那就重新加载这个resourceList。 package com.minzheng.blog.handler;import com.minzheng.blog.dao.RoleDao; import com.minzheng.blog.dto.ResourceRoleDTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct; import java.util.Collection; import java.util.List;/*** 接口拦截规则** author yezhiqiu* date 2021/07/27*/ Component public class FilterInvocationSecurityMetadataSourceImpl implements FilterInvocationSecurityMetadataSource {/*** 资源角色列表*/private static ListResourceRoleDTO resourceRoleList;Autowiredprivate RoleDao roleDao;/*** 加载资源角色信息*/PostConstructprivate void loadDataSource() {resourceRoleList roleDao.listResourceRoles();}/*** 清空接口角色信息*/public void clearDataSource() {resourceRoleList null;}Overridepublic CollectionConfigAttribute getAttributes(Object object) throws IllegalArgumentException {// 修改接口角色关系后重新加载if (CollectionUtils.isEmpty(resourceRoleList)) {this.loadDataSource();}FilterInvocation fi (FilterInvocation) object;// 获取用户请求方式String method fi.getRequest().getMethod();// 获取用户请求UrlString url fi.getRequest().getRequestURI();AntPathMatcher antPathMatcher new AntPathMatcher();// 获取接口角色信息若为匿名接口则放行若无对应角色则禁止for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) {if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) resourceRoleDTO.getRequestMethod().equals(method)) {ListString roleList resourceRoleDTO.getRoleList();if (CollectionUtils.isEmpty(roleList)) {return SecurityConfig.createList(disable);}return SecurityConfig.createList(roleList.toArray(new String[]{}));}}return null;}Overridepublic CollectionConfigAttribute getAllConfigAttributes() {return null;}Overridepublic boolean supports(Class? aClass) {return FilterInvocation.class.isAssignableFrom(aClass);}}自定义AccessDecisionManagerImpl 在上一步获取到了访问某一资源需要的权限后接下来按照security的尿性它会交给访问决策管理器然后决策管理器会交给投票器然后再根据投票结果确定是否能访问当前资源。 但是博客中是直接查询到当前用户拥有的权限然后看这些权限有没有符合要求的一旦发现有符合要求的直接返回否则抛出异常。这里所说的权限是要看UserDetailDTO#getAuthorities它实现了UserDetails接口从该方法中可以看出就是指的角色。 Component public class AccessDecisionManagerImpl implements AccessDecisionManager {Overridepublic void decide(Authentication authentication, Object o, CollectionConfigAttribute collection) throws AccessDeniedException, InsufficientAuthenticationException {// 获取用户权限列表ListString permissionList authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());for (ConfigAttribute item : collection) {if (permissionList.contains(item.getAttribute())) {return;}}throw new AccessDeniedException(没有操作权限);}Overridepublic boolean supports(ConfigAttribute configAttribute) {return true;}Overridepublic boolean supports(Class? aClass) {return true;} }会话管理 用户登录完成后在后面的请求中如何标识到这个用户呢以前的时候用的是cookie前后端分离项目中一般用的是请求头如Authorization。但是在博客中使用的是cookie做的会话管理后面的请求都是通过cookie来标识用户的也就是如果用户仅用了cookie那就无法登录可以登录成功但是后面再发的请求就报用户未登录也就是说只要删除某个cookie那就把对应的用户给踢下线了。Security当用户登录完成后会把用户认证的对象存入SecurityContext中而SecurityContext会被存入HttpSession中在security遇到一个请求时第二个处理的过滤器就是SecurityContextPersistenceFilter它就是负责从会话前端须传入cookie中获取登陆时存入的SecurityContext然后把它设置到当前线程上下文中。这也就是说如果我们需要改成请求头的方式而不是用cookie则需要自己实现这一段逻辑。 明白了上面的逻辑后可以参考再参考下会话管理的源码Springsecurity会话管理与配置 它也是通过过滤器来实现的通过http.sessionManagement()添加的 一个SessionManagementFilter会话管理过滤器它负责使用自己的securityContextRepository来对每个请求验证是否有SecurityContext如果有直接放行如果没有则尝试获取绑定到当前线程的Authentcation如果能获取到则让会话认证策略处理并存入自己的securityContextRepository。一个ConcurrentSessionFilter过滤器。其中SessionManagementFilter 限制单个用户登录人数挤掉登录 或 阻止登录 这里似乎发现了原博客中的一点小问题需要重写UserDetailDTO的hashcode和equals方法。不重写的话达不到控制单个用户会话数量的效果。 在SessionManagementConfigurer#init(H http)中会去获取会话认证策略如果用户有设置maximumSessions这个属性那么就会添加一个会话并发控制的会话策略到会话认证策略中并且把它设置到sharedObject中。 而在formLogin()配置的AbstractAuthenticationFilterConfigurer在configure方法中是有去从sharedObject拿会话认证策略的显然init方法执行的顺序在configure方法之前所以是能拿到的所以用户在UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter登录之后就会来到ConcurrentSessionControlAuthenticationStrategy#onAuthentication方法里面去判断数量是否超过允许的最大会话数量如果超过是否阻止当前登录或者移除最近未使用的会话并让它们失效注意这个失效只是标记了SessionInformation的expired属性为true真正的会话失效在ConcurrentSessionFilter的doFilter方法中。 踢人下线 踢人下线的道理跟上面是一样的只要从sessionRegistry中找到对应的用户id的SessionInformation会话信息标记它们失效下次这些用户请求时在ConcurrentSessionFilter的doFilter方法中会去检查它们如果被标记为失效那就走失效逻辑。 注册会话 上面提到了会话删除那么会话是什么时候注册进去的呢同样是在SessionManagementConfigurer#init(H http)中获取会话认证策略时除了会添加那个ConcurrentSessionControlAuthenticationStrategy也会添加RegisterSessionAuthenticationStrategy那么在登录的时候在AbstractAuthenticationProcessingFilter就会来到RegisterSessionAuthenticationStrategy中去注册一个会话然后放入sessionRegistry中这样就起到了管理会话的作用 会话监听 当用户长时间无操作时用户的这个会话应当要失效。web中的HttpSession会话当长时间无访问时(默认30分钟)就会自己invalidate失效掉。那么security中既然用SessionRegistryImpl去维护HttpSession那么它应当要监听会话销毁时间因此在博客中注册了HttpSessionEventPublisher这个bean它实现了HttpSessionListener接口来监听httpsession的各种事件然后把事件通过spring容器发布出来。而SessionRegistry则应当要监听这些会话事件可以看下SessionRegistryImpl的实现它实现了ApplicationListener接口监听的泛型事件类型为AbstractSessionEvent。这里面其实就是用到了spring的事件机制。 public class SessionRegistryImpl implements SessionRegistry, ApplicationListenerAbstractSessionEvent {protected final Log logger LogFactory.getLog(SessionRegistryImpl.class);// principal:Object,SessionIdSetprivate final ConcurrentMapObject, SetString principals;// sessionId:Object,SessionInformationprivate final MapString, SessionInformation sessionIds;public SessionRegistryImpl() {this.principals new ConcurrentHashMap();this.sessionIds new ConcurrentHashMap();}public SessionRegistryImpl(ConcurrentMapObject, SetString principals,MapString, SessionInformation sessionIds) {this.principals principals;this.sessionIds sessionIds;}Overridepublic ListObject getAllPrincipals() {return new ArrayList(this.principals.keySet());}Overridepublic ListSessionInformation getAllSessions(Object principal, boolean includeExpiredSessions) {SetString sessionsUsedByPrincipal this.principals.get(principal);if (sessionsUsedByPrincipal null) {return Collections.emptyList();}ListSessionInformation list new ArrayList(sessionsUsedByPrincipal.size());for (String sessionId : sessionsUsedByPrincipal) {SessionInformation sessionInformation getSessionInformation(sessionId);if (sessionInformation null) {continue;}if (includeExpiredSessions || !sessionInformation.isExpired()) {list.add(sessionInformation);}}return list;}Overridepublic SessionInformation getSessionInformation(String sessionId) {Assert.hasText(sessionId, SessionId required as per interface contract);return this.sessionIds.get(sessionId);}Overridepublic void onApplicationEvent(AbstractSessionEvent event) {if (event instanceof SessionDestroyedEvent) {SessionDestroyedEvent sessionDestroyedEvent (SessionDestroyedEvent) event;String sessionId sessionDestroyedEvent.getId();removeSessionInformation(sessionId);}else if (event instanceof SessionIdChangedEvent) {SessionIdChangedEvent sessionIdChangedEvent (SessionIdChangedEvent) event;String oldSessionId sessionIdChangedEvent.getOldSessionId();if (this.sessionIds.containsKey(oldSessionId)) {Object principal this.sessionIds.get(oldSessionId).getPrincipal();removeSessionInformation(oldSessionId);registerNewSession(sessionIdChangedEvent.getNewSessionId(), principal);}}}Overridepublic void refreshLastRequest(String sessionId) {Assert.hasText(sessionId, SessionId required as per interface contract);SessionInformation info getSessionInformation(sessionId);if (info ! null) {info.refreshLastRequest();}}Overridepublic void registerNewSession(String sessionId, Object principal) {Assert.hasText(sessionId, SessionId required as per interface contract);Assert.notNull(principal, Principal required as per interface contract);if (getSessionInformation(sessionId) ! null) {removeSessionInformation(sessionId);}if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format(Registering session %s, for principal %s, sessionId, principal));}this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));this.principals.compute(principal, (key, sessionsUsedByPrincipal) - {if (sessionsUsedByPrincipal null) {sessionsUsedByPrincipal new CopyOnWriteArraySet();}sessionsUsedByPrincipal.add(sessionId);this.logger.trace(LogMessage.format(Sessions used by %s : %s, principal, sessionsUsedByPrincipal));return sessionsUsedByPrincipal;});}Overridepublic void removeSessionInformation(String sessionId) {Assert.hasText(sessionId, SessionId required as per interface contract);SessionInformation info getSessionInformation(sessionId);if (info null) {return;}if (this.logger.isTraceEnabled()) {this.logger.debug(Removing session sessionId from set of registered sessions);}this.sessionIds.remove(sessionId);this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) - {this.logger.debug(LogMessage.format(Removing session %s from principals set of registered sessions, sessionId));sessionsUsedByPrincipal.remove(sessionId);if (sessionsUsedByPrincipal.isEmpty()) {// No need to keep object in principals Map anymorethis.logger.debug(LogMessage.format(Removing principal %s from registry, info.getPrincipal()));sessionsUsedByPrincipal null;}this.logger.trace(LogMessage.format(Sessions used by %s : %s, info.getPrincipal(), sessionsUsedByPrincipal));return sessionsUsedByPrincipal;});}}
http://www.w-s-a.com/news/277355/

相关文章:

  • 建设银行网站 无法访问东莞淘宝运营
  • 做家电网站做网站美工需要会什么软件
  • 深圳营销型定制网站开发1000建设银行网站特点分析
  • 安装网站系统重庆知名网站
  • 巴彦淖尔市 网站建设怀化北京网站建设
  • 内部网站管理办法建立网站后台
  • 自学考试网站建设与管理郑州网站建设开拓者
  • 宁夏制作网站公司慈溪建设集团网站
  • 国家企业官方网站查询系统站酷设计网站官网入口文字设计
  • 彩票网站开发制作需要什么wordpress连接微博专业版v4.1
  • 孝感建设银行官网站百度一下你就知道啦
  • 做网站如何做视频广告制作公司简介怎么写
  • 做网站 买空间商务网站内容建设包括
  • 萝岗网站建设为什么点不开网站
  • 惠州网站制作询问薇北京网站建设最便宜的公司
  • 注册网站英语怎么说wordpress 3.8.3
  • 甘肃张掖网站建设网站开发软件是什么专业
  • 海口省建设厅网站网站数据库怎么做同步
  • 做网站建设月收入多少app开发公司广州英诺
  • 新闻播报最新网站优化外包费用
  • wordpress分页出现404最专业的seo公司
  • 连云港网站建设电话连云港市建设局网站
  • 平面设计网站有哪些比较好drupal网站建设 北京
  • 健康资讯网站模板网页价格表
  • 2008发布asp网站宝安建网站的公司
  • 郑州市城市建设管理局网站制作公司网站 优帮云
  • 网站开发 瀑布结构普陀网站建设
  • 12380网站建设情况汇报plone vs wordpress
  • c 网站开发数据库连接与wordpress类似的都有哪些
  • 状元村建设官方网站长春做网站seo的