springboot+springsecurity+jwt+elementui圖書管理系統

2023-05-28 12:00:12

​圖書管理系統​

一、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

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;
 
 /**
  * <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選擇器