旭锋科技制造信息管理系统--客户端主页面

客户端主页面

    • 客户端home页面
      • (一)业务功能
      • (二)前端页面构建
        • 2.1 html实现
        • 2.2 CSS实现
        • 2.3 Js实现
      • (三)左侧菜单栏的动态构建实现
        • 3.1 业务流程
        • 3.2 业务实现
          • 3.2.1 获取当前登陆用户信息及权限,构建左侧菜单栏
          • 3.2.2 服务器端校验token
          • 3.2.3 缓存从数据库中查询的数据保到redis中。
          • 3.2.4 获取当前用户拥有权限的菜单
          • 3.2.5 获取当前用户信息,并保存到`sessionStorage`中
      • (四) 构建动态面包屑
        • 4.1业务流程
        • 4.2 业务实现
          • 4.2.1 设置保存变量和处理方法
          • 4.2.2设置监听器。
          • 4.2.3 初始化默认激活菜单项和面包屑数据。
          • 4.2.4构建动态面包屑
      • (五)用户退出业务
        • 5.1 业务流程
        • 5.2 业务实现
          • 5.2.1 下列菜单添加点击事件处理函数。
          • 5.2.2 在Home的Vue对象的method属性中定义该处理函数。
          • 5.2.3 在method属性中定义`currentUserLogout()`
          • 5.2.4 服务器接收退出并处理请求
      • (六)权限控制
        • 6.1 业务流程
        • 6.2 业务实现
          • 6.2.1 前端业务实现
          • 6.2.2 后端业务实现
          • 6.2.3 前端接收权限不足响应时处理。

客户端home页面

(一)业务功能

在home页面加载完成后,获取当前用户。

  • 根据用户名请求用户名所有的权限菜单信息。完成左侧菜单栏的动态构建。
  • 根据用户信息,构建用户头像等。
  • 动态构建面包屑
  • 用户退出

(二)前端页面构建

2.1 html实现
<template>
  <div class="home">
    <el-container>

      <el-menu class="el-menu-vertical-demo" background-color="#060037" text-color="#ffffff" :unique-opened="true" :router="true" :collapse="isCollapse">
        <el-menu-item>
          <img src="../assets/logo.png" width="24" height="24"/>
          <span style="font-family: 楷体; font-size: 18px; color: #E3E63C; ">旭锋信息管理系统span>
        el-menu-item>
      el-menu>

      <el-container>

        <el-header>
          <el-row type="flex" justify="space-between">
            <el-row :span="2" align="center">
              <i v-if="isCollapse" class="fas fa-indent" style="font-size: 36px; margin-top: 8px" @click="isCollapse=!isCollapse">i>
              <i v-else class="fas fa-outdent" style="font-size: 36px; margin-top: 8px" @click="isCollapse=!isCollapse">i>
            el-row>
            <el-row :span="12" align="end">
              <el-dropdown trigger="click" @command="handleCommand" size="small">
                <div style="margin-top: 10px">
                  <a-avatar icon="user" shape="square" />
                  <span class="el-dropdown-link" style="color: #ffffff">
                    {{username}}<i class="el-icon-arrow-down el-icon--right">i>
                  span>
                div>
                <el-dropdown-menu slot="dropdown" style="width: 120px">
                  <el-dropdown-item command="psnCenter">个人中心el-dropdown-item>
                  <el-dropdown-item divided command="logout">安全退出el-dropdown-item>
                el-dropdown-menu>
              el-dropdown>
            el-row>
          el-row>
        el-header>

        <div style="height: 25px; background-color: #7dbcea; box-shadow: 0 4px 15px  #888888;">
          <el-row type="flex" justify="end">
            <el-breadcrumb separator-class="el-icon-arrow-right" style="margin-top:6px; margin-right: 20px">
              <el-breadcrumb-item :to="{ path: '/' }">首页el-breadcrumb-item>
              <el-breadcrumb-item>活动管理el-breadcrumb-item>
              <el-breadcrumb-item>活动列表el-breadcrumb-item>
              <el-breadcrumb-item>活动详情el-breadcrumb-item>
            el-breadcrumb>
          el-row>
        div>

        <el-main>
          <router-view>router-view>
        el-main>

        <el-footer>Footerel-footer>
      el-container>
    el-container>
  div>
template>
2.2 CSS实现

2.3 Js实现
<script>
// @ is an alias to /src

export default {
  name: 'Home',
  data() {
    return {
      isCollapse: false,
      username: "[email protected]"
    };
  },
  methods: {
    handleCommand(command)
    {
      this.$message('click on item ' + command);
    }
  }
}
</script>

(三)左侧菜单栏的动态构建实现

3.1 业务流程
3.2 业务实现
3.2.1 获取当前登陆用户信息及权限,构建左侧菜单栏

3.2.1.1 声明变量用于保存菜单数据

data() {
    return {
      isCollapse: false,
      username: "",
      /*保存菜单数据*/
      menuData: []
    };
  },

3.2.1.2 定义请求用户信息及授权菜单数据方法

/*请求当前用户所拥有权限的未禁用的菜单类型的菜单*/
    async getCurrentUserMenus()
    {
      const {data : res} = await this.$axios.get(`/menu/usermenu/${this.username}`);
      if(res.status == 2000 && res.data && res.data.length > 0)
      {
        this.menuData = res.data;
      }
    },
    /*重新获取当前用户信息包含用户拥有的授权标识*/
    async reloadCurrentUser()
    {
      let username;
      if(sessionStorage.getItem("username"))
      {
        username = sessionStorage.getItem("username")
      }

      const { data: res } = await this.$axios.get(`/relogin/${username}`);
      if(2000 === res.status && res.data)
      {
        sessionStorage.setItem("currentUser", JSON.stringify(res.data));
      }
      else
      {
        this.$message.error(res.message);
      }
    },

3.2.1.3 在Vue对象创建完成后,自动请求菜单

created(){
    this.username = sessionStorage.getItem("username");
    /*获取当前登陆用户信息*/
    this.reloadCurrentUser();
    /*获取构建当前用户左侧菜单栏的数据*/
    this.getCurrentUserMenus();
    /*当页面重新构建时即设置当前激活菜单项及获取面包屑相关数据*/
    this.setDefaultsAliveMenuAndBreadData();
  },
3.2.2 服务器端校验token
  • 校验方式:使用过滤器。

3.2.2.1 三大JAVAEE组件

  • 过滤器

    • 概述

      • Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的服务器端程序,主要的用途是过滤字符编码、做一些业务逻辑判断等。其工作原理是,只要配置好要拦截的客户端请求,它都会帮你拦截到请求,此时你就可以对请求或响应(Request、Response)统一设置编码,简化操作;同时还可以进行逻辑判断,如用户是否已经登录、有没有权限访问该页面等等工作,它是随你的web应用启动而启动的,只初始化一次,以后就可以拦截相关的请求,只有当你的web应用停止或重新部署的时候才能销毁。
    • 作用

      • 请求和响应的过滤,传入的request,response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者springMVC的DispatcherServlet进行业务逻辑。
    • 在javax.servlet.Filter接口中定义了3个方法:

      • void init(FilterConfig filterConfig) 用于完成过滤器的初始化

      • void destroy() 用于过滤器销毁前,完成某些资源的回收

      • void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) 实现过滤功能,该方法对每个请求增加额外的处理。

    • 自定义Filter实现

      • 第一步:自定义Filter类并实现javax.servlet.Filter接口。

        package com.jt.filter;
        import javax.servlet.*;
        import java.io.IOException;
        /**
         * javaee规范中的过滤器,对请求和响应数据进行过滤
         * 1)统一数据的编码
         * 2)统一数据格式校验 (今日头条的灵犬系统)
         * 3)统一身份认证
         */
        public class DemoFilter implements Filter {
            @Override
            public void doFilter(ServletRequest servletRequest,
                                 ServletResponse servletResponse,
                                 FilterChain filterChain) throws IOException, ServletException {
                System.out.println("==doFilter==");
                servletRequest.setCharacterEncoding("UTF-8");
                String id= servletRequest.getParameter("id");
                System.out.println("id="+id);
                filterChain.doFilter(servletRequest,servletResponse);
            }
        }
        
        
      • 第二步:将自定义Filter类的对象注册到项目中

        package com.jt.config;
        
        /**
         * 在这里配置javaee规范中的三大组件
         */
        @Configuration
        public class ComponentConfig {
        
            ....
            //注册过滤器
            @Bean
            public FilterRegistrationBean filterRegistrationBean(){
                FilterRegistrationBean bean=
                        new FilterRegistrationBean(new DemoFilter());
                bean.addUrlPatterns("/hello");//对哪个请求进行处理
                return bean;
            }
        
        }
        
    • OncePerRequestFilter

      • Spring框架对javax.servlet.Filter封装。
      • OncePerRequestFilter,顾名思义,它能够确保在一次请求中只通过一次filter,而需要重复的执行。大家常识上都认为,一次请求本来就只filter一次,为什么还要由此特别限定呢,往往我们的常识和实际的实现并不真的一样,经过一番资料的查阅,此方法是为了兼容不同的web container,也就是说并不是所有的container都入我们期望的只过滤一次,servlet版本不同,执行过程也不同,因此,为了兼容各种不同运行环境和版本,默认filter继承OncePerRequestFilter是一个比较稳妥的选择。
  • 拦截器

    • 概念

      • 拦截器是在面向切面编程中应用的,就是在你的service或者一个方法前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。拦截器不是在web.xml配置的,比如struts在struts.xml配置,在springMVC在spring与springMVC整合的配置文件中配置。
    • 在springmvc中,定义拦截器要实现HandlerInterceptor接口,并实现该接口中提供的三个方法

      • preHandle方法:进入Handler方法之前执行。可以用于身份认证、身份授权。比如如果认证没有通过表示用户没有登陆,需要此方法拦截不再往下执行(return false),否则就放行(return true)。
      • postHandle方法:进入Handler方法之后,返回ModelAndView之前执行。可以看到该方法中有个modelAndView的形参。应用场景:从modelAndView出发:将公用的模型数据(比如菜单导航之类的)在这里传到视图,也可以在这里同一指定视图。
      • afterCompletion方法:执行Handler完成之后执行。应用场景:统一异常处理,统一日志处理等。
      • 在springmvc中,拦截器是针对具体的HandlerMapping进行配置的,也就是说如果在某个HandlerMapping中配置拦截,经过该 HandlerMapping映射成功的handler最终使用该拦截器。
  • 监听器

    • 概念

      • Servlet的监听器Listener,它是实现了javax.servlet.ServletContextListener接口的服务器端程序,它也是随web应用的启动而启动,只初始化一次,随web应用的停止而销毁。主要作用是:做一些初始化的内容添加工作、设置一些基本的内容、比如一些参数或者是一些固定的对象等等。
    • 原理

      • 在javax.servlet.ServletContextListener接口中定义了2种方法:

        void contextInitialized(ServletContextEvent sce) 监听器的初始化

        void contextDestroyed(ServletContextEvent sce) 监听器销毁

  • 过滤器与拦截器的执行流程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0XCCD48v-1638847005433)(./filepng/过滤器与拦截器.png)]

3.2.2.2 通过过滤器检验token

  • 自定义token校验过滤器

    @Component
    @PropertySource("classpath:/jwt.properties")
    @Slf4j
    public class JwtTokenFilter extends OncePerRequestFilter
    {
        private final Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class);
        @Value("${jwt.header}")
        private String tokenHeader;
        @Value("${jwt.head}")
        private String tokenHead;
        @Autowired
        private JWTUtils jwtUtils;
        @Autowired
        private UserDetailsService userDetailsService;
        /**
         * 校验token
         * */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
        {
    
            /**执行流程:*/
            /**
             * 1.由于所有请求都会经过此过滤器,但有些url不需要进行token校验,需要直接进入过滤链的下一个过滤器。
             * 放行/login与/captcha等url*/
            String url = request.getRequestURI();
            if("/login".equals(url) || "/captcha".equals(url))
            {
                /*该方法不能结束函数的后续语句的执行*/
                filterChain.doFilter(request, response);
                return;
            }
            /**2.通过request对象中获取请求携带的token.
             * 如果token存在,则继续校验流程;则校验是否是以自定义的tokenHead开始。两者都检验成功则进行下一项校验。
             * 如果不存在或者不是以自定义tokenHead,则响应前端,无效token,请重新登录。*/
            String token = request.getHeader(tokenHeader);
            if(null == token || !token.startsWith(tokenHead))
            {
                JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
                return;
            }
            /**3.校验token的主体
             * 如果token主体长度为0,则响应前端无效token
             * 如果长度不为0,则继续校验*/
            String tokenBody = token.substring(tokenHead.length());
            if(tokenBody.length() <= 0)
            {
                JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
                return;
            }
            String username = null;
            try
            {
                username = jwtUtils.getUserNameFromToken(tokenBody);
            }catch (Exception e)
            {
                logger.error(e.getMessage());
                JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.EXPIRED_TOKEN));
                return;
            }
            /**4.校验通过token获取的用户名是否为空。
             * 如果为空则说明token被篡改,为无效token
             * 如果不为空则进行下一步校验
             * */
            if (null == username)
            {
                JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
                return;
            }
            /**5.校验用户信息是否有效,即userDetailsService.loadUserByUsername()是否抛出异常,
             * 如果抛出异常说明token无效。
             * 如果未抛出异常,进行下一步校验。
             * */
            SysUserPojo userDetails;
            try{
                userDetails = (SysUserPojo) userDetailsService.loadUserByUsername(username);
            }catch (Exception e)
            {
                logger.error(e.getMessage());
                JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.ILLEGAL_TOKEN));
                return;
            }
            /**6.所有校验都通过后,构建 UsernamePasswordAuthenticationToken 对象
             * 该对象有两个构造函数:
             * 一个两个参数的,public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
             * 一个三个参数的。public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection authorities)
             * 当使用两参的构造,则该用户会自动设置为未登录,
             * 当使用三参的构造,则该用户会自动设置为已登录。
             * */
            UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());
            /*将被设置为已登录的UsernamePasswordAuthenticationToken 对象放到安全上下文中。此时该用户才会被认为是登录用户。*/
            SecurityContextHolder.getContext().setAuthentication(userToken);
            filterChain.doFilter(request, response);
        }
    }
    
  • 将自定义的token校验过滤器配置到SpringSecurity中

    • 修改SpringSecurity配置类如下:

      @Configuration
      public class MySecurityConfig extends WebSecurityConfigurerAdapter
      {
          @Autowired
          private MyUserDetailService myUserDetailService;
          @Autowired
          private JwtTokenFilter jwtTokenFilter;
          @Bean
          public PasswordEncoder passwordEncoder()
          {
              return new BCryptPasswordEncoder();
          }
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception
          {
              auth.userDetailsService(myUserDetailService)
                      .passwordEncoder(passwordEncoder());
          }
          @Override
          public void configure(WebSecurity web) throws Exception
          {
              web.ignoring().antMatchers("/login", "/captcha");
          }
          /*对http请求认证授权配置*/
          @Override
          protected void configure(HttpSecurity http) throws Exception
          {
              http.authorizeRequests()
                      /*配置所有请求都需要登录认证*/
                      .anyRequest().authenticated()
                      .and()
                      /*关闭跨站请求伪造*/
                      .csrf().disable()
                      /*配置session管理器*/
                      .sessionManagement()
                      /*由于使用jwt令牌来保存用户登录信息,因此不需要使用创建session来保存用户登录信息*/
                      .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                      .and()
                      /*将token校验过滤器添加在认证过滤器前*/
                      .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
              ;
          }
      }
      
  • 前响应信息构建成Json响应给前端

    • 由于过滤器执行在SpringMVC之前,因此需要将响应给前端信息自行构建成json串。

    • 方法:通过SpringMVC依赖的Jackson的ObjectMapper对象完成

    • 具体实现:

      package org.wjk.utils;
      
      import com.fasterxml.jackson.databind.ObjectMapper;
      import org.wjk.vo.ResPaging;
      
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.io.PrintWriter;
      
      public class JsonResUtils
      {
          public static void response(HttpServletRequest request, HttpServletResponse response, ResPaging res) throws IOException
          {
              /*设置响应头*/
              response.setCharacterEncoding("utf-8");
              response.setContentType("application/json");
              /*获取对象到JSON的映射器*/
              ObjectMapper mapper = new ObjectMapper();
      
              /*ServletOutputStream outputStream = response.getOutputStream();
              outputStream.print(mapper.writeValueAsString(res));
              outputStream.flush();
              outputStream.close();*/
      
              PrintWriter writer = response.getWriter();
              /*通过映射器对象将响应封装对象映射为JSON*/
              writer.print(mapper.writeValueAsString(res));
              writer.flush();
              writer.close();
          }
      }
      
3.2.3 缓存从数据库中查询的数据保到redis中。

3.2.3.1 业务需求:

  • 由于每次http请求校验token时,都要调用userDetailsService.loadUserByUsername(username)方法查询数据获取用户信息。因此为提高程序运行效率,减小数据库访问压力,可以将数据库的查询结果保存到redis中。

3.2.3.2实现方法:

  • 通过redis提供的客户端jedis或SpringBoot提供RedisTemplate将连接redis实现数据的保存。
  • Jedis与RedisTemplate的区别:
    • Jedis是由redis厂商提供的Java连接redis的工具。而RedisTemplate是Spring框架对Jedis的封装。因此在效率上Jedis明显高于RedisTemplate,而在易用性则RedisTemplate优于Jedis。

3.2.3.3实现细节:

  • /** 
     * redis中保存的数据的数据类型在5.0时只有String,List,Hash,Set,zSet五种类型的数据。
     * 当保存到redis中的数据为java对象时,则需要将对象转换成以上类型的一种。常用的是String类型。
     * 而Redis的String类型:是二进制安全的字符串。也就是说,Redis中即可以保存普通的String对象,也可以是字节数组。因此redis的string可以包含任何数据。
     *      比如jpg图片或者序列化的对象。
     * 将java对象序列化成字符串,有两种方法:
     * 1.通过Serialize方式:
     *      该方式要求需要待序列化的Java对象的类型必须实现Serialize接口。
     *      并且需要自定义序列化与反序列化方法。
     * 2.通过JSON方式:
     *      该方式要求当实现反序列化时要求必须是实体类的对象,并且该对象必须有无参构造函数。
     *      如果使用接口或抽像类的引用对象去接收反序列化时将会出错。
     * */
    

3.2.3.4业务实现

  • userDetailsService.loadUserByUsername(username)返回的UserDetails接口的某一实现类的对象。因此要实现缓存,则需要通过Serialize方式实现序列化。

  • 自定义序列化与反序列化工具类。具体实现如下:

    package org.wjk.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    @Slf4j
    public class SerializeUtils
    {
        private static final Logger LOGGER = LoggerFactory.getLogger(SerializeUtils.class);
    
        public static byte[] serialize(Object resource)
        {
            /**
             * ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。
             * 可以使用 ObjectInputStream 读取(重构)对象。通过在流中使用文件可以实现对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或				  另一个进程中重构对象。
             *
             * 只能将支持 java.io.Serializable 接口的对象写入流中。每个 serializable 对象的类都被编码,编码内容包括类名和类签名、对象的字段值和				 数组值,以及从初始对象中引用的其他所有对象的闭包。
             *
             * writeObject 方法用于将对象写入流中。所有对象(包括 String 和数组)都可以通过 writeObject 写入。可将多个对象或基元写入流中。
             * 必须使用与写入对象时相同的类型和顺序从相应 ObjectInputstream 中读回对象。
             *
             * 还可以使用 DataOutput 中的适当方法将基本数据类型写入流中。还可以使用 writeUTF 方法写入字符串。
             *
             * 对象的默认序列化机制写入的内容是:
             *      对象的类,类签名,以及非瞬态和非静态字段的值。其
             *      他对象的引用(瞬态和静态字段除外)也会导致写入那些对象。
             *      可使用引用共享机制对单个对象的多个引用进行编码,这样即可将对象的图形恢复为最初写入它们时的形状。
             * */
            ObjectOutputStream oos = null;
            /**
             * 此类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray() 和 toString() 获				取数据。 
             *
             * 关闭 ByteArrayOutputStream 无效。此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException。
             * */
            ByteArrayOutputStream baos = null;
            try
            {
                baos = new ByteArrayOutputStream();
                // 创建写入指定 OutputStream 的 ObjectOutputStream。此构造方法将序列化流部分写入底层流;调用者可以通过立即刷新流,确保在读取头					部时,用于接收 ObjectInputStreams 构造方法不会阻塞。
                oos = new ObjectOutputStream(baos);
                // 将指定的对象写入 ObjectOutputStream。对象的类、类的签名,以及类及其所有超类型的非瞬态和非静态字段的值都将被写入。可以使用 					   writeObject 和 readObject 方法重写类的默认序列化。
                // 由此对象引用的对象是以可变迁的方式写入的,这样,可以通过ObjectInputStream 重新构造这些对象的完全等价的图形。 
                oos.writeObject(resource);
                return baos.toByteArray();
            }
            catch (Exception e)
            {
                e.printStackTrace();
                LOGGER.error("序列化失败,原因是:"+ e.getMessage());
            }
            return null;
        }
        public static Object unserialize(byte [] resource)
        {
            // ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化
            ObjectInputStream ois = null;
            ByteArrayInputStream bais = null;
            try
            {
                bais = new ByteArrayInputStream(resource);
                /*创建从指定 InputStream 读取的 ObjectInputStream。从流读取序列化头部并予以验证。在对应的 ObjectOutputStream 写入并刷新头部				  之前,此构造方法将阻塞。*/
                ois = new ObjectInputStream(bais);
                /*readObject 方法负责使用通过对应的 writeObject 方法写入流的数据,为特定类读取和恢复对象的状态。该方法本身的状态,不管是属于其超类					还是属于其子类,都没有关系。恢复状态的方法是,从个别字段的 ObjectInputStream 读取数据并将其分配给对象的适当字段。DataInput 				  支持读取基本数据类型。 */
                return ois.readObject();
            }catch (Exception e)
            {
                e.printStackTrace();
                LOGGER.error("反序列化失败,原因是:" + e.getMessage());
            }
            return null;
        }
    }
    
  • 通过AOP实现数据缓存到redis

    • 添加AOP依赖及Jedis 连接依赖

      <dependency>
          <groupId>org.springframework.bootgroupId>
          <artifactId>spring-boot-starter-aopartifactId>
      dependency>
      <dependency>
           <groupId>redis.clientsgroupId>
           <artifactId>jedisartifactId>
                  
      dependency>
      
    • 完成Jedis配置文件:新建redis.properties配置文件

      #redis所在主机配置
      redis.host=192.168.75.55
      redis.port=6379
      # redis连接池配置
      redis.pool.maxTotal=32
      redis.pool.maxIdle=32
      redis.pool.minIdle=16
      redis.pool.maxWait=5
      redis.pool.testOnBorrow=false
      
    • 完成Jedis连接池创建:新建RedisConfig配置类

      package org.wjk.config.redis;
      
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.context.annotation.PropertySource;
      import redis.clients.jedis.JedisPool;
      import redis.clients.jedis.JedisPoolConfig;
      
      import java.time.Duration;
      
      @PropertySource("classpath:/redis.properties")
      @Configuration
      public class RedisConfig
      {
          private final Logger logger = LoggerFactory.getLogger(RedisConfig.class);
          @Value("${redis.host}")
          private String jedisHost;
          @Value("${redis.port}")
          private Integer jedisPort;
          @Value("${redis.pool.maxTotal}")
          private Integer maxTotal;
          @Value("${redis.pool.maxIdle}")
          private Integer maxIdle;
          @Value("${redis.pool.minIdle}")
          private Integer minIdle;
          @Value("${redis.pool.maxWait}")
          private Integer maxWait;
          @Value("${redis.pool.testOnBorrow}")
          private Boolean testOnBorrow;
          @Bean
          public JedisPool jedisPool()
          {
              logger.debug("Redis's host is {} and port is {}", jedisHost, jedisPort);
              JedisPoolConfig poolConfig = new JedisPoolConfig();
              /*设置redis连接池中最大连接数*/
              poolConfig.setMaxTotal(maxTotal);
              poolConfig.setMaxIdle(maxIdle);
              poolConfig.setMinIdle(minIdle);
              poolConfig.setMaxWait(Duration.ofSeconds(maxWait));
              poolConfig.setTestOnBorrow(testOnBorrow);
              return new JedisPool(poolConfig, jedisHost, jedisPort);
          }
      }
      
    • 创建用于AOP缓存读取与刷新的自定义注解

      • 缓存读取注解

        package org.wjk.annotation;
        
        import java.lang.annotation.ElementType;
        import java.lang.annotation.Retention;
        import java.lang.annotation.RetentionPolicy;
        import java.lang.annotation.Target;
        
        @Target(ElementType.METHOD)
        @Retention(RetentionPolicy.RUNTIME)
        public @interface CacheReader
        {
            String value() default "";
        }
        
      • 缓存刷新注解

        package org.wjk.annotation;
        
        import java.lang.annotation.ElementType;
        import java.lang.annotation.Retention;
        import java.lang.annotation.RetentionPolicy;
        import java.lang.annotation.Target;
        
        @Target(ElementType.METHOD)
        @Retention(RetentionPolicy.RUNTIME)
        public @interface CacheWriter
        {
            String value() default "";
        }
        
    • 实现缓存

      package org.wjk.aspect;
      
      import lombok.extern.slf4j.Slf4j;
      import org.aspectj.lang.ProceedingJoinPoint;
      import org.aspectj.lang.annotation.Around;
      import org.aspectj.lang.annotation.Aspect;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.scheduling.annotation.Async;
      import org.springframework.stereotype.Component;
      import org.wjk.annotation.CacheReader;
      
      import org.wjk.exception.ExcInfo;
      import org.wjk.exception.ServiceException;
      import org.wjk.utils.SerializeUtils;
      import redis.clients.jedis.Jedis;
      import redis.clients.jedis.JedisPool;
      
      
      import java.util.Arrays;
      
      @Aspect
      @Component
      @Slf4j
      public class CacheOperation
      {
          @Autowired
          private JedisPool jedisPool;
          private final Logger logger = LoggerFactory.getLogger(CacheOperation.class);
          
          @Around("@annotation(anCacheReader)")
          public Object getDataFromCache(ProceedingJoinPoint joinPoint,CacheReader anCacheReader) throws Throwable
          {
              /*构建Redis中的key*/
              String jdsKey = anCacheReader.value();
              jdsKey = jdsKey + Arrays.toString(joinPoint.getArgs());
              Jedis jedis = jedisPool.getResource();
              Object result = null;
              /*如果Redis中存在jdsKey,则获取该Key对应的数据。*/
              if(jedis.exists(jdsKey))
              {
                  try
                  {
                      logger.info("从redis中获取数据");
                      result = SerializeUtils.unserialize(jedis.get(jdsKey.getBytes()));
                  }
                  catch(Exception e)
                  {
                      logger.error("从redis中获取数据失败,原因是:" + e.getMessage());
                      throw new ServiceException(ExcInfo.UNKNOW_EXCEPTION);
                  }
                  finally
                  {
                      jedis.close();
                  }
      
              }
              /*如果Redis中不存在jdsKey,则从数据库中获取数据,并将数据以jdsKey为Key保存至Redis中*/
              else
              {
                  logger.info("从mysql中获取数据");
                  result = joinPoint.proceed();
                  storeToRedis(jedis, jdsKey, result);
              }
      
              return result;
          }
          /*异步实现*/
          @Async
          protected void storeToRedis(Jedis jedis, String key, Object object)
          {
              if(jedis == null)
              {
                  jedis = jedisPool.getResource();
              }
              try
              {
                  logger.info("以{}为key保存数据", key);
                  jedis.set(key.getBytes(), SerializeUtils.serialize(object));
              }
              catch (Exception e)
              {
                  logger.error("保存数据到redis失败,原因是:" + e.getMessage());
              }
              finally
              {
                  jedis.close();
              }
          }
      }
      
    • 关于SpringBoot项目的多线程

      • SpringBoot项目默认是支持多线程的。

      • 多线程的实现

        • 核心配置文件中,完成线程池的配置

          spring:
            #关闭SpringBoot图标
            main:
              banner-mode: off
            #配置数据源信息
            datasource:
              driver-class-name: org.mariadb.jdbc.Driver
              url: jdbc:mysql://192.168.75.55:3306/xfsy?severTimezone=GMT%2B8&characterEncoding=utf8
              username: root
              password: root
            #配置线程池信息
            task:
              execution:
                pool:
                	#核心线程数,当池中线程数没达到core-size的值时,每接收一个新的任务都会创建一个新线程,然后存储到池。假如池中线程数已经达到					core-size设置的值,再接收新的任务时,要检测是否有空闲的核心线程,假如有,则使用空闲的核心线程执行新的任务。
                  core-size: 16
                  #最大线程数,当任务队列已满,核心线程也都在忙,再来新的任务则会创建新的线程,但所有线程数不能超过max-size设置的值,否则可能会					出现异常(拒绝执行)
                  max-size: 16
                  #队列容量,假如核心线程数已达到core-size设置的值,并且所有的核心线程都在忙,再来新的任务,会将任务存储到任务队列。
                  queue-capacity: 32
                  #线程空闲时间,假如池中的线程数多余core-size设置的值,此时又没有新的任务,则一旦空闲线程空闲时间超过keep-alive设置的时间					值,则会被释放。
                  keep-alive: 60s
                thread-name-prefix: cims-task-
          
        • 开启多线程,在启动类上使用@EnableAsync注解

          @SpringBootApplication
          @EnableAsync
          public class BsServerApplication
          {
              public static void main(String[] args)
              {
                  SpringApplication.run(BsServerApplication.class, args);
              }
          }
          
        • 使用@Async注解描述要异步执行的方法。如写入缓存的方法

          /*异步实现*/
              @Async
              protected void storeToRedis(Jedis jedis, String key, Object object)
              {
                  if(jedis == null)
                  {
                      jedis = jedisPool.getResource();
                  }
                  try
                  {
                      logger.info("以{}为key保存数据", key);
                      jedis.set(key.getBytes(), SerializeUtils.serialize(object));
                  }
                  catch (Exception e)
                  {
                      logger.error("保存数据到redis失败,原因是:" + e.getMessage());
                  }
                  finally
                  {
                      jedis.close();
                  }
              }
          
3.2.4 获取当前用户拥有权限的菜单

3.2.4.1 定义封装数据库菜单表记录的实体类

package org.wjk.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Data
@Accessors(chain = true)
@TableName("sys_menu")
public class SysMenuPojo extends BasePojo
{
    private static final long serialVersionUID = -514867408544331729L;
    @TableId(type = IdType.AUTO)
    @NotNull(groups = Update.class)
    private Integer id;
    @NotNull
    private Integer type;
    private String path;
    private String component;
    @NotEmpty
    private String name;
    @NotEmpty
    private String iconCls;
    private String permission;
    private String permissionNote;
    private Integer parentId=0;
    private Integer enabled = 1;
    @TableField(exist = false)
    private List<SysMenuPojo> children;

    public interface Insert {}
    public interface Update {}

    public static List<SysMenuPojo> listConvertToNode(List<SysMenuPojo> source)
    {
        List<SysMenuPojo> target = new ArrayList<>();
        Map<Integer, SysMenuPojo> map = new HashMap<>();
        for(SysMenuPojo item : source)
            map.put(item.id, item);
        for(SysMenuPojo it : source)
        {
            if(it.id == 0 || null == map.get(it.parentId))
            {
                target.add(it);
            }
            else
            {
                SysMenuPojo parent = map.get(it.parentId);
                if (null == parent.children)
                    parent.children = new ArrayList<>();
                parent.children.add(it);
            }
        }
        return target;
    }
}

3.2.4.2 定义接收的Controller类SysMenuCtrller

package org.wjk.controller;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wjk.service.SysMenuSvc;
import org.wjk.vo.ResPaging;

import javax.validation.constraints.NotNull;

@RestController
@RequestMapping("/menu")
public class SysMenuCtrller
{
    private SysMenuSvc sysMenuSvc;
	// 使用构造方法方式完成依赖注入。当交由Spring管理的类中,只有一个构造方法时,该构造方法默认是使用@Autowired注解描述,即使不显示使用该注解。
    public SysMenuCtrller(SysMenuSvc sysMenuSvc)
    {
        this.sysMenuSvc = sysMenuSvc;
    }

   /*处理获取当前用户授权访问的菜单数据的请求。*/
    @GetMapping("/usermenu/{username}")
    public ResPaging getCurrentUserMenus(@PathVariable @Validated @NotEmpty String username)
    {
        return ResPaging.success("OK", sysMenuSvc.getCurrentUserMenus(username));
    }
}

3.2.4.3 定义Service与Serivce实现类:SysMenuSvcSysMenuSvcImpl

package org.wjk.service;
import org.wjk.pojo.SysMenuPojo;
import java.util.List;

public interface SysMenuSvc
{
    List<SysMenuPojo> getCurrentUserMenus(String username);
}
package org.wjk.service.impl;

import org.springframework.stereotype.Service;
import org.wjk.annotation.CacheReader;
import org.wjk.dao.SysMenuDao;
import org.wjk.exception.ExcInfo;
import org.wjk.exception.ServiceException;
import org.wjk.pojo.SysMenuPojo;
import org.wjk.service.SysMenuSvc;

import java.util.List;

@Service
public class SysMenuSvcImpl implements SysMenuSvc
{
    private SysMenuDao sysMenuDao;

    public SysMenuSvcImpl(SysMenuDao sysMenuDao)
    {
        this.sysMenuDao = sysMenuDao;
    }

   /*获取当前用户授权的菜单数据*/
    @Override
    @CacheReader("MENU_USER::")
    public List<SysMenuPojo> getCurrentUserMenus(String username)
    {
        List<SysMenuPojo> source = sysMenuDao.getCurrentUserMenus(username);
        if(source.isEmpty())
            throw new ServiceException(ExcInfo.ILLEGAL_DB_CONNECT);
        List<SysMenuPojo> target = SysMenuPojo.listConvertToNode(source);
        if(target.isEmpty())
            throw new ServiceException(ExcInfo.DATA_CAST_ERROR);
        return target;
    }
}

3.2.4.4 定义Dao层:SysMenuDao

package org.wjk.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.wjk.pojo.SysMenuPojo;
import java.util.List;

@Mapper
public interface SysMenuDao extends BaseMapper<SysMenuPojo>
{
    List<SysMenuPojo> getCurrentUserMenus(String username);
}

3.2.4.5 定义映射文件: SysMenu


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.wjk.dao.SysMenuDao">
    
    <select id="getCurrentUserMenus" resultType="org.wjk.pojo.SysMenuPojo">
        select mn.id id, mn.type type, mn.path path, mn.component component, mn.name name, mn.icon_cls iconCls, mn.permission permission,
                mn.permission_note permissionNote, mn.parent_id parentId, mn.enabled enabled
            from sys_menu mn
            left join sys_pstn_prmsn spm on mn.id=spm.menu_id
            left join sys_user su on spm.pstn_id=su.pstn_id
            where su.username=#{username} and mn.type=0 and mn.enabled=1
    select>

mapper>

3.2.4.6前端动态菜单构建

      <el-menu class="el-menu-vertical-demo" background-color="#060037" text-color="#ffffff" :unique-opened="true" :router="true" :collapse="isCollapse">
        <el-menu-item>
          <img src="../assets/logo.png" width="24" height="24"/>
          <span style="font-family: 楷体; font-size: 18px; color: #E3E63C; ">旭锋信息管理系统span>
        el-menu-item>
        <template v-for="(item, index) in menuData">
          <el-menu-item v-if="!item.children" :index="item.path">
            <i :class="item.iconCls">i>
            <span slot="title" class="myAwe">{{item.name}}span>
          el-menu-item>
          <el-submenu v-else :index="'second'+index">
            <template slot="title">
              <i :class="item.iconCls">i>
              <span slot="title" class="myAwe">{{item.name}}span>
            template>
            <template v-for="(subitem, subIndex) in item.children">
              <el-menu-item v-if="!subitem.children" :index="subitem.path">
                <i :class="subitem.iconCls">i>
                <span slot="title" class="myAwe">{{subitem.name}}span>
              el-menu-item>
              <el-submenu v-if="subitem.children" :index="'third'+subIndex">
                <template slot="title">
                  <i :class="subitem.iconCls">i>
                  <span slot="title" class="myAwe">{{subitem.name}}span>
                template>
                <template v-for="(thirdItem) in subitem.children">
                  <el-menu-item :index="thirdItem.path">
                    <i :class="thirdItem.iconCls">i>
                    <span slot="title" class="myAwe">{{thirdItem.name}}span>
                  el-menu-item>
                template>
              el-submenu>
            template>
          el-submenu>
        template>
      
3.2.5 获取当前用户信息,并保存到sessionStorage

3.2.5.1 定义获取当前登陆用户的函数:在Home组件的methods属性中,获取成功后的响应后,将当前用户信息保存到sessionStorage中。

 /*重新获取当前用户信息包含用户拥有的授权标识*/
async reloadCurrentUser()
 {
  let username;
  if(sessionStorage.getItem("username"))
   {
	   username = sessionStorage.getItem("username")
   }
  const { data: res } = await this.$axios.get(`/relogin/${username}`);
  if(2000 === res.status && res.data)
  {
    sessionStorage.setItem("currentUser", JSON.stringify(res.data));
  }
  else
  {
    this.$message.error(res.message);
  }
},

3.2.5.2 在生命周期钩子created()中发送该请求。

created(){
  this.username = sessionStorage.getItem("username");
  /*获取当前登陆用户信息*/
  this.reloadCurrentUser();
  /*获取构建当前用户左侧菜单栏的数据*/
  this.getCurrentUserMenus();
  /*当页面重新构建时即设置当前激活菜单项及获取面包屑相关数据*/
  this.setDefaultsAliveMenuAndBreadData();
},

3.2.5.3 在LoginCtrller中定义响应方法

@GetMapping("/relogin/{username}")
    public ResPaging reloadCurrentUser(@PathVariable @Validated @NotEmpty String username)
    {
        SysUserPojo userDetails = loginService.reloadCurrentUser(username);
        /*将当前用户信息也返回给前端*/
        userDetails.setPassword(null);
        return ResPaging.success("登陆成功!", userDetails);
    }

3.2.5.4 在LoginSvsImpl中定义处理逻辑

@Override
    public SysUserPojo reloadCurrentUser(String username)
    {
        return (SysUserPojo) userDetailService.loadUserByUsername(username);
    }

(四) 构建动态面包屑

4.1业务流程
  • 方法:通过监控路由组件变化获取路由组件及父路由组件的元信息,构建面包屑。
4.2 业务实现
4.2.1 设置保存变量和处理方法
  • 定义保存默认激活菜单项和面包屑变量

    data() {
        return {
          isCollapse: false,
          username: "",
          /*保存菜单数据*/
          menuData: [],
          /*保存默认选中菜单的index值*/
          defaultMenu: "",
          breadCrumbData: [],
        };
    
  • 定义设置默认激活菜单项和面包屑所需数据: homeVue的method属性中

    /*设置默认激活菜单和面包屑数据*/
        setDefaultsAliveMenuAndBreadData()
        {
          if(this.$route.meta.length >= 0)
          {
            this.$route.meta.forEach(item => {
              if(item.type==0)
              {
                this.defaultMenu = item.path;
              }
              this.breadCrumbData.push(item.title);
            })
          }
        }
    
4.2.2设置监听器。
  • 监听路由组件变化,创建组件变化时设置面包屑及默认激活菜单数据。

    /*Vue的监听器,当监听对象发生变化时,则自动调用回调函数*/
      watch:{
        /*监听路由组件变化*/
        '$route'(to, from){
         this.breadCrumbData = [];
         this.setDefaultsAliveMenuAndBreadData();
        }
      }
    
4.2.3 初始化默认激活菜单项和面包屑数据。
created(){
    let user = JSON.parse(sessionStorage.getItem("currentUser"));
    this.username = user.username;
    this.getCurrentUserMenus();
    /*当页面重新构建时即设置当前激活菜单项及获取面包屑相关数据*/
    this.setDefaultsAliveMenuAndBreadData();

  },
4.2.4构建动态面包屑

        <div style="height: 25px; background-color: #7dbcea; box-shadow: 0 4px 15px 5px #7dbcea;">
          <el-row type="flex" justify="end">
            <el-breadcrumb separator-class="el-icon-arrow-right" style="margin-top:6px; margin-right: 20px">
              <el-breadcrumb-item v-for="(item, index) in breadCrumbData">{{item}}el-breadcrumb-item>
            el-breadcrumb>
          el-row>
        div>

(五)用户退出业务

5.1 业务流程

用户点击安全退出下拉菜单后

  • 客户端向服务器发出退出请求。
  • 服务器接收退出请求,SpringSecurity调用退出处理器,进行退出处理。
  • 当退出清理完成后,SpringSecurity将调用退出成功处理器,响应客户端。
5.2 业务实现
5.2.1 下列菜单添加点击事件处理函数。
<el-dropdown trigger="click" @command="handleCommand" size="small">
5.2.2 在Home的Vue对象的method属性中定义该处理函数。
/*当前用户下拉菜单选中处理方法*/
    handleCommand(command)
    {
      //this.$message('click on item ' + command);
      switch(command)
      {
        case "logout":
          this.currentUserLogout();
          break;
        case "psnCenter":
          break;
      }
    },
5.2.3 在method属性中定义currentUserLogout()
  • 业务流程

    • 向服务器发出退出请求
    • 当服务器响应退出成功时
      • 清理当前用户产生的所有的sessionStorage
      • 显示退出成功信息给用户。
      • 页面跳转到登录页面。
    • 当服务器响应失败时
      • 显示失败信息给用户。
  • 具体实现

    /*自定义退出方法*/
        async currentUserLogout()
        {
          const {data : res} = await this.$axios.get("/logout");
          if(2000 == res.status)
          {
            sessionStorage.clear();
            this.$message.success(res.message);
            await this.$router.push("/login");
          }
          else
          {
            this.$message.error(res.message);
          }
        }
    
5.2.4 服务器接收退出并处理请求

5.2.4.1 定义自定义退出逻辑处理器

  • 该处理器需要实现SpringSecurity提供的LogoutHandler接口

    • 需要重写LogoutHandler接口的public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)方法。
      • 该方法主要任务是清理SpringSecurity安全上下文。
  • 具体实现

    package org.wjk.config.security;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.authentication.logout.LogoutHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    /*用户退出的业务处理方法*/
    @Component
    public class MyLogoutHandler implements LogoutHandler
    {
        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
        {
            SecurityContextHolder.clearContext();
        }
    }
    

5.2.4.2 定义自定义退出成功处理器

  • 该处理器需要实现SpringSecurity提供的LogoutSuccessHandler接口

    • 需要重写该接口的public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException方法。
      • 该方法主要响应前端退出结果。
  • 具体实现

    package org.wjk.config.security;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    import org.springframework.stereotype.Component;
    import org.wjk.utils.JsonResUtils;
    import org.wjk.vo.ResPaging;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    @Component
    public class MyLogoutSuccessHandler implements LogoutSuccessHandler
    {
    
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
        {
            JsonResUtils.response(request, response, ResPaging.success("退出成功!"));
        }
    }
    

5.2.4.3 配置SpringSecurity接收退出请求并设置自定义退出处理和退出成功处理供SpringSecurity退出调用。

  • 配置SpringSecurity在自定义MySecurityConfig配置类

    • 通过protected void configure(HttpSecurity http) throws Exception方法
  • 具体实现

    @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http.authorizeRequests()
                    /*配置所有请求都需要登录认证*/
                    .anyRequest().authenticated()
                    .and()
                    /*配置自定义退出逻辑*/
                    .logout().permitAll()
                    .addLogoutHandler(logoutHandler)
                    .logoutSuccessHandler(logoutSuccessHandler)
                    .and()
                    /*关闭跨站请求伪造*/
                    .csrf().disable()
                    /*配置session管理器*/
                    .sessionManagement()
                    /*由于使用jwt令牌来保存用户登录信息,因此不需要使用创建session来保存用户登录信息*/
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    /*将token校验过滤器添加在认证过滤器前*/
                    .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
            ;
        }
    

(六)权限控制

6.1 业务流程
  • 客户端在页面跳转前,校验用户是否有跳转页面的访问权限。

    • 如果有,则页面跳转。

    • 如果没有,则页面不跳转。

  • 当客户端请求服务器资源时,校验登录用户是否有访问该资源的权限。

    • 如果有,则响应客户端请求资源。
    • 如果没有,则响应客户端无权请求该资源。
6.2 业务实现
6.2.1 前端业务实现

6.2.1.1 在路由管理器组件中,为需要权限的路由组件配置meta属性,属性中包含所需的权限标识。

  • 具体实现如下:

    const routes = [
      {
        path: '/',
        redirect: "/login",
      },
      {
        path: "/login",
        name: "Login",
        component: Login
      },
      {
        path: "/home",
        name: "Home",
        component: Home,
        children: [
          {
            path: "/menu",
            component: _ => import("../views/menu/menumngr"),
            meta: [{title: "系统设置"},{title: "菜单管理", permission: "sys:menu:mngr" /*配置权限标识*/, type: 0, path: "/menu"}]
          }
        ]
      },
      {
        path: '/about',
        name: 'About',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
      }
    ]
    

6.2.1.2 在前置路由导航守卫中校验权限

  • 具体实现如下:

    router.beforeEach((to, from, next) => {
      let token = sessionStorage.getItem("token");
      let permissions;
      if(JSON.parse(sessionStorage.getItem("currentUser")))
      {
        permissions = JSON.parse(sessionStorage.getItem("currentUser")).permissions;
      }
      let toPermission;
      if(Object.keys(to.meta).length !== 0)
      {
        toPermission = to.meta[to.meta.length - 1].permission;
      }
    
      if("/login" === to.path || (token && token.trim().length > 0))
      {
        if(to.path !== "/login" && permissions && toPermission && !permissions.includes(toPermission))
        {
          Message.error("您无访问该资源的权限,请联系管理员!");
          next(from.path);
        }
        next();
      }
      else
        next("/login");
    })
    
  • 解决vue-router连接两次跳转到相同路径时,报错的问题。

    • 当vue-router连接两次跳转到相同路径时,将报错如下错误

      Error: Redirected when going from "/home" to "/menu" via a navigation guard.
      Error: Navigation cancelled from "/home" to "/menu" with a new navigation.
      
    • 解决方式,在路由管理器组件的index.js文件中,添加如下代码:

      Vue.use(VueRouter)
      //解决编程式路由往同一地址跳转时会报错的情况
      const originalPush = VueRouter.prototype.push;
      const originalReplace = VueRouter.prototype.replace;
      //push
      VueRouter.prototype.push = function push (location, onResolve, onReject) {
        // if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
        return originalPush.call(this, location).catch(err => {return;})
      }
      // replace
      VueRouter.prototype.replace = function replace (location, onResolve, onReject) {
        // if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)
        return originalReplace.call(this, location).catch(err => {return;})
      }
      
6.2.2 后端业务实现

6.2.2.1 开启SpringSecurity权限

  • 使用@EnableWebSecurity@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)描述自定义SpringSecurity配置类。

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class MySecurityConfig extends WebSecurityConfigurerAdapter
    {
        ......
    }
    

6.2.2.2 标识方法所需权限

  • 使用@PreAuthorize("hasAuthority('sys:menu:mngr')")注解描述需要权限访问的controller方法。如:

    	@GetMapping("/usermenu/{userId}")
        @PreAuthorize("hasAuthority('sys:menu:mngr')")// 该注解说明,要访问此方法的用户必须有sys:menu:mngr标识的权限
        public ResPaging getCurrentUserMenus(@PathVariable @Validated @NotNull Integer userId)
        {
            return ResPaging.success("获取成功", sysMenuSvc.getCurrentUserMenus(userId));
        }
    

6.2.2.3 处理权限不足

  • 当用户无权访问,SpringSecurity将抛出AccessDeniedException异常。

    • 使用SpringSecurity完成登录操作时,需要自定义权限不足处理逻辑处理该异常。
    • 使用SpringMVC完成登录操作时,则该异常需要在全局异常处理类中处理。
  • 全局异常处理该异常。

        @ExceptionHandler(RuntimeException.class)
        public ResPaging MyRuntimeException(RuntimeException exception)
        {
            exception.printStackTrace();
            if(exception instanceof UsernameNotFoundException)
                return ResPaging.failture(ExcInfo.ILLEGAL_PASSWORD);
            else if(exception instanceof LockedException)
                return ResPaging.failture(ExcInfo.ACCOUNT_LOCKED);
            else if(exception instanceof DisabledException)
                return ResPaging.failture(ExcInfo.ACCOUNT_NOT_ENABLED);
            /*处理权限不足异常*/
            else if(exception instanceof AccessDeniedException)
                return ResPaging.failture(ExcInfo.PERMISSION_DENIED);
            else
                return ResPaging.failture(ExcInfo.UNKNOW_EXCEPTION);
        }
    

6.2.2.4 原始 定义权限不足时的处理逻辑即配置

  • 定义时机:在使用SpringSecurity完成登录时,而非自定义登录逻辑时,需要自定义权限不足时的处理逻辑

  • 当用户没有权限访问资源时,SpringSecurity则会回调该类的方法。

  • 自定义权限不足的处理逻辑必须实现AccessDeniedHandler,并重写public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException方法。

  • 主要作用:响应前端用户权限不足。

  • 权限不足的处理逻辑

    package org.wjk.config.security;
    
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    import org.wjk.exception.ExcInfo;
    import org.wjk.utils.JsonResUtils;
    import org.wjk.vo.ResPaging;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    @Component
    public class MyAccessDeniedHandler implements AccessDeniedHandler
    {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException
        {
            JsonResUtils.response(request, response, ResPaging.failture(ExcInfo.PERMISSION_DENIED));
        }
    }
    
  • 配置权限不足的处理逻辑

     /*对http请求认证授权配置*/
        @Override
        protected void configure(HttpSecurity http) throws Exception
        {
            http.authorizeRequests()
                    /*配置所有请求都需要登录认证*/
                    .anyRequest().authenticated()
                    .and()
                    /*配置自定义退出逻辑*/
                    .logout().permitAll()
                    .addLogoutHandler(logoutHandler)
                    .logoutSuccessHandler(logoutSuccessHandler)
                    .and()
                    /*关闭跨站请求伪造*/
                    .csrf().disable()
                    /*配置session管理器*/
                    .sessionManagement()
                    /*由于使用jwt令牌来保存用户登录信息,因此不需要使用创建session来保存用户登录信息*/
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    /*处理各类SpringSecurity抛出的异常*/
                    .exceptionHandling()
                    /*处理权限不足的异常*/
                    .accessDeniedHandler(accessDeniedHandler)
                    .and()
                    /*将token校验过滤器添加在认证过滤器前*/
                    .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
            ;
        }
    
6.2.3 前端接收权限不足响应时处理。
  • 处理方法,通过axios响应拦截器处理。

  • 具体实现

    axios.interceptors.response.use(res => {
      /*处理token过期的处理逻辑*/
      if(5005 === res.data.status || 5006 === res.data.status)
      {
        Modal.error({
          title: "旭峰科技信息管理系统",
          content: res.data.message,
          onOk(){
            sessionStorage.clear();
            router.push("/login").then(r => res);
          }
        })
      }
      /*拦截权限不足处理逻辑*/
      if(5014 === res.data.status)
      {
        Message.error(res.data.message);
      }
      return res;
    })
    

你可能感兴趣的:(单体架构的旭锋项目,vue.js,前端,javascript,java,spring)