天天看点

2021年1月23日 springboot + jwt + axios + vue-element ui 简单demo

简介

demo在这里

https://github.com/tao1993/spring-boot-demos/tree/main/demo4

jwt的机制大概是后端生成一个token,前端自己每次请求带上这个token来给后端做认证

JWT流程

-》 用户登录

-》 服务器根据用户信息生成一个token作为令牌

-》令牌保存在客户端(一般是localStorage或sessionStorage),服务端不做保存

-》客户端之后每次请求都要带该令牌 (一般是http的header的authorization)

-》 服务端只需要验证该令牌 (签名是否正确,token是否过期,token的接收方是否是自己 等等)

验证ok就放行,不ok就不放行

三个组成部分就是三个json对象,最后会被整合成一个字符串

头部 header

负载 payload

签名 signature

2021年1月23日 springboot + jwt + axios + vue-element ui 简单demo

参考资料

jwt的部分我是跟着b站的视频教程学的,参考这个,通俗易懂

【编程不良人】JWT认证原理、流程整合springboot实战应用,前后端分离认证的解决方案!

前端部分,vue和element ui 用来做界面,axios用来发请求,都是第一次用,前台代码我乱写的

vue文档

axios中文文档

element ui 的NavMenu 导航菜单

Window localStorage 属性

环境

spring boot 2.3.7

jwt 3.12.0

前端效果

未登录

2021年1月23日 springboot + jwt + axios + vue-element ui 简单demo

登录后

2021年1月23日 springboot + jwt + axios + vue-element ui 简单demo

访问被保护资源

2021年1月23日 springboot + jwt + axios + vue-element ui 简单demo
2021年1月23日 springboot + jwt + axios + vue-element ui 简单demo

目录结构

2021年1月23日 springboot + jwt + axios + vue-element ui 简单demo

集成过程

1.添加依赖

jwt用这个

<dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>3.12.0</version>
      </dependency>
           

pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.12.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
           

2.封装JWT

src/main/java/cc/mm/demo4/UtilJWT.java

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.Map;

public class UtilJWT {

    //加密用到的私钥
    private static final String SECRET_KEY = "[email protected]#$$";

    public static String createToken(Map<String, String> map) {

        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.HOUR, 24 * 7);  //token 设定7天过期
        JWTCreator.Builder builder = JWT.create();
        //通过withClaim()放一些用户信息,这些信息是可以被轻松解码解密的,不要放敏感信息在这里
        map.forEach((k, v) -> {
            builder.withClaim(k, v);
        });
        String token = builder
                .withExpiresAt(calendar.getTime())  //设定token 过期信息
                .sign(Algorithm.HMAC384(SECRET_KEY));  //指定加密算法,注意创建token用到的加解密算法要和后面的verify()验证 统一
        return token;

    }

    //返回 ok  表示验证成功
    //返回其他的表示错误信息
    public static String verify(String token) {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC384(SECRET_KEY)).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);
            return "ok";
        } catch (SignatureVerificationException e) {
            return "无效签名";
        } catch (TokenExpiredException e) {
            return "token过期";
        } catch (AlgorithmMismatchException e) {
            return "token算法不一致";
        } catch (Exception e) {
            return "无效签名(其他错误)";
        }
    }

    public static DecodedJWT getDecodedJWT(String token) {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC384(SECRET_KEY)).build();
        DecodedJWT verify = jwtVerifier.verify(token);
        return verify;
    }

}
           

3.配置拦截器

src/main/java/cc/mm/demo4/interceptors/JwtInterceptor.java

import cc.mm.demo4.UtilJWT;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JwtInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        String verifyResult = UtilJWT.verify(token);
        if(verifyResult.equals("ok")) {
            //验证成功  放行
            return true;
        } else {
            //验证失败  返回一个json
            JSONObject json = new JSONObject();
            json.put("result","fail");
            json.put("info",verifyResult);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            return false;
        }

    }

}
           

src/main/java/cc/mm/demo4/ConfigInterceptor.java

import cc.mm.demo4.interceptors.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class ConfigInterceptor implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor())
                .addPathPatterns("/profile");   //测试demo,仅对 /profile 路径做token验证
                //.excludePathPatterns("/")

    }
}
           

4.controller和enity

src/main/java/cc/mm/demo4/controller/HomeController.java

import cc.mm.demo4.UtilJWT;
import cc.mm.demo4.enity.User;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

@Controller
public class HomeController {


    @GetMapping("/")
    String index(){
        return "index";
    }


    //我的资料 数据,在拦截器里面配置了要带token的请求才能访问
    @PostMapping ("/profile")
    @ResponseBody
    Object profile(HttpServletRequest req){
        String token = req.getHeader("token");
        String strPayload = UtilJWT.getDecodedJWT(token).getPayload();
        strPayload = new String(Base64.getDecoder().decode(strPayload));
        //测试,简单返回一些内容
        JSONObject json = JSON.parseObject(strPayload);
        json.put("info","哈哈哈");
        return json;
    }


    //  接收前台的用户登录请求,验证账号和密码
    //  一开始发现 axios发的请求参数 我spring boot后台接收不到,解决办法参考:
    //   https://blog.csdn.net/qq_36208461/article/details/103079514
    //  我是通过后台加 @RequestBody   解决的,前台axios不做改动,使用默认配置
    @RequestMapping ("/login")
    @ResponseBody
    Object login(@RequestBody User user){
        JSONObject json = new JSONObject();

        //这里因为测试demo,简单模拟验证过程
        if(user.getUserName().equals("abc") && user.getUserPass().equals("123")) {
            //账号密码对的上说明登陆成功,开始用jwt生成一个token返回给前端,让前端自己去把token保存到localStorage
            //可以把一些用户信息放进token,但不要放敏感信息
            Map<String,String> map = new HashMap<>();
            map.put("userId","11122");  //因为测试,简单写死
            map.put("userName",user.getUserName());
            String token = UtilJWT.createToken(map);
            System.out.println(token);
            //给前端返回信息
            json.put("token",token);
            json.put("result","success");
        }else {
            json.put("result","fail");  //登录失败
        }
        return json;
    }

}
           

src/main/java/cc/mm/demo4/enity/User.java

import lombok.Data;

@Data
public class User {
    String userName;
    String userPass;
}

           

5.前端页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <!-- 引入vue  -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <!-- 引入element ui  -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <!-- 引入axios  -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>

<body>
<div id="app">
    <el-menu
            :default-active="activeIndex2"
            class="el-menu-demo"
            mode="horizontal"
            @select="handleSelect"
            background-color="#545c64"
            text-color="#fff"
            active-text-color="#ffd04b">

        <el-menu-item index="1">首页</el-menu-item>


        <!-- <el-menu-item v-if="isLogined"  >-->
        <el-menu-item @click="postProfileWithToken">
            我的资料(带token请求)
        </el-menu-item>

        <el-menu-item @click="postProfileWithoutToken">
            我的资料(不带token请求)
        </el-menu-item>

        <!--登录时显示-->
        <el-menu-item v-if="isLogined" @click="onLogout">注销</el-menu-item>


        <!--未登录时显示-->
        <el-menu-item v-if="!isLogined" index="4">
            <el-form :inline="true" :model="userData" class="demo-form-inline">
                <el-form-item>
                    <el-input style="margin-top: 7px;" v-model="userData.userName" placeholder="账号"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input style="margin-top: 7px;" v-model="userData.userPass" placeholder="密码"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button style="margin-top: 7px;" type="primary" @click="onSubmit">登录</el-button>
                </el-form-item>
            </el-form>
        </el-menu-item>



        <!--登录时显示-->
        <el-menu-item v-if="isLogined">欢迎用户: {{ currentUserName }}</el-menu-item>
    </el-menu>
</div>

</body>

<script>
    //Base64工具,可以用来前台解码token,拿到token的payload的用户信息
    Base64 = { _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", encode: function(e) { var t = ""; var n, r, i, s, o, u, a; var f = 0; e = Base64._utf8_encode(e); while (f < e.length) { n = e.charCodeAt(f++); r = e.charCodeAt(f++); i = e.charCodeAt(f++); s = n >> 2; o = (n & 3) << 4 | r >> 4; u = (r & 15) << 2 | i >> 6; a = i & 63; if (isNaN(r)) { u = a = 64 } else if (isNaN(i)) { a = 64 } t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a) } return t }, decode: function(e) { var t = ""; var n, r, i; var s, o, u, a; var f = 0; e = e.replace(/[^A-Za-z0-9+/=]/g, ""); while (f < e.length) { s = this._keyStr.indexOf(e.charAt(f++)); o = this._keyStr.indexOf(e.charAt(f++)); u = this._keyStr.indexOf(e.charAt(f++)); a = this._keyStr.indexOf(e.charAt(f++)); n = s << 2 | o >> 4; r = (o & 15) << 4 | u >> 2; i = (u & 3) << 6 | a; t = t + String.fromCharCode(n); if (u != 64) { t = t + String.fromCharCode(r) } if (a != 64) { t = t + String.fromCharCode(i) } } t = Base64._utf8_decode(t); return t }, _utf8_encode: function(e) { e = e.replace(/rn/g, "n"); var t = ""; for (var n = 0; n < e.length; n++) { var r = e.charCodeAt(n); if (r < 128) { t += String.fromCharCode(r) } else if (r > 127 && r < 2048) { t += String.fromCharCode(r >> 6 | 192); t += String.fromCharCode(r & 63 | 128) } else { t += String.fromCharCode(r >> 12 | 224); t += String.fromCharCode(r >> 6 & 63 | 128); t += String.fromCharCode(r & 63 | 128) } } return t }, _utf8_decode: function(e) { var t = ""; var n = 0; var r = c1 = c2 = 0; while (n < e.length) { r = e.charCodeAt(n); if (r < 128) { t += String.fromCharCode(r); n++ } else if (r > 191 && r < 224) { c2 = e.charCodeAt(n + 1); t += String.fromCharCode((r & 31) << 6 | c2 & 63); n += 2 } else { c2 = e.charCodeAt(n + 1); c3 = e.charCodeAt(n + 2); t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63); n += 3 } } return t } }
</script>

<script>

    var app = new Vue({
        el: '#app',
        data: {
            activeIndex: '1',
            activeIndex2: '1',
            userData: {
                userName: '',
                userPass: ''
            }
        },
        methods: {
            handleSelect(key, keyPath) {
                //console.log(key, keyPath);
            },
            onSubmit() {

                //用户登录,post请求向后台 验证账号和密码
                let user = this.userData;
                axios.post('/login', {
                    userName: user.userName,
                    userPass: user.userPass
                }).then(function (response) {
                    //console.log(response);

                    if (response.data.result === 'fail') {
                        app.$message('登录失败');
                    } else if (response.data.result === 'success') {
                        console.log(response.data.token)
                        //登录成功,拿到token,放入localStorage
                        localStorage.setItem("token", response.data.token)
                        //刷新页面
                        location.reload();
                    }

                }).catch(function (error) {
                    console.log(error);
                });
            },
            onLogout() {  //注销操作
                //清除localStorage里面的token,然后刷新页面
                localStorage.removeItem("token");
                location.href = "/";
            },
            postProfileWithToken(){  //带上token访问 /profile
                axios({
                    url:'/profile',
                    method:'post',
                    headers: {'token': localStorage.getItem("token")},
                    data:{},
                }).then(function (response) {
                    app.$message(JSON.stringify(response.data));
                    console.log(response);
                }).catch(function (error) {
                    console.log(error);
                });
            },
            postProfileWithoutToken(){  // 不带token访问 /profile
                axios({
                    url:'/profile',
                    method:'post',
                    data:{},
                }).then(function (response) {
                    app.$message(JSON.stringify(response.data));
                    console.log(response);
                }).catch(function (error) {
                    console.log(error);
                });
            }
        },
        computed: {
            isLogined() {
                //判断当前是否有用户已登录状态,从localstorage里面找token
                //没有token说明未登录
                if (localStorage.getItem("token")) {
                    return true;
                }
                return false;
            },
            currentUserName(){
                //解析localStorage里面的token,获得payload部分的用户信息userName
                let token = localStorage.getItem("token");
                let user = decodeURIComponent(escape(window.atob(token.split('.')[1])));
                let userName = JSON.parse(user).userName;
                return userName;
            }
        }

    })
</script>


</html>