2

springboot+springsecurity+jwt+elementui图书管理系统 - Java大师-

 11 months ago
source link: https://www.cnblogs.com/dalaba/p/17437953.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

​图书管理系统​

一、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 {
 
     /**
      * <p>
      * 读取控制台内容
      * </p>
      */
     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

<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;
 
 /**
  * <p>
  * 
  * </p>
  *
  * @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;
 
 /**
  * <p>
  *  服务实现类
  * </p>
  *
  * @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.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.0</version>
 </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<ConfigAttribute>
    @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.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.7.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.7.0</version>
</dependency>
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.7</version>
</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","[email protected]"))
                .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;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @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;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @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": "[email protected]",
      "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选择器


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK