参考 B 站楠哥教你学 Java30 分钟学会 Spring Boot + JWT + Vue视频

JWT 介绍

什么是 JWT?

JSON Web Token,通过数字签名的方式,以 JSON 对象为载体,在不同的服务终端之间安全的传输信息。

JWT 有什么用?

JWT 最常见的场景就是授权认证,一旦用户登录,后续每个请求都将包含 JWT,系统在每次处理用户请求的之前,都要先进行 JWT 安全校验,通过之后再进行处理。

JWT 的组成

JWT 由 3 部分组成,用.拼接

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2MjMyMjM2NzUsImp0aSI6ImQ2MTJjZjcxLWI5ZmUtNGMwNy04MzQwLTViOWViZmMyNjExNyJ9.FOS9Y7rYNdc2AOidnSPrgg2XTYePU0yGZ598h2gtabE

这三部分分别是:

Header

1
2
3
4
{
'typ': 'JWT',
'alg': 'HS256'
}

Payload

1
2
3
4
5
{
"sub": '1234567890',
"name": 'john',
"admin":true
}

Signature

1
2
3
var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, "secret");

所需依赖

pom.xml

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
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>

实现登录认证功能(核心代码)

本次代码以用户 User 为例

前端(Vue 3.x)

  1. Login.vue里添加代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    login() {
    request.post("/user/login", this.form).then((res) => {
    if (res.code === "0") {
    localStorage.setItem('access', JSON.stringify(res.data))
    let message = '欢迎"' + res.data.username + '"用户登录'
    ElMessage.success(message);
    this.$router.replace('/'); //登录成功之后进行页面的跳转,跳转到首页
    } else {
    ElMessage.error(res.msg);
    }
    });
    },
  2. src\router\index.js添加代码:(按需更改)

    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
    // 路由跳转之前要进行的操作
    router.beforeEach((to, from, next) => {
    //如果当前在登录页面,则把本地存储用户信息移除
    if (to.path == "/login" || to.path == "/register") {
    window.localStorage.removeItem("access");
    next();
    } else {
    //如果在其它页面,则将用户信息在数据库里验证一下
    let access = JSON.parse(window.localStorage.getItem("access"));
    if (!access) {
    ElMessage.warning("请先登录!");
    next({ path: "/login" });
    } else if (access.role == 1) {
    //普通用户
    //是否在后台页面
    if (to.path.startsWith("/back")) {
    next({ path: "/403" });
    } else {
    //校验token合法性
    request
    .get("/user/checkToken", {
    headers: {
    token: access.token,
    },
    })
    .then((res) => {
    if (!res) {
    ElMessage.error("用户登录信息失效");
    next({ path: "/login" });
    }
    next();
    });
    }
    } else if (access.role == 2) {
    //管理员
    //校验token合法性
    request
    .get("/admin/checkToken", {
    headers: {
    token: access.token,
    },
    })
    .then((res) => {
    if (!res) {
    ElMessage.error("管理员登录信息失效");
    next({ path: "/login" });
    }
    next();
    });
    }
    }
    });
  3. 添加src\utils\request.js:(按需添加)

    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
    import axios from "axios";

    const request = axios.create({
    baseURL: "/api", // 注意!! 这里是全局统一加上了 '/api' 前缀,也就是说所有接口都会加上'/api'前缀在,页面里面写接口的时候就不要加 '/api'了,否则会出现2个'/api',类似 '/api/api/user'这样的报错,切记!!!
    timeout: 5000,
    });

    // request 拦截器
    // 可以自请求发送前对请求做一些处理
    // 比如统一加token,对请求参数统一加密
    request.interceptors.request.use(
    (config) => {
    config.headers["Content-Type"] = "application/json;charset=utf-8";

    // config.headers['token'] = user.token; // 设置请求头
    return config;
    },
    (error) => {
    return Promise.reject(error);
    }
    );

    // response 拦截器
    // 可以在接口响应后统一处理结果
    request.interceptors.response.use(
    (response) => {
    let res = response.data;
    // 如果是返回的文件
    if (response.config.responseType === "blob") {
    return res;
    }
    // 兼容服务端返回的字符串数据
    if (typeof res === "string") {
    res = res ? JSON.parse(res) : res;
    }
    return res;
    },
    (error) => {
    console.log("err" + error); // for debug
    return Promise.reject(error);
    }
    );

    export default request;

在前端解决跨域问题:

  1. 更改vue.config.js代码:(按需添加)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 跨域配置
    module.exports = {
    devServer: {
    port: 8060, // 启动端口号
    proxy: {
    //设置代理,必须填
    "/api": {
    //设置拦截器 拦截器格式 斜杠+拦截器名字,名字可以自己定
    target: "http://localhost:6666", //代理的目标地址(后端)
    changeOrigin: true, //是否设置同源,输入是的
    pathRewrite: {
    //路径重写
    "/api": "", //选择忽略拦截器里面的单词
    },
    },
    },
    },
    };

后端(Spring Boot)

  1. src/main/java/com.hassan/controller/User.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //与数据库表名一一对应,必须写
    @TableName("user")
    @Data
    public class User {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private String pwd;
    private String nickname;
    private String phone;
    private String email;

    @TableField(exist = false)
    private String token;
    //普通用户为1,管理员为2
    @TableField(exist = false)
    private int role = 1;
    }
  2. 新建src/main/java/com.hassan/util/JwtUtil.java

    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
    public class JwtUtil {

    private static long time = 1000*60*60*24; //有效期
    private static String signature = "admin"; //签名

    public static String createToken() {
    JwtBuilder jwtBuilder = Jwts.builder();
    String jwtToken = jwtBuilder
    //header
    .setHeaderParam("typ", "JWT")
    .setHeaderParam("alg", "HS256")
    //payload
    .claim("username", "admin")
    .claim("role", "admin")
    .setSubject("admin-test")
    .setExpiration(new Date(System.currentTimeMillis()+time))
    .setId(UUID.randomUUID().toString())
    //signature
    .signWith(SignatureAlgorithm.HS256, signature)
    .compact();
    return jwtToken;
    }

    public static boolean checkToken(String token) {
    if(token == null) {
    return false;
    }
    try {
    Jws<Claims> claimsJws = Jwts.parser().setSigningKey(signature).parseClaimsJws(token);
    } catch (Exception e) {
    return false;
    }
    return true;
    }
    }
  3. src/main/java/com.hassan/controller/UserController.java

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
public class UserController {

@Resource
UserMapper userMapper;

//登录
@PostMapping("/login")
public Result<?> login(@RequestBody User user) {
User res = userMapper.selectOne(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, user.getUsername())
.eq(User::getPwd, user.getPwd()));
if (res == null) {
return Result.error("-1", "用户名或密码错误");
} else {
//添加Token
res.setToken(JwtUtil.createToken());
return Result.success(res);
}
}

//Token校验
@GetMapping("/checkToken")
public Boolean checkToken(HttpServletRequest request) {
String token = request.getHeader("token");
return JwtUtil.checkToken(token);
}
}
  1. 新建src/main/java/com.hassan/common/Result.java文件(按需添加)

    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
    //Json数据
    public class Result<T> {
    private String code;
    private String msg;
    private T data;

    public String getCode() {
    return code;
    }

    public void setCode(String code) {
    this.code = code;
    }

    public String getMsg() {
    return msg;
    }

    public void setMsg(String msg) {
    this.msg = msg;
    }

    public T getData() {
    return data;
    }

    public void setData(T data) {
    this.data = data;
    }

    public Result() {
    }

    public Result(T data) {
    this.data = data;
    }

    public static Result success() {
    Result result = new Result<>();
    result.setCode("0");
    result.setMsg("成功");
    return result;
    }

    public static <T> Result<T> success(T data) {
    Result<T> result = new Result<>(data);
    result.setCode("0");
    result.setMsg("成功");
    return result;
    }

    public static Result error(String code, String msg) {
    Result result = new Result();
    result.setCode(code);
    result.setMsg(msg);
    return result;
    }
    }

补充

如何取出 localStorage 里的值?

比如:this.user = JSON.parse(window.localStorage.getItem('access'))

结束语

哎,东拼西凑地把毕设上的登录认证功能给完成了,我太拉了