一. 概述

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

二. 无状态的token方案

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

  2. 后续浏览器只需要将token携带到服务器,服务器就可以根据浏览器的token令牌获取redis的信息
    2.1. 如果获取不到信息,说明token令牌无效
    2.2. 获取到信息,就向客户端返回请求的数据

  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组成

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

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

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

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

  • 载荷【playload】 :JSON格式,用户数据就存放在里面,也需要BASE64编码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    "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的第三部分是一个签证信息,通过指定的算法生成哈希,以确保数据不会被篡改,这个签证信息由三部分组成:
    1
    2
    3
    head(base64编码后的)
    playload(base64编码后的)
    secret(秘钥)

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

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

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

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

四. 加密算法介绍

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

1. 不可逆加密算法

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

2. 可逆对称加密算法

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

3. 可逆非对称加密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
特征:
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. 依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--     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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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. 载荷数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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返回客户端数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 对登录成功的用户信息进行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;
}

获取登录用户所有菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 <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. 账号登录(示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 // 账户登录
@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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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.前端后置拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//======================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. 登录成功时需要存储信息到浏览器本地

1
2
3
4
5
6
7
8
9
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个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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页面登录成功之后需要刷新一下本地的路由缓存

1
2
3
4
5
6
7
8
9
10
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路径下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//处理页面刷新动态路由失效问题
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并返回给前端

1
2
3
4
5
6
7
8
9
<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指令

语法格式:

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

3. 定义vue权限指令

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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中引用

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

5. 使用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>