0%

菜单权限和按钮权限的实现,以及JWT的使用

一. 概述

  1. 一个系统中用户登陆之后看到的菜单,必须是当前登录人拥有权限的菜单才能展示,没有权限的菜单直接不显示;
  2. 使用无状态token方案,登录只存储了loginInfo信息,没有登录人相关的权限(菜单、按钮权限);
  3. 难道我们在登录的时候需要将登录人相关的权限信息也一并存储到redis中码?
    答:如果人数多,并发量大,redis就不是一个好的方案:redis是一个内存数据库,内存有局限,数据量越大,内存占用率高,影响读取性能。

二. 无状态的token方案

  1. 后端验证登录信息成功之后,会生成一个随机串作为token将用户信息保存在redis,并将token令牌传回给浏览器;

  2. 后续浏览器只需要将token携带到服务器,服务器就可以根据浏览器的token令牌获取redis的信息

    1. 如果获取不到信息,说明token令牌无效
    1. 获取到信息,就向客户端返回请求的数据
  3. 缺点:

  • 每次请求都需要查库【查询redis数据库】,效率低

  • 如果redis保存的数据多【用户登录信息,当前用户的权限信息,当前用户的菜单信息】,会影响性能。

三. JWT方案

1. 为什么要用JWT ?

如果将登录信息放在redis - 只存登录信息也还行
如果1.并发量高  2.保存的不只是登录信息,还有菜单和权限  redis保存的数据就非常多。redis内存数据库,影响服务器的性能

jwt:登录成功,把登录信息还有菜单和权限进行加密【jwt - json web token = 加密之后的字符串】
将jwt保存在浏览器的localStorage中

2. 什么是JWT

JSON Web Token【JWT】是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息

通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token并且这个JWT token带有签名信息,接收后可以校验是否被篡改。所以可以用于在各方之间安全地将信息作为Json对象传输。

服务器生成JWT token后,响应给浏览器客户端。客户端保存起来。在后续的请求中,客户端将JWT token连同请求内容一起发送给服务器,服务器收到请求后通过JWT token验证用户,如果验证不通过则不返回请求的数据 。验证通过就会向客户端返回请求的数据。

总结:使用JWT生产的Token是安全的,可以理解成就是在无状态的token方案基础上,将token从随机串换成包含登录人信息、权限等内容,且做了加密处理之后的串,实现了数据的安全传输。

image-20220923184833243

3. JWT特点

  • 基于JSON,方便解析,因为JSON的通用性,所以JWT可以跨语言支持
  • 可以在令牌中定义内容,方便扩展。他不是一个随机token串,而是可以携带自定义内容的加密token串
  • 使用非对称加密算法中提供数字签名,JWT防篡改
  • 后端服务使用JWT可以不依赖redis即可完成权限校验

4. JWT组成

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分:我们称它为头部(header),用于存放token类型和加密协议,一般都是固定的
第二部分:我们称其为载荷(payload),用户数据就存放在里面
第三部分:是签证(signature),主要用于服务端的验证
  • 头部【header】: JSON格式,描述JWT的最基本的信息:
    {
      'typ': 'JWT',
      'alg': 'HS256'
    }
    

    jwt 的头部承载两部分信息:

    • 声明类型 , 告知这里是 jwt
    • 声明加密的算法 通常直接使用 HMAC, SHA256

    在使用过程中会对该JSON进行BASE64编码,得到Jwt的第一部分:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

  • 载荷【playload】 :JSON格式,用户数据就存放在里面,也需要BASE64编码:
    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
    然后将其进行BASE64加密,得到Jwt的第二部分:
    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
    
    载荷playload也包含三部分:
    1. 标准中注册的声明(建议但不强制使用)
        iss: jwt签发者
        sub: jwt所面向的用户zs
        aud: 接收jwt的一方
        exp: jwt的过期时间,这个过期时间必须要大于签发时间
        nbf: 定义在什么时间之前,该jwt都是不可用的
        iat: jwt的签发时间
        jti: jwt的唯一身份标识,主要用来作为一次性token
    2. 公共的声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密
    3. 私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息
    
  • 签名 【signature】: jwt的第三部分是一个签证信息,通过指定的算法生成哈希,以确保数据不会被篡改,这个签证信息由三部分组成:
    head(base64编码后的)
    playload(base64编码后的)
    secret(秘钥)
    

    这个部分需要BASE64加密后的headerBASE64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:

    let encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
    let signature = HMACSHA256(encodedString, '密钥');
    

    加密之后,得到signature签名信息,即Jwt的第三部分:
    TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

  • 将这三部分用.连接成一个完整的字符串,就构成了最终的Jwt:
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    

四. 加密算法介绍

明文:加密之前的内容,原始内容
暗文:加密之后的内容
公钥:可见的公共的钥匙
私钥:不可见的私有的钥匙

1. 不可逆加密算法

特征:只能加密不能解密
技术:md5
作用:一般对登录密码处理,用于做密码比对
问题:只能加密,不能解密,不能用来对网络中传输的数据进行加密

2. 可逆对称加密算法

特征:
    1. 可以加密,也可以解密
    2. 加密和解密的密钥是同一个
实现:DES,AES
作用:对数据库密码进行加密
算法:
    密文为s,加解密算法为AES,秘钥为k,明文为c
    加密:s = AES(k,c)
    解密:c = AES(k,s)
问题:
    1. 数据可能会被其他人解密
    2. 数据可能会被篡改

3. 可逆非对称加密算法

特征:
    1. 可以加密,也可以解密
    2. 加密和解密的密钥不是同一个。但是是成对出现的。一个私钥就对应一个公钥。如果使用私钥加密,
       只能使用与之对应公钥来解决。反之如果使用公钥加密,只能使用与之对应私钥解密
实现:RSA,RSA2
作用:网络传输中对数据进行加解密
算法:
    密文为s,加解密算法为RSA私钥为k1,公钥为k2,明文为c
    第一组:私钥加密公钥解密
        加密:s = rsa(k1,c)
        解密:c = rsa(k2,s)
    第二组:公钥加密私钥解决
        加密:s = rsa(k2,c)
        解密:c = rsa(k1,s)
注意:加密一次不安全,要加密两次,解密两次。第一次加密和解密并不是真正的数据,而是数字签名和签名认证/确认身份

4. .网络加密技术有哪些?

  • ​ 1.不可逆【只能加密不能解密】的加密技术:md5

    ​ 用来对比密码,不能用来传输数据

  • ​ 2.可逆【可以加密也能解密】对称【加密和解密使用的是同一个秘钥】加密算法:AES,DES

    ​ 风险:截取数据

    ​ 篡改数据

  • ​ 3.可逆非对称【加密和解密使用的不是同一个秘钥,使用公钥和私钥】

    ​ 前提:交换公钥

    ​ 加密:篡改数据

    加密2次,解密2次:

    ​ 先用对方的公钥加密,然后再用自己的私钥加密

    ​ 先用对方的公钥解密,然后再用自己的私钥解密

五. 常用工具类

生成JWT,需要先获取公钥,私钥

1. 依赖

    <!--     JWT   -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.11</version>
        </dependency>

2. RsaUtils

package io.coderyeah.basic.jwt;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * RSA工具类   负责对RSA密钥的创建、读取功能(公钥和私钥)
 */
public class RsaUtils {

    private static final int DEFAULT_KEY_SIZE = 2048;  // 生成的大小

    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }


    /**
     * 从文件中读取密钥
     *
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) {
        try{
            bytes = Base64.getDecoder().decode(bytes);
            X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
            KeyFactory factory = KeyFactory.getInstance("RSA");
            return factory.generatePublic(spec);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     */
    public static void generateKey(String publicKeyFilename,
                                   String privateKeyFilename,
                                   String secret, int keySize) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }


    public static void main(String[] args) throws Exception{
        //1 生成秘钥对 xxx_rsa.pub xxxx_rsa
        generateKey("E:\\springboot\\pethome\\src\\main\\resources\\auth_rsa.pub",
                "E:\\springboot\\pethome\\src\\main\\resources\\auth_rsa.pri","coderyeah",2048);
    }
}

3. JwtUtils

package io.coderyeah.basic.jwt;

import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;

import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
import java.util.UUID;

/**
 * JWT 密钥的解析和加密 工具类
 */
public class JwtUtils {

    private static final String JWT_PAYLOAD_USER_KEY = "user";


    private static String createJTI() {
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }
    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位分钟
     * @return JWT
     */
    public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JSONObject.toJSONString(userInfo))
                .setId(createJTI())
                //当前时间往后加多少分钟
                .setExpiration(DateTime.now().plusMinutes(expire).toDate())
                .signWith(SignatureAlgorithm.RS256,privateKey)
                .compact();

    }

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位秒
     * @return JWT
     */
    public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JSONObject.toJSONString(userInfo))
                .setId(createJTI())
                .setExpiration(DateTime.now().plusSeconds(expire).toDate())
                .signWith(SignatureAlgorithm.RS256,privateKey)
                .compact();
    }

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥
     * @return Jws<Claims>
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }


    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        T t = JSONObject.parseObject(body.get(JWT_PAYLOAD_USER_KEY).toString(),userType);
        claims.setLoginData(t);
        claims.setExpiration(body.getExpiration());
        return claims;
    }

    /**
     * 获取token中的载荷信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setExpiration(body.getExpiration());
        return claims;
    }


    public static void main(String[] args) throws Exception {
        // 1 获取token
        PrivateKey privateKey = RsaUtils.getPrivateKey(JwtUtils.class.getClassLoader().getResource("auth_rsa.pri").getFile());
        System.out.println(privateKey);
        String token = generateTokenExpireInSeconds(new User(1L, "zs"), privateKey, 10);
        System.out.println(token);

        // 2 解析token里面内容
        PublicKey publicKey = RsaUtils.getPublicKey(JwtUtils.class.getClassLoader().getResource("auth_rsa.pub").getFile());
        Payload<User> payload = getInfoFromToken(token, publicKey, User.class);
        System.out.println(payload);
        Thread.sleep(11000); //超时后继续解析

    }
}

class User{
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public User() {
    }

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

4. 载荷数据

package io.coderyeah.basic.jwt;

import java.util.Date;

public class Payload<T> {

    private String id;  // jwt的id(token)
    private T loginData;  // 用户信息:用户数据,不确定,可以是任意类型
    private Date expiration;  // 过期时间

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public T getLoginData() {
        return loginData;
    }

    public void setLoginData(T loginData) {
        this.loginData = loginData;
    }

    public Date getExpiration() {
        return expiration;
    }

    public void setExpiration(Date expiration) {
        this.expiration = expiration;
    }

    @Override
    public String toString() {
        return "Payload{" +
                "id='" + id + '\'' +
                ", loginData=" + loginData +
                ", expiration=" + expiration +
                '}';
    }
}

5. 需要保存到前端的数据

LoginData

package io.coderyeah.basic.jwt;

import io.coderyeah.system.domain.Menu;
import io.coderyeah.user.domain.LoginInfo;
import io.coderyeah.user.domain.User;
import lombok.Data;

import java.util.List;

@Data
public class LoginData {
   //1.登录信息对象Lonininfo对象 - 在前端显示用户数据信息的【
    private Logininfo logininfo;
    //2.当前登录人的所有权限的sn - 按钮或资源权限【没有访问该资源的按钮直接不显示】
    private List<String> permissions;
    //3.当前登录人的菜单信息 - 菜单权限【不同的人登录之后菜单是不一样的】
    private List<Menu> menus;
}

六. 业务实现

1. 用户登录成功后使用jwt返回客户端数据

 // 对登录成功的用户信息进行jwt加密
    private Map<String, Object> loginSuccessJwtHandler(LoginInfo loginInfo) {
        final HashMap<String, Object> map = new HashMap<>();
        final LoginData loginData = new LoginData();
        // 登录信息
        loginInfo.setSalt(null);
        loginInfo.setPassword(null);
        map.put("loginInfo", loginInfo);
        loginData.setLoginInfo(loginInfo);
        if (loginInfo.getType() == 0) {// 管理员用户
            // 获取登录用户所有权限
            final List<String> permissions = employeeMapper.getPermissionSnByLoginInfoId(loginInfo.getId());
            map.put("permissions", permissions);
            loginData.setPermissions(permissions);
            // 获取登录用户所有菜单
            List<Menu> menus = employeeMapper.getMenus(loginInfo.getId());
            map.put("menus", menus);
            loginData.setMenus(menus);
        }
        try {
            // 生成私钥
            final PrivateKey privateKey = RsaUtils.getPrivateKey(LoginInfoServiceImpl.class.getClassLoader().getResource("auth_rsa.pri").getFile());
            // 使用私钥对登录数据进行加密
            final String token = JwtUtils.generateTokenExpireInMinutes(loginData, privateKey, 30);
            map.put("token", token);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

获取登录用户所有菜单

 <resultMap id="menuMap" type="io.coderyeah.system.domain.Menu">
        <id property="id" column="mid"/>
        <result property="name" column="mname"/>
        <result property="icon" column="micon"/>
        <collection property="children" ofType="io.coderyeah.system.domain.Menu">
            <id property="id" column="id"/>
            <result property="name" column="name"/>
            <result property="component" column="component"/>
            <result property="url" column="url"/>
            <result property="icon" column="icon"/>
            <result property="index" column="index"/>
            <result property="parentId" column="parent_id"/>
            <result property="intro" column="intro"/>
            <result property="state" column="state"/>
        </collection>
    </resultMap>
    
<select id="getMenus" resultMap="menuMap">
    select tm1.id mid, tm1.name mname, tm1.icon micon, tm2.*
    from t_menu tm1
             join
         (
             select tm.*
             from t_employee te
                      join t_employee_role ter on te.id = ter.employee_id
                      join t_role tr on tr.id = ter.role_id
                      join t_role_menu trm on tr.id = trm.role_id
                      join t_menu tm on trm.menu_id = tm.id
             where te.logininfo_id = #{id}
         ) tm2
         on tm1.id = tm2.parent_id
</select>

2. 账号登录(示例)

 // 账户登录
    @Override
    public Map<String, Object> accountLogin(LoginDto loginDto) {
        // 效验空值
        if (StrUtil.isBlank(loginDto.getAccount()) || StrUtil.isBlank(loginDto.getCheckPass())) {
            throw new BusinessException("信息不能为空!!!");
        }
        // 账号效验
        LoginInfo loginInfo = checkLogin(loginDto);
        // 效验密码
        if (!DigestUtil.md5Hex(loginInfo.getSalt() + loginDto.getCheckPass()).equals(loginInfo.getPassword())) {
            throw new BusinessException("账号或密码错误!!!");
        }
        if (!loginInfo.getDisable()) {
            throw new BusinessException("该账号被禁用,请联系管理员!!!");
        }
//        生成token,并将登录信息保存到redis数据库,设置30有效
        final Map<String, Object> map = loginSuccessJwtHandler(loginInfo);
        return map;
    }

3. 登录拦截器核心代码

//1.获取token
String token = req.getHeader("token");
//3.如果有token,通过token获取redis的登录信息
if (token != null) {
     LoginInfo info=null;
    try {
        // (私钥加密)获取公钥解密
        final PublicKey publicKey = RsaUtils.getPublicKey(LoginInterceptor.class.getClassLoader().getResource("auth_rsa.pub").getFile());
        // 获取用户信息
        final Payload<LoginData> payload = JwtUtils.getInfoFromToken(token, publicKey, LoginData.class);
        info = payload.getLoginData().getLoginInfo();
    } catch (ExpiredJwtException e) { //jwt过期时抛出的异常
        resp.setContentType("application/json;charset=UTF-8");
        resp.getWriter().println("{\"success\":false,\"message\":\"timeout\"}");
        return false;
    }

4.前端后置拦截器

//======================axios的后置拦截器【处理后台登录拦截的结果】====================//
axios.interceptors.response.use(res => {
    //后端响应的是没有登录的信息
    if (false === res.data.success && "noLogin" === res.data.message) {
        localStorage.removeItem("token");
        localStorage.removeItem("loginInfo");
        localStorage.removeItem("menus");
        localStorage.removeItem("permissions");
        router.push({path: '/login'});
    }
    if (false === res.data.success && "noPermission" === res.data.message) {
        Message.warning('您没有访问权限')
    }
    if (false === res.data.success && "timeout" === res.data.message) {
        localStorage.removeItem("token");
        localStorage.removeItem("loginInfo");
        localStorage.removeItem("menus");
        localStorage.removeItem("permissions");
        Message.error('超时啦')
    }
    return res;
}, error => {
    Promise.reject(error)
})
//======================axios的后置拦截器【处理后台登录拦截的结果】====================//

5. 登录成功时需要存储信息到浏览器本地

this.$message({
  message: "登录成功",
  type: 'success'
});
let {token, loginInfo, menus, permissions} = res.data.data
localStorage.setItem("token", token)
localStorage.setItem("loginInfo", JSON.stringify(loginInfo))
localStorage.setItem("menus", JSON.stringify(menus))
localStorage.setItem("permissions", JSON.stringify(permissions))

七. 动态菜单

1. router.js中的需要动态展示的路由配置需要去掉 (保留5个)

import Login from './views/Login.vue'
import NotFound from './views/404.vue'
import Home from './views/Home.vue'
import echarts from './views/charts/echarts.vue'
const ShopRegister = () => import('./views/ShopRegister')

let routes = [
    {
        path: '/register',
        component: ShopRegister,
        name: '',  //不需显示name没有意义
        hidden: true //不需要在菜单显示
    },
    {
        path: '/login',
        component: Login,
        name: '',
        hidden: true
    },
    {
        path: '/404',
        component: NotFound,
        name: '',
        hidden: true
    },

    {
        path: '/',
        component: Home,
        name: '图形化数据展示',
        iconCls: 'el-icon-s-data',
        children: [
            {path: '/echarts', component: echarts, name: 'echarts'}
        ]
    },
    {
        path: '*',
        hidden: true,
        redirect: {path: '/404'}
    }
];

export default routes;

2.login.vue页面登录成功之后需要刷新一下本地的路由缓存

let {token, loginInfo, menus, permissions} = res.data.data
localStorage.setItem("token", token)
localStorage.setItem("loginInfo", JSON.stringify(loginInfo))
localStorage.setItem("menus", JSON.stringify(menus))
localStorage.setItem("permissions", JSON.stringify(permissions))
console.log(res.data);
//跳转到后台首页
this.$router.push({path: '/echarts'});
// 刷新路由缓存
location.reload()

3. main.js配置动态路由(vue中的@符号表示在src路径下)

//处理页面刷新动态路由失效问题
initIndexRouters();

function initIndexRouters() {
    // 判断本地是否有当前用户的菜单权限
    if (!localStorage.menus) {
        return;
    }
    //防止重复配置路由:5就是main.js中路由的个数 - 如果你的静态路由是6个这里要写成6
    if (router.options.routes.length > 5) {
        return;
    }
    // 获取本地登录用户的菜单
    let menus = localStorage.getItem('menus');
    // 将保存在本地的json字符串转化为json对象
    menus = JSON.parse(menus);
    let tempRouters = [];
    // 遍历当前用户所有的菜单
    menus.forEach(menu => {
        let indexRouter = {
            path: '/',
            iconCls: menu.icon,
            name: menu.name,
            component: resolve => require(['@/views/Home'], resolve),
            children: []
        }
        // 遍历所有子级菜单
        menu.children.forEach(cMenu => {
            let cr = {
                path: cMenu.url,
                name: cMenu.name,
                iconCls: cMenu.icon,
                component: resolve => require(['@/views/' + cMenu.component], resolve)
            }
            indexRouter.children.push(cr)
        })
        tempRouters.push(indexRouter)
        router.options.routes.push(indexRouter)
    })
    //动态路由配置
    router.addRoutes(tempRouters);
}

八. 按钮权限的实现

1. 获取当前登录用户的所有权限

前面登录成功会将登录人的权限数据封装permissions并返回给前端

<select id="getPermissionSnByLoginInfoId" resultType="java.lang.String">
    select tp.sn
    from t_employee te
             join t_employee_role ter on te.id = ter.employee_id
             join t_role tr on ter.role_id = tr.id
             join t_role_permission trp on tr.id = trp.role_id
             join t_permission tp on tp.id = trp.permission_id
    where te.logininfo_id = #{id}
</select>

2. 自定义vue指令

语法格式:

Vue.directive('指令名', {
    // 当被绑定的元素插入到 DOM 中时……
    inserted: (el, binding, vnode) => {
       // 需要完成的操作。。。
    }
});

3. 定义vue权限指令

  • 可在src/common/js/permission.js中定义权限指令

    import Vue from 'vue';
    // 注册一个全局自定义指令 `v-perm`
    Vue.directive('perm', {
        // 当被绑定的元素插入到 DOM 中时……
        inserted: (el, binding, vnode) => {
            //获取自定义标签v-perm的值
            const value = binding.value;
            //json格式字符串
            let permissions = localStorage.getItem('permissions');
            if (permissions) {
                //转成json对象
                let auths = JSON.parse(permissions);
                //将数组中的每一个元素按照,号进行拼接  然后 再检索
                if (auths.join(",").indexOf(value) == -1) {
                    //如果不包含权限就移除
                    el.parentNode.removeChild(el);
                }
            }
        }
    });
    

4. 在main.js中引用

import permission from './common/js/permission'
//@ 等价于 /src 这个目录,避免写麻烦又易错的相对路径
import '@/common/js/permission'

5. 使用举例

<el-form-item>
    <el-button type="primary" v-on:click="keywordQuery" v-perm="'department:list'" >
        关键字查询
    </el-button>
</el-form-item>
<el-form-item>
    <el-button type="primary" v-perm="'department:save'" @click="handleAdd">
        新增
    </el-button>
</el-form-item>

<el-button size="small" v-perm="'department:update'" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button type="danger" size="small" v-perm="'department:delete'" @click="handleDel(scope.$index, scope.row)">删除</el-button>
-------------本文结束感谢您的阅读-------------
you can reward me here. Thank you!

欢迎关注我的其它发布渠道