• springboot+springsecurity+jwt+elementui图书管理系统


    ​图书管理系统​

    一、springboot后台

    1、mybatis-plus整合

    1.1添加pom.xml

    <!--mp逆向工程 -->
             <dependency>
                 <groupId>org.projectlombok</groupId>
                 <artifactId>lombok</artifactId>
             </dependency>
             <dependency>
                 <groupId>com.baomidou</groupId>
                 <artifactId>mybatis-plus-boot-starter</artifactId>
                 <version>3.4.3.1</version>
             </dependency>
             <dependency>
                 <groupId>com.baomidou</groupId>
                 <artifactId>mybatis-plus-generator</artifactId>
                 <version>3.1.0</version>
             </dependency>
             <dependency>
                 <groupId>org.freemarker</groupId>
                 <artifactId>freemarker</artifactId>
                 <version>2.3.31</version>
             </dependency>
             <dependency>
                 <groupId>mysql</groupId>
                 <artifactId>mysql-connector-java</artifactId>
                 <version>8.0.28</version>
             </dependency>
             <dependency>
                 <groupId>org.apache.commons</groupId>
                 <artifactId>commons-lang3</artifactId>
                 <version>3.7</version>
             </dependency>

    1.2创建CodeGenerator代码生成类

    package com.ds.book.mp;
     
     import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
     import com.baomidou.mybatisplus.core.toolkit.StringPool;
     import com.baomidou.mybatisplus.generator.AutoGenerator;
     import com.baomidou.mybatisplus.generator.InjectionConfig;
     import com.baomidou.mybatisplus.generator.config.*;
     import com.baomidou.mybatisplus.generator.config.po.TableInfo;
     import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
     import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
     import org.apache.commons.lang3.StringUtils;
     
     import java.util.ArrayList;
     import java.util.List;
     import java.util.Scanner;
     
     public class CodeGenerator {
     
         /**
          * 

    * 读取控制台内容 *

    */
    public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("java大师"); gc.setOpen(false); // gc.setSwagger2(true); 实体属性 Swagger2 注解 mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://175.24.198.63:3306/book?useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8"); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("root@1234!@#"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); // pc.setModuleName(scanner("模块名")); pc.setParent("com.ds.book"); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); /* cfg.setFileCreate(new IFileCreate() { @Override public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { // 判断自定义文件夹是否需要创建 checkDir("调用默认方法创建的目录,自定义目录用"); if (fileType == FileType.MAPPER) { // 已经生成 mapper 文件判断存在,不想重新生成返回 false return !new File(filePath).exists(); } // 允许生成模板文件 return true; } }); */ cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); // 配置自定义输出模板 //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 // templateConfig.setEntity("templates/entity2.java"); // templateConfig.setService(); // templateConfig.setController(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setTablePrefix("t_"); // strategy.setInclude("t_user"); // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!"); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 公共父类 // strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!"); // 写于父类中的公共字段 strategy.setSuperEntityColumns("id"); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); // strategy.setTablePrefix(pc.getModuleName() + "_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }

    1.3生成crontroller、service、mapper、entity等业务实体类

    运行CodeGenerator,生成业务实体类

    请输入表名,多个英文逗号分割: t_user,t_menu,t_role,t_user_role,t_role_menu

    2、springsecurity-jwt整合

    2.1整合springsecurity

    1)

    <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-starter-security</artifactId>
     </dependency>

    2.2认证授权流程

    img

    认证管理

    流程图解读:

    1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

    2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。

    3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。

    4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

    授权管理

    访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。

    2.3编写自己的UserDetails和UserDetailService

    2.3.1UserDetails
    package com.ds.book.entity;
     
     import com.baomidou.mybatisplus.annotation.TableName;
     import java.io.Serializable;
     import java.util.Collection;
     
     import lombok.Data;
     import lombok.EqualsAndHashCode;
     import lombok.experimental.Accessors;
     import org.springframework.security.core.GrantedAuthority;
     import org.springframework.security.core.userdetails.UserDetails;
     
     /**
      * 

    * *

    * * @author java大师 * @since 2023-03-17 */
    @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("t_user") public class User implements Serializable, UserDetails { private static final long serialVersionUID = 1L; private Integer id; /** * 登录名 */ private String name; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 是否有效:1-有效;0-无效 */ private String status; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles .stream() .map(role -> new SimpleGrantedAuthority(role.getRoleCode())) .collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
    2.3.2userDetailService

    登录成功后,将UserDetails的roles设置到用户中

    package com.ds.book.service.impl;
     
     import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
     import com.ds.book.entity.User;
     import com.ds.book.mapper.UserMapper;
     import com.ds.book.service.IUserService;
     import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
     import org.apache.commons.lang3.StringUtils;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.security.core.userdetails.UserDetails;
     import org.springframework.security.core.userdetails.UserDetailsService;
     import org.springframework.security.core.userdetails.UsernameNotFoundException;
     import org.springframework.stereotype.Service;
     
     /**
      * 

    * 服务实现类 *

    * * @author java大师 * @since 2023-03-17 */
    @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User loginUser = userMapper.selectOne(new QueryWrapper<User>().eq("username", username)); if (loginUser == null){ throw new UsernameNotFoundException("用户名或密码错误"); } loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId())); return loginUser; } }
    2.3.2加载userDetailService

    将我们自己的UserDetailService注入springsecurity

    package com.ds.book.config;
     
     import com.ds.book.filter.JwtTokenFilter;
     import com.ds.book.service.impl.UserServiceImpl;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
     import org.springframework.security.config.annotation.ObjectPostProcessor;
     import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
     import org.springframework.security.config.annotation.web.builders.HttpSecurity;
     import org.springframework.security.config.annotation.web.builders.WebSecurity;
     import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
     import org.springframework.security.config.http.SessionCreationPolicy;
     import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
     import org.springframework.security.crypto.password.PasswordEncoder;
     import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
     import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
     
     @Configuration
     public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
     
         @Autowired
         private UserServiceImpl userService;
     
         @Bean
         public PasswordEncoder passwordEncoder(){
             return new BCryptPasswordEncoder();
         }
     
         //注入我们自己的UserDetailService
         @Override
         protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
         }
     }

    问题:前后端分离项目,通常不会使用springsecurity自带的登录界面,登录界面由前端完成,后台只需要提供响应的服务即可,且目前主流不会采用session去存取用户,后端会返回响应的token,前端访问的时候,会在headers里面带入token.

    2.4JwtToken

    2.4.1 JWT描述

    Jwt token由Header、Payload、Signature三部分组成,这三部分之间以小数点”.”连接,JWT token长这样:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU

    token解析后长这样: header部分,有令牌的类型(JWT)和签名算法名称(HS256): { "alg": "HS256", "typ": "JWT" } Payload部分,有效负载,这部分可以放任何你想放的数据:

    { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

    Signature签名部分,由于这部分是使用header和payload部分计算的,所以还可以以此来验证payload部分有没有被篡改:

    HMACSHA256(

    base64UrlEncode(header) + "." +

    base64UrlEncode(payload),

    123456 //这里是密钥,只要够复杂,一般不会被破解

    )

    2.4.2 pom.xml
    <dependency>
         <groupId>io.jsonwebtokengroupId>
         <artifactId>jjwtartifactId>
         <version>0.9.0version>
     dependency>
    2.4.3 JwtToken工具类
    package com.ds.book.tool;
     
     
     import io.jsonwebtoken.Claims;
     import io.jsonwebtoken.JwtBuilder;
     import io.jsonwebtoken.Jwts;
     import io.jsonwebtoken.SignatureAlgorithm;
     
     import javax.crypto.SecretKey;
     import javax.crypto.spec.SecretKeySpec;
     import java.util.Base64;
     import java.util.Date;
     import java.util.UUID;
     
     /**
      * JWT工具类
      */
     public class JwtUtil {
     
         //有效期为
         public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
         //设置秘钥明文
         public static final String JWT_KEY = "dashii";
     
         public static String getUUID(){
             String token = UUID.randomUUID().toString().replaceAll("-", "");
             return token;
         }
     
         /**
          * 生成jtw
          * @param subject token中要存放的数据(json格式)
          * @return
          */
         public static String createJWT(String subject) {
             JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
             return builder.compact();
         }
     
         /**
          * 生成jtw
          * @param subject token中要存放的数据(json格式)
          * @param ttlMillis token超时时间
          * @return
          */
         public static String createJWT(String subject, Long ttlMillis) {
             JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
             return builder.compact();
         }
     
         private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
             SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
             SecretKey secretKey = generalKey();
             long nowMillis = System.currentTimeMillis();
             Date now = new Date(nowMillis);
             if(ttlMillis==null){
                 ttlMillis= JwtUtil.JWT_TTL;
             }
             long expMillis = nowMillis + ttlMillis;
             Date expDate = new Date(expMillis);
             return Jwts.builder()
                     .setId(uuid)              //唯一的ID
                     .setSubject(subject)   // 主题  可以是JSON数据
                     .setIssuer("dashi")     // 签发者
                     .setIssuedAt(now)      // 签发时间
                     .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                     .setExpiration(expDate);
         }
     
         /**
          * 创建token
          * @param id
          * @param subject
          * @param ttlMillis
          * @return
          */
         public static String createJWT(String id, String subject, Long ttlMillis) {
             JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
             return builder.compact();
         }
     
         public static void main(String[] args) throws Exception {
             String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
             Claims claims = parseJWT(token);
             System.out.println(claims);
         }
     
         /**
          * 生成加密后的秘钥 secretKey
          * @return
          */
         public static SecretKey generalKey() {
             byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
             SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
             return key;
         }
     
         /**
          * 解析
          *
          * @param jwt
          * @return
          * @throws Exception
          */
         public static Claims parseJWT(String jwt) throws Exception {
             SecretKey secretKey = generalKey();
             return Jwts.parser()
                     .setSigningKey(secretKey)
                     .parseClaimsJws(jwt)
                     .getBody();
         }
     }
    2.4.4 JwtTokenFilter
    package com.ds.book.filter;
    
    import com.ds.book.entity.User;
    import com.ds.book.mapper.UserMapper;
    import com.ds.book.service.IMenuService;
    import com.ds.book.service.IUserService;
    import com.ds.book.tool.JwtUtil;
    import io.jsonwebtoken.Claims;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    public class JwtTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        private IUserService userService;
        @Autowired
        private UserMapper userMapper;
    
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            //1、获取token
            String token = httpServletRequest.getHeader("token");
            if (StringUtils.isEmpty(token)){
                filterChain.doFilter(httpServletRequest,httpServletResponse);
                return;
            }
            String userId;
            try {
                Claims claims = JwtUtil.parseJWT(token);
                userId = claims.getSubject();
            } catch (Exception exception) {
                exception.printStackTrace();
                throw new RuntimeException("token非法");
            }
            User user = userService.getUserById(Integer.parseInt(userId));
            user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId)));
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            filterChain.doFilter(httpServletRequest,httpServletResponse);
        }
    }

    在springsecurity中,第一个经过的过滤器是UsernamePasswordAuthenticationFilter,所以前后端分离的项目,我们自己定义的过滤器要放在这个过滤器前面,具体配置如下

    @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/login").permitAll()
                    .anyRequest().authenticated();
            http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
            http.cors();
        }
    2.4.5授权
    2.4.5.1 开启preAuthorize进行收取(Controller路径匹配)

    1)主启动类上添加EnableGlobalMethodSecurity注解

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @SpringBootApplication
    @MapperScan("com.ds.book.mapper")
    public class BookSysApplication {
        public static void main(String[] args) {
            SpringApplication.run(BookSysApplication.class,args);
        }
    }

    2)Controller方法上添加@PreAuthorize注解

    @RestController
    public class HelloController {
    
        @GetMapping("/hello")
        @PreAuthorize("hasRole('ROLE_ADMIN')")
        public String hello(){
            return "hello";
        }
    }
    2.4.5.2 增强方式授权(数据库表配置)

    1)创建我们自己的FilterInvocationSecurityMetadataSource,实现getAttributes方法,获取请求url所需要的角色

    @Component
    public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource {
    
        @Autowired
        private IMenuService menuService;
        AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        //获取访问url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,访问sys/user时获取到必须要有ROLE_ADMIN角色。返回		Collection
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
            //获取所有的菜单及角色
            List<Menu> menus = menuService.getMenus();
            for (Menu menu : menus) {
                if (antPathMatcher.match(menu.getUrl(),requestURI)){
                    String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new);
                    return SecurityConfig.createList(roles);
                }
            }
            return null;
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return false;
        }
    }

    2)创建我们自己的决策管理器AccessDecisionManager,实现decide方法,判断步骤1)中获取到的角色和我们目前登录的角色是否相同,相同则允许访问,不相同则不允许访问,

    @Component
    public class MyAccessDecisionManager implements AccessDecisionManager {
        
        //1、认证通过后,会往authentication中填充用户信息
        //2、拿authentication中的权限与上一步获取到的角色信息进行比对,比对成功后,允许访问
        @Override
        public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (ConfigAttribute configAttribute : configAttributes) {
                for (GrantedAuthority authority : authorities) {
                    if (authority.getAuthority().equals(configAttribute.getAttribute())){
                        return;
                    }
                }
            }
            throw new AccessDeniedException("权限不足,请联系管理员");
        }
    
        @Override
        public boolean supports(ConfigAttribute attribute) {
            return false;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return false;
        }
    }

    3)在SecurityConfig中,添加后置处理器(增强器),让springsecurity使用我们自己的datametasource和decisionMananger

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private MySecurtiMetaDataSource mySecurtiMetaDataSource;
        @Autowired
        private MyAccessDecisionManager myAccessDecisionManager;
        @Autowired
        private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
        @Autowired
        private MyAccessDeniedHandler myAccessDeniedHandler;
    
        @Autowired
        private UserServiceImpl userService;
    
        @Autowired
        private JwtTokenFilter jwtTokenFilter;
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    .antMatchers("/login").permitAll()
                    .anyRequest().authenticated()
                	//后置处理器,使用我们自己的FilterSecurityInterceptor拦截器配置
                    .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor> () {
                        @Override
                        public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                            o.setSecurityMetadataSource(mySecurtiMetaDataSource);
                            o.setAccessDecisionManager(myAccessDecisionManager);
                            return o;
                        }
                    })
                    .and()
                    .headers().cacheControl();
            http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
            http.cors();
        }
    }
    2.4.6异常处理

    1)前端渲染工具类

    public class WebUtils
    {
        /**
         * 将字符串渲染到客户端
         *
         * @param response 渲染对象
         * @param string 待渲染的字符串
         * @return null
         */
        public static String renderString(HttpServletResponse response, String string) {
            try
            {
                response.setStatus(200);
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.getWriter().print(string);
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            return null;
        }
    }

    2)未登录异常处理,实现commence方法

    @Component
    public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            Result result = new Result(401,"未登录,请先登录",null);
            String json = JSON.toJSONString(result);
            WebUtils.renderString(httpServletResponse,json);
    
        }
    }

    3)授权失败异常处理,实现Handle方法

    @Component
    public class MyAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
            Result result = new Result(403,"权限不足请联系管理员",null);
            String s = JSON.toJSONString(result);
            WebUtils.renderString(httpServletResponse,s);
        }
    }

    3、整合swagger2

    1)添加pom.xml依赖

    <dependency>
        <groupId>io.springfoxgroupId>
        <artifactId>springfox-swagger2artifactId>
        <version>2.7.0version>
    dependency>
    <dependency>
        <groupId>io.springfoxgroupId>
        <artifactId>springfox-swagger-uiartifactId>
        <version>2.7.0version>
    dependency>
    <dependency>
        <groupId>com.github.xiaoymingroupId>
        <artifactId>knife4j-spring-boot-starterartifactId>
        <version>2.0.7version>
    dependency>

    2)创建swagger配置文件

    package com.ds.book.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.*;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spi.service.contexts.SecurityContext;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Configuration
    @EnableSwagger2
    public class Swagger2Config {
        @Bean
        public Docket createRestApi() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .pathMapping("/")
                    .apiInfo(apiInfo())
                    .select()
                    //swagger要扫描的包路径
                    .apis(RequestHandlerSelectors.basePackage("com.ds.book.controller"))
                    .paths(PathSelectors.any())
                    .build()
                    .securityContexts(securityContexts())
                    .securitySchemes(securitySchemes());
        }
    
        private ApiInfo apiInfo() {
            return new ApiInfoBuilder().title("图书管理系统接口文档")
                	//作者、路径和邮箱
                    .contact(new Contact("java大师","http://localhost:8080/doc.html","fry000@qq.com"))
                    .version("1.0").description("图书管理接口文档").build();
        }
    
        private List<SecurityContext> securityContexts() {
            //设置需要登录认证的路径
            List<SecurityContext> result = new ArrayList<>();
            result.add(getContextByPath("/.*"));
            return result;
        }
    
        //通过pathRegex获取SecurityContext对象
        private SecurityContext getContextByPath(String pathRegex) {
            return SecurityContext.builder()
                    .securityReferences(defaultAuth())
                    .forPaths(PathSelectors.regex(pathRegex))
                    .build();
        }
    
        //默认为全局的SecurityReference对象
        private List<SecurityReference> defaultAuth() {
            List<SecurityReference> result = new ArrayList<>();
            AuthorizationScope authorizationScope = new AuthorizationScope("global",
                    "accessEverything");
            AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
            authorizationScopes[0] = authorizationScope;
            result.add(new SecurityReference("Authorization", authorizationScopes));
            return result;
        }
    
        private List<ApiKey> securitySchemes() {
            //设置请求头信息
            List<ApiKey> result = new ArrayList<>();
            //设置header中的token
            ApiKey apiKey = new ApiKey("token", "token", "header");
            result.add(apiKey);
            return result;
        }
    }

    3)修改SecurityConfig配置类,允许访问swagger的地址

    //主要的配置文件,antMatchers匹配的路径,全部忽略,不进行JwtToken的认证
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/login",
                "/logout",
                "/css/**",
                "/js/**",
                "/index.html",
                "favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**"
        );
    }

    4)编写LoginController接口,通过@Api和@ApiOperation注解使用swagger

    package com.ds.book.controller;
    
    import com.ds.book.entity.Result;
    import com.ds.book.entity.User;
    import com.ds.book.service.IUserService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.SecurityConfig;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @Api(tags = "登录")
    public class LoginController {
    
        @Autowired
        private IUserService userService;
    
        @ApiOperation("登录")
        @PostMapping("/login")
        public Result login(@RequestBody User user){
            return userService.login(user);
        }
    }

    5)输入地址 http://localhost:8080/doc.html,进入swagger

    6)点击登录进入登录接口,点击调试,发送

    测试成功!

    4、业务接口

    4.1 登录接口

    注意:前后端分离项目,退出的时候,由前端清除浏览器请求header中的token和sessionStorage或者LocalStorage,后端只要返回一个退出成功的消息。

    package com.ds.book.controller;
    
    import com.ds.book.entity.Result;
    import com.ds.book.entity.User;
    import com.ds.book.service.IUserService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.SecurityConfig;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.security.Principal;
    
    @RestController
    @Api(tags = "登录")
    public class LoginController {
    
        @Autowired
        private IUserService userService;
        @Autowired
        private UserDetailsService userDetailsService;
    
        @ApiOperation("登录")
        @PostMapping("/login")
        public Result login(@RequestBody User user){
            return userService.login(user);
        }
    
        @ApiOperation("退出")
        @PostMapping("/logout")
        public Result logout(){
            return Result.success("退出成功");
        }
    
        @ApiOperation("获取当前登录用户信息")
        @GetMapping("/user/info")
        public User user(Principal principal){
            if (principal == null){
                return null;
            }
            String username = principal.getName();
            User user = (User)userDetailsService.loadUserByUsername(username);
            user.setPassword(null);
            return user;
        }
    
    }

    4.2菜单接口

    package com.ds.book.controller;
    
    
    import com.ds.book.entity.Menu;
    import com.ds.book.entity.Result;
    import com.ds.book.service.IMenuService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import io.swagger.models.auth.In;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    /**
     * 

    * 前端控制器 *

    * * @author java大师 * @since 2023-03-09 */
    @RestController @Api(tags = "菜单管理") public class MenuController { @Autowired private IMenuService menuService; @GetMapping("/menus") @ApiOperation("获取菜单树") public Result getMenus(){ List<Menu> allMenus = menuService.getMenuTree(); return Result.success("查询成功",allMenus); } @PostMapping("/menu/add") @ApiOperation("添加菜单") public Result addMenu(@RequestBody Menu menu){ return menuService.addMenu(menu); } @PostMapping("/menu/update") @ApiOperation("修改菜单") public Result updateMenu(@RequestBody Menu menu){ return menuService.updateMenu(menu); } @PostMapping("/menu/delete/{id}") @ApiOperation("删除菜单") public Result deleteMenu(@PathVariable Integer id){ return menuService.deleteMenu(id); } }

    4.3用户接口

    package com.ds.book.controller;
    
    
    import com.ds.book.entity.Result;
    import com.ds.book.entity.User;
    import com.ds.book.service.IUserService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import io.swagger.models.auth.In;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.web.bind.annotation.*;
    
    import javax.jws.soap.SOAPBinding;
    import java.util.List;
    
    /**
     * 

    * 前端控制器 *

    * * @author java大师 * @since 2023-03-09 */
    @RestController @Api(tags = "用户管理") public class UserController { @Autowired private IUserService userService; @Autowired private PasswordEncoder passwordEncoder; @GetMapping("/users") @ApiOperation("查询用户列表") public Result getUsers(){ List<User> list = userService.getUsers(); if (list != null){ return Result.success("查询成功",list); } return Result.error("查询失败"); } @PostMapping("/user/add") @ApiOperation("添加用户") public Result addUser(@RequestBody User user){ user.setPassword(passwordEncoder.encode("123456")); return userService.addUser(user); } @PostMapping("/user/update") @ApiOperation("修改用户") public Result updateUser(@RequestBody User user){ return userService.updateUser(user); } @PostMapping("/user/chooseRole/{userId}/{roleId}") @ApiOperation("选择角色") public Result chooseRole(@PathVariable Integer userId,@PathVariable Integer roleId){ return userService.chooseRole(userId,roleId); } @PostMapping("/user/delete/{id}") @ApiOperation("删除用户") public Result deleteUser(@PathVariable Integer id){ return userService.deleteUser(id); } }

    4.4角色接口

    package com.ds.book.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.ds.book.entity.Menu;
    import com.ds.book.entity.Result;
    import com.ds.book.entity.Role;
    import com.ds.book.entity.RoleMenu;
    import com.ds.book.mapper.RoleMapper;
    import com.ds.book.mapper.RoleMenuMapper;
    import com.ds.book.service.IRoleService;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 

    * 服务实现类 *

    * * @author java大师 * @since 2023-03-09 */
    @Service public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IRoleService { @Autowired private RoleMapper roleMapper; @Autowired private RoleMenuMapper roleMenuMapper; private List<Menu> buildMenuTree(List<Menu> menus, Integer parentId) { List<Menu> treeMenus = new ArrayList<>(); for (Menu menu : menus) { if (parentId==0 ? menu.getParentId()==0 : parentId.equals(menu.getParentId())) { List<Menu> children = buildMenuTree(menus, menu.getId()); if (!children.isEmpty()) { menu.setChildren(children); } treeMenus.add(menu); } } return treeMenus; } @Override public List getRoles() { List<Role> roles = roleMapper.getRoles(); for (Role role : roles) { role.setMenus(buildMenuTree(role.getMenus(),0)); } return roles; } @Override public Result chooseMenus(Integer roleId, Integer[] menuIds) { try { roleMenuMapper.delete(new QueryWrapper<RoleMenu>().eq("role_id",roleId)); for (Integer menuId : menuIds) { RoleMenu roleMenu = new RoleMenu(); roleMenu.setRoleId(roleId); roleMenu.setMenuId(menuId); roleMenuMapper.insert(roleMenu); } return Result.success("添加成功"); } catch (Exception exception) { return Result.error("添加失败"); } } }

    二、springboot前端

    1、vue-cli创建vue-book前端项目

    vue create vue-book

    选择Vue2,运行完毕,出现以下画面

    执行绿色的命令,出现下列界面代表脚手架创建项目成功

    2、整合elementui

    //命令行安装
    npm i element-ui -S
    
    //main.js使用element-ui
    import Vue from 'vue';
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    import App from './App.vue';
    
    Vue.use(ElementUI);
    
    new Vue({
      el: '#app',
      render: h => h(App)
    });

    3、安装vue-router

    2.1安装依赖

    npm install vue-router@3

    2.2创建路由文件

    import Vue from 'vue'
    import VueRouter from "vue-router";
    
    Vue.use(VueRouter)
    
    //配置localhost:8080/跳转为登录页
    const routes =[
        {
            path:'/',
            name:'Login',
            component:() => import('@/pages/Login.vue')
        }
    ]
    
    export default new VueRouter({
        routes
    })

    4、整合json-server

    4.1安装json-server

    npm install -g json-server

    4.2创建mock文件夹,新建db.json

    {
      "posts": [
        {
          "id": 1,
          "title": "json-server",
          "author": "typicode"
        }
      ],
      "users": [
        {
          "id": 1,
          "username": "admin",
          "password": "123"
        }
      ],
      "login":
      {
        "code": 200,
        "message":"返回成功",
        "data": {
          "id": "1237361915165020161",
          "username": "admin",
          "phone": "111111111111",
          "nickName": "javads",
          "realName": "javads",
          "sex": 1,
          "deptId": "1237322421447561216",
          "deptName": "测试部门",
          "status": 1,
          "email": "xxxx@qq.com",
          "token":"ASDSADASDSW121DDSA",
          "menus": [
            {
              "id": "1236916745927790564",
              "title": "系统管理",
              "icon": "el-icon-star-off",
              "path": "/sys",
              "name": "Sys",
              "children": [
                {
                  "id": "1236916745927790578",
                  "title": "角色管理",
                  "icon": "el-icon-s-promotion",
                  "path": "/sys/roles",
                  "name": "Roles",
                  "children": []
                },
                {
                  "id": "1236916745927790560",
                  "title": "菜单管理",
                  "icon": "el-icon-s-tools",
                  "path": "/sys/menus",
                  "name": "Menus",
                  "children": []
                },
                {
                  "id": "1236916745927790575",
                  "title": "用户管理",
                  "icon": "el-icon-s-custom",
                  "path": "/sys/users",
                  "name": "User",
                  "children": []
                }
              ],
              "spread": true,
              "checked": false
            },
            {
              "id": "1236916745927790569",
              "title": "账号管理",
              "icon": "el-icon-s-data",
              "path": "/account",
              "name": "Account",
              "children": []
            }
          ],
          "permissions": [
            "sys:log:delete",
            "sys:user:add",
            "sys:role:update",
            "sys:dept:list"
          ]
        }
      },
      "comments": [
        {
          "id": 1,
          "body": "some comment",
          "postId": 1
        }
      ],
      "profile": {
        "name": "typicode"
      }
    }

    4.3修改vue.config.js,json-server的默认端口为3000,将代理服务器的的端口改成3000

    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
      transpileDependencies: true,
      lintOnSave:false,
      devServer:{
        proxy:{
          '/api':{
            target:'http://localhost:3000',
            pathRewrite:{'^/api':''},
            ws:true, //不写为true,websocket
            changeOrigin:true //不写为true
          }
        }
      }
    })

    4.4修改package.json,在scripts添加以下代码

    "mock": "json-server src/mock/db.json --port 3000 --middlewares src/mock/middlewares.js"

    4.5 运行json-server,出现以下界面代表运行成功

    json-server.cmd --watch db.jso

    5、整合axios

    5.1配置axios请求拦截器,新建utils文件夹,新建api.js,输入以下内容

    import router from '../router'
    import axios from 'axios'
    import {Message} from 'element-ui'
    import {Loading} from 'element-ui'
    
    
    axios.defaults.baseURL = '/api'
    
    //添加遮罩层代码
    let loading;
    let loadingNum = 0;
    
    //弹出遮罩层
    function showLoading(){
        if (loadingNum ===0){
            loading = Loading.service({
                lock:true,
                text:'加载中,请稍后...',
                background:'rgba(255,255,255,0.5)'
            })
        }
        loadingNum++;
    }
    
    //关闭遮罩层
    function hiddenLoading(){
        loadingNum--;
        if (loadingNum <=0){
            loading.close();
        }
    
    }
    
    /**
     * 添加响应拦截器,在浏览器每次发请求之前,token放入http消息头当中
     */
    axios.interceptors.request.use(config =>{
        showLoading();
        if(window.sessionStorage.getItem('token')){
            config.headers.Authorization =window.sessionStorage.getItem('token')
        }
        console.log(config)
        return config
    },error => {
        console.log(error)
    })
    
    /**
     * 添加响应拦截器
     */
    axios.interceptors.response.use(success => {
        hiddenLoading();
        if (success.status && success.status == 200){
            if (success.data.code == 500 || success.data.code == 401 || success.data.code == 403) {
                Message.error({
                    offset:200,
                    message:success.data.message
                })
                router.replace("/")
            }
            if (success.data.message){
                Message.success({
                    offset:200,
                    message:success.data.message
                })
            }
        }
        return success.data
    },error => {
        hiddenLoading();
        if (error.response.code == 504 || error.response.code == 404) {
            Message.error({
                message: '服务器跑路了'
            });
        } else if (error.response.status == 403) {
            Message.error({
                message: '权限不足,请联系管理员'
            });
        } else if (error.response.code == 401) {
            Message.error({
                message: '尚未登录,请先登录'
            })
            router.replace('/');
        } else {
            if (error.response.data.message) {
                Message.error({
                    message: error.response.data.message
                });
            } else {
                Message.error({
                    message: '未知错误'
                });
            }
        }
        return;
    })
    
    export default axios

    5.2创建请求接口,新建http.js

    import axios from './api'
    
    export const login = (param) =>{
        return axios.get(`/posts`, {param})
    }
    
    
    export const getUser = () =>{
        return axios.get(`/users`, {})
    }

    6、业务功能

    6.1登录界面

    <template>
      <div class="login-container">
        <el-form ref="form" :model="form" label-width="100px" class="login-form">
          <h1 style="margin-bottom: 20px;text-align: center">欢迎登录h1>
          <el-form-item label="用户名">
            <el-input v-model="form.username">el-input>
          el-form-item>
          <el-form-item label="密码">
            <el-input type="password" v-model="form.password">el-input>
          el-form-item>
          <el-form-item>
            <el-button type="primary" @click="onSubmit">登录el-button>
            <el-button>取消el-button>
          el-form-item>
        el-form>
      div>
    template>
    <script>
    import {initRoutes} from "@/utils/routesUtil";
    import {login,getUser} from "@/utils/http";
    export default {
      name:'Login',
      data() {
        return {
          form: {
            username: '',
            password: '',
          }
        }
      },
      methods: {
        onSubmit() {
          login(this.form).then(res=>{
            if(res){
              //浏览器中存储token,以后每次调用后端接口,浏览器都会带入这个token
              window.sessionStorage.setItem("token",res.data.token)
              //初始化路由数据
              let myRoutes = initRoutes(res.data.menus)
              //将路由进行替换并添加到router中
              this.$router.options.routes = [myRoutes]
              this.$router.addRoute(myRoutes)
              this.$router.replace("/home")
            }else{
              return false
            }
          })
        },
      }
    }
    script>
    <style scoped>
    
    .login-form {
      border: 1px #DCDFE6 solid;
      border-radius: 4px;
      padding: 40px;
      margin: 110px 400px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
      width: 400px;
    }
    
    .login-container {
      /*background: url(../assets/image/login2.jpg) no-repeat;*/
      height: 100%;
      width: 100%;
      overflow: hidden;
      background-size: cover;
    }
    style>

    6.2处理后台请求返回工具类

    export const initTmpRoutes = (menus) => {
        let tmpRoutes = []
        menus.forEach(menu => {
            let {id,title,icon,path,name,children} = menu
            if(children instanceof Array){
                children = initTmpRoutes(children)
            }
            let tmpRoute = {
                path:path,
                meta:{icon:icon,title:title},
                name:name,
                children:children,
                component:children.length?{render(c){return c('router-view')}}:()=>import(`@/pages${path}/${name}.vue`)
            }
            console.log('tmpRoute',tmpRoute.path)
            tmpRoutes.push(tmpRoute)
        })
        return tmpRoutes
    }
    
    export const initRoutes = (menus)=>{
        const homeRoute = {
            path:'/home',
            name:'Home',
            meta:{title:'首页',icon: 'el-icon-star-off'},
            component:() => import('@/pages/Home.vue'),
        }
        homeRoute.children = initTmpRoutes(menus);
        console.log('homeRoute',homeRoute)
        return homeRoute;
    }

    6.3首页、导航页和主页

    home.vue

    <template>
      <div class="box">
        <el-container style="height: 100%;" direction="vertial">
          <el-aside width="200px">
            <Nav/>
          el-aside>
          <el-container>
            <el-header class="homeHeader">
              <el-dropdown class="userInfo" @command="handlecommand">
           <span class="el-dropdown-link">
           span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="userInfo">个人中心el-dropdown-item>
                  <el-dropdown-item command="setting">设置el-dropdown-item>
                  <el-dropdown-item command="logout">退出el-dropdown-item>
                el-dropdown-menu>
              el-dropdown>
            el-header>
            <el-main>
              <Main/>
            el-main>
            <el-footer>底部el-footer>
          el-container>
        el-container>
      div>
    template>
    
    <script>
    import Nav from "@/components/Nav";
    import Main from "@/components/Main";
    import RecursiveMenu from "@/components/RecursiveMenu";
    export default{
      data(){
        return {
          user:JSON.parse(window.sessionStorage.getItem('user'))
        }
      },
      components:{
        Nav,
        RecursiveMenu,
        Main
      },
      methods:{
        handlecommand(command){
          if(command=='logout'){
            this.$confirm('确定退出?', '提示', {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              type: 'warning'
            }).then(()=>{
              logout();
              window.sessionStorage.removeItem('user');
              window.sessionStorage.removeItem('token');
              this.$store.commit('initRoutes',[]);
              this.$router.replace('/');
            }).catch(()=>{
    
            })
          }
        }
      },
    }
    
    script>
    
    <style>
    #app,
    html,
    body,
    .box,
    .el-container{
      padding: 0px;
      margin: 0px;
      height: 100%;
    }
    .el-header,
    .el-footer {
      background-color: #B3C0D1;
      color: #333;
      text-align: right;
      line-height: 60px;
    }
    
    .el-aside {
      background-color: #545C64;
      color: #333;
      text-align: center;
      line-height: 300px;
    }
    
    .el-main {
      background-color: #E9EEF3;
      color: #333;
      display: flex;
      flex-direction: column;
    }
    
    body>.el-container {
      margin-bottom: 40px;
    }
    
    .homeHeader .userInfo{
      cursor: pointer;
    }
    
    .el-dropdown-link img{
      width: 36px;
      height: 36px;
      border-radius: 18px;
    }
    style>

    Nav.vue

    <template>
      <el-menu router>
        <template v-for="item in routes">
          <el-submenu v-if="item.children.length" :index="item.path">
            <template slot="title">{{ item.meta.title }}template>
            <recursive-menu :menu="item.children">recursive-menu>
          el-submenu>
          <el-menu-item v-else :index="item.path">{{ item.meta.title }}el-menu-item>
        template>
      el-menu>
    template>
    
    <script>
    import RecursiveMenu from "@/components/RecursiveMenu";
    export default {
      name: 'Nav',
      components:{
        RecursiveMenu
      },
      computed:{
        routes(){
          console.log('Nav routes:',this.$router.options.routes.length)
          // return this.$router.options.routes[1].children;
          return this.$router.options.routes;
        }
      }
    }
    script>

    RecursiveMenu.vue

    <template>
      <div>
        <el-menu router>
          <template v-for="item in menu">
            <el-submenu v-if="item.children.length" :index="item.path">
              <template slot="title">{{ item.meta.title }}template>
              <recursive-menu :menu="item.children">recursive-menu>
            el-submenu>
            <el-menu-item v-else :index="item.path">{{ item.meta.title }}el-menu-item>
          template>
        el-menu>
      div>
    
    template>
    
    <script>
    export default {
      name: 'RecursiveMenu',
      props: {
        menu: {
          type: Array,
          required: true
        },
      },
      components: {
        RecursiveMenu: () => import('./RecursiveMenu.vue')
      }
    }
    script>

    可以看到左边的菜单和路由已经展示在浏览器中

    注意:这里有一个坑,页面刷新以后,路由中的数据就会丢失,系统菜单会不显示

    原因:页面刷新后,页面会重新实例化路由数据,因为是动态路由,所以页面刷新后会将router置为router/index.js配置的原始路由数据,所以匹配路由地址的时候会报错。

    解决方法

    思路:因为目前login接口返回的时候,直接将菜单数据传回前端,所以我们需要将菜单缓存起来,因为每次页面刷新vuex数据都会重置,所以不适合存储在vuex中,可以将菜单数据存储在sessionStorage中,页面刷新在实例化vue的created生命周期函数之前初始化路由即可

    步骤

    1)安装vuex

    npm install vuex@3

    2)修改登录页Login.vue

    <template>
      <div class="login-container">
        <el-form ref="form" :model="form" label-width="100px" class="login-form">
          <h1 style="margin-bottom: 20px;text-align: center">欢迎登录h1>
          <el-form-item label="用户名">
            <el-input v-model="form.username">el-input>
          el-form-item>
          <el-form-item label="密码">
            <el-input type="password" v-model="form.password">el-input>
          el-form-item>
          <el-form-item>
            <el-button type="primary" @click="onSubmit">登录el-button>
            <el-button>取消el-button>
          el-form-item>
        el-form>
      div>
    template>
    <script>
    import {initRoutes} from "@/utils/routesUtil";
    import {login,getUser} from "@/utils/http";
    export default {
      name:'Login',
      data() {
        return {
          form: {
            username: '',
            password: '',
          }
        }
      },
      methods: {
        onSubmit() {
          login(this.form).then(res=>{
            if(res){
    		  //将token和menus保存在vuex中
              this.$store.dispatch("UPDATETOKEN",res.data.token);
              this.$store.dispatch("UPDATEUSERDATA",res.data.menus)
              //登录的时候,初始化菜单放在vuex中,不在登录页进行处理
              this.$store.commit('INITROUTES',res.data.menus)
              // 以下代码为注释
              // let myRoutes = initRoutes(res.data.menus)
              // this.$router.options.routes = [myRoutes]
              // this.$router.addRoute(myRoutes)
              this.$router.replace("/home")
            }else{
              return false
            }
          })
        },
      }
    }
    script>
    <style scoped>
    
    .login-form {
      border: 1px #DCDFE6 solid;
      border-radius: 4px;
      padding: 40px;
      margin: 110px 400px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
      width: 400px;
    }
    
    .login-container {
      /*background: url(../assets/image/login2.jpg) no-repeat;*/
      height: 100%;
      width: 100%;
      overflow: hidden;
      background-size: cover;
    }
    style>

    3)创建store文件夹,创建index.js

    import Vuex from 'vuex'
    import Vue from "vue";
    import {initRoutes} from "@/utils/routesUtil";
    import Router from "@/router";
    Vue.use(Vuex)
    
    const state = {
        token:window.sessionStorage.getItem('token')||'',
        userData:window.sessionStorage.getItem('userData')||{},
        routes:{}
    }
    const mutations = {
        SETTOKEN(state,token){
            window.sessionStorage.setItem('token',token)
            state.token = token
        },
        SETUSERDATA(state,userData){
            window.sessionStorage.setItem('userData',JSON.stringify(userData))
            state.userData = userData
        },
        INITROUTES(state,menus){
            let myRoutes = initRoutes(menus)
            Router.options.routes = [myRoutes]
            Router.addRoute(myRoutes);
            state.routes = myRoutes
        }
    }
    
    const actions = {
        UPDATETOKEN(context,value){
            context.commit('SETTOKEN',value)
        },
        UPDATEUSERDATA(context,value){
            context.commit('SETUSERDATA',value)
        }
    }
    
    
    const getters = {
        userinfo(state){
            return state.userData
        },
        menus(state){
            return state.userData.menus
        },
        routes(state){
            return state.routes.filter(item => {
                return item.name==='Home'
            })[0].children
        }
    }
    
    export default new Vuex.Store({
        state,
        mutations,
        actions,
        getters
    })

    4)main.js修改

    import Vue from 'vue'
    import App from './App.vue'
    import ElementUI from 'element-ui'
    import router from './router'
    import 'element-ui/lib/theme-chalk/index.css'
    import store from "@/store"
    
    Vue.config.productionTip = false
    Vue.use(ElementUI)
    
    //生成路由,由于没有获取菜单接口,所以直接从sessionStorage中直接去userData数据,进行路由的初始化
    const init = async ()=>{
      if (sessionStorage.getItem('token')){
        if (store.state.routes){
          await store.commit('INITROUTES',JSON.parse(sessionStorage.getItem('userData')))
        }
      }
    }
    
    //此处await不可缺少,需要等待路由数据先生成,才能进行vue实例的创建,否则会报错
    async function call(){
      await init();
      new Vue({
        render: h => h(App),
        router,
        store
      }).$mount('#app')
    }
    call()

    5)如果未登录,则跳转到login页处理,main.js添加如下内容

    //路由导航守卫,每次路由地址改变前出发
    router.beforeEach((to,from,next)=>{
      if (sessionStorage.getItem('token')) {
        next();
      } else {
        //如果是登录页面路径,就直接next()
        if (to.path === '/login') {
          next();
        } else {
          if(to.path === '/home'){
            next();
          }
          next('/login');
        }
      }
    })

    安装e-icon-picker选择器

  • 相关阅读:
    https的加密原理
    保姆级阿里云ESC服务器安装nodejs和服务器node服务管理工具PM2安装使用
    python gui(六)全局设定
    嚼一嚼Halcon中的3D手眼标定
    Kernel Memory 入门系列:快速开始
    云原生k8s的金箍棒
    log日志异常堆栈打印的正确姿势
    MySQL-JDBC
    LayaBox---TypeScript---三斜线指令
    Qt地铁智慧换乘系统浅学(四 )实现添加线路,添加站点,添加边 并且存储到本地txt文件
  • 原文地址:https://www.cnblogs.com/dalaba/p/17437953.html