每天一小步-JWT授权

这篇主要介绍Springboot集成JWT生成Token,前端调用后端接口时加层验证的介绍。

大致流程是,后端引入JWT,根据用户登录时传给后端的用户信息生成Token,然后再返回给前端,前端在拿到这个Token后,可以存到浏览器缓存(localstorage)或者cookie中,在后续前端调用后端接口时,把token放在请求头里传给后端,后端会在调用接口前,在JWT拦截器拦截请求,拦截器主要验证Token是否过期,以及是否合法,校验通过后再去调用接口,做其他业务请求。

后端要做的事情:

1.pom.xml引入JWT依赖。

<!-- 引入JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.6.0</version>
</dependency>

2.写JWT实体类,主要有生成token,解析token,获取用户信息

package com.example.bootdemo.helper;

import cn.hutool.core.util.StrUtil;
import com.example.bootdemo.vo.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtils {
    //签名私钥
    private static final String key = "admin";
    //签名失效时间 一分钟
    private static final long failureTime = 60000;

    /**
     * 设置认证token
     *
     * @param id      用户登录ID
     * @param subject 用户登录名
     * @param map     其他私有数据
     * @return
     */
    public static String createJwt(String id, String subject, Map<String, Object> map) {

        //1、设置失效时间啊
        long now = System.currentTimeMillis();  //毫秒
        long exp = now + failureTime;

        //2、创建JwtBuilder
        JwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(subject)
                .setIssuedAt(new Date())
                //设置签名防止篡改
                .signWith(SignatureAlgorithm.HS256, key);

        //3、根据map设置claims
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            jwtBuilder.claim(entry.getKey(), entry.getValue());
        }
        jwtBuilder.setExpiration(new Date(exp));

        //4、创建token
        String token = jwtBuilder.compact();
        return token;
    }


    /**
     * 获取用户登录信息
     *
     * @return User
     */
    public static UserInfo getCurrentUser(String token) {
        if (StrUtil.isBlank(token)) {
            return null;
        }
        //验证token
        Claims map = parseJwt(token);
        System.out.println("map:" + map);

        if (map == null) {
            return null;
        }
        UserInfo user = new UserInfo();

        user.setUserID(Integer.parseInt((map.getId())));
        user.setPhoneNumber((String) map.get("phoneNumber"));
        user.setUsername((String) map.get("sub"));
        System.out.println("user:" + user);
        return user;
    }

    /**
     * 解析token
     *
     * @param token
     * @return
     */
    public static Claims parseJwt(String token) {
        try {
            //截取token头
            String nowToken = token.substring(7);
            System.out.println("token:" + token);
            System.out.println("nowToken:" + nowToken);
            return Jwts.parser().setSigningKey(key).parseClaimsJws(nowToken).getBody();
        } catch (Exception e) {
            // 在这里记录错误或执行其他错误处理逻辑
            System.out.println("error:" + e.getMessage());
            return null;
        }
    }

}

3.编写拦截器,主要逻辑是,拦截前端请求,从请求头中获取token,然后对token进行验证,验证通过则进行下一步。

package com.example.bootdemo.helper;

import cn.hutool.core.util.StrUtil;
import com.example.bootdemo.vo.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("路径 '{}' 已被拦截",request.getRequestURI());
        //1.拦截controller方法,handler还可能是RequestResourceHandler类型,放行
        if(!(handler instanceof HandlerMethod))
            return true;

        //获取token
        String token = request.getHeader("Authorization");  //请求头中获取token
        System.out.println("前端token传:"+ token);

        String requestURI = request.getRequestURI();    //请求URI
        log.info("================request start ===============");
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("request token:{}",token);
        log.info("================request  end ================");
        if (StrUtil.isBlank(token))
            token = request.getParameter("token");  //请求头中没有,从请求参数中获取
        System.out.println("拦截器:"+ token);
        //判断token是否为空
        if (StrUtil.isBlank(token)) {
            log.error("获取当前登录的token失败,token:{}", token);
            return false;
        }

        //登陆验证,并获取user登录基本信息
        UserInfo currentUser = JwtUtils.getCurrentUser(token);
        if(currentUser == null){
            log.error("验证失败,token:{}", token);
            return false;
        }
        //将用户信息存入ThreadLocal中
        UserThreadLocal.setThreadLocal(currentUser);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //删除用户信息,防止内存泄漏
        UserThreadLocal.removeThreadLocal();
    }
}

4.配置拦截信息

package com.example.bootdemo.helper;

import org.springframework.beans.factory.annotation.Autowired;
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 WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器,并指定需要拦截的路径
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/login", "/error", "/swagger**/**", "/v2/api-docs", "/webjars/**"); // 排除不需要拦截的路径
    }

    // 其他配置...
}

ok,后端关于JWT的准备工作做好之后,接下来就是写个login接口,让前端调用,登录接口的逻辑大体是,在判断登陆信息正确后,调用生成token的方法,返回给前端。(当然,我这不全也不太规范,真正的登录的jwt还有很多nb的集成。仅供参考)

//获取token
@PostMapping("/login")
public String login(@RequestBody Map<String, String> loginMap) {
    String username = loginMap.get("username");
    String password = loginMap.get("password");
    //获取用户数据
    UserInfo userInfo = loginService.loginData(username, password);
    if (userInfo == null) {
        return "";
    } else {
        Map<String, Object> map = new HashMap<>();
        map.put("password", userInfo.getPassword());
        map.put("phoneNumber", userInfo.getPhoneNumber());

        return JwtUtils.createJwt(String.valueOf(userInfo.getUserID()), username, map);
    }
}

写一个接口,验证token验证效果。我这个接口地址,会被拦截做验证再调用接口。如果token过期,或者验证不通过,接口不会调用。(单纯为了验证写的接口,忽略接口业务..)

//获取用户信息
@PostMapping("/getUserData")
public UserInfo getUserData(@RequestHeader("Authorization") String token) {
    return JwtUtils.getCurrentUser(token);

}

至此,后端的事情做完了。

前端要做的就是用户登录接口后,拿到token,存起来以备后续接口使用

登录页面:

<template>

  <h1>ZWQ-INT</h1>

  <form @submit.prevent="submitForm">

    <div class="form-group">

      <label class="form-label" for="username">用户名:</label>

      <input

        class="form-input"

        type="text"

        id="username"

        v-model="form.username"

        required

      />

    </div>

    <div class="form-group">

      <label class="form-label" for="password">密码:</label>

      <input

        class="form-input"

        type="password"

        id="password"

        v-model="form.password"

        required

      />

    </div>

    <button class="form-button" type="submit">登录</button>

    <button class="form-button" type="reset" @click="resetForm">重置</button>

  </form>

</template>

<script>

import { reactive } from "vue";

import axios from "axios";

import { useRouter } from "vue-router";

export default {

  setup() {

    const router = useRouter();

    const form = reactive({

      username: "",

      password: "",

      //http://localhost:8082/zwq/getIsLogin

    });

    //后端接口地址

    const backendUrl = "http://localhost:8082/zwq";

    //点击登录

    const submitForm = async () => {

      if (form.username.trim() && form.password.trim()) {

        try {

          // 注意:GET请求通常通过查询参数传递数据,而不是请求体

          // 所以我们将参数附加到URL上

          // const response = await axios.get(

          //   `${backendUrl}/getIsLogin?username=${form.username}&password=${form.password}`

          // );

          // console.log(response);

          //获取token

          const response = await axios.post(`${backendUrl}/login`, {

            username: form.username.trim(),

            password: form.password.trim(),

          });

          console.log(response);

          // 假设后端返回了一个包含登录状态的对象,例如 { isLogin: true }

          //获取token

          if (response.data != null && response.data != "") {

            localStorage.setItem("access-admin", response.data);

            alert("登陆成功");

            await router.push("/index");

          } else {

            alert("登陆失败");

          }

          //获取用户信息

        } catch (error) {

          alert("请求失败");

        }

      }

    };

    //点击重置

    const resetForm = () => {

      debugger;

      form.password = "";

      form.username = "";

    };

    return {

      form,

      submitForm,

      resetForm,

    };

  },

};

</script>

<style scoped>

.form-group {

  display: flex;

  align-items: center; /* 垂直居中 */

  margin-bottom: 10px; /* 每个表单项之间的间距 */

}

.form-label {

  flex: 1; /* 占据剩余空间 */

  text-align: right; /* 文字右对齐 */

  padding-right: 10px; /* 与输入框之间的间距 */

}

.form-input {

  flex: content; /* 占据更多空间 */

  max-width: 10%;

  margin-right: 45%;

}

.form-button {

  margin-right: 1%;

  margin-top: 10px; /* 与上面表单项的间距 */

}

</style>

**登陆成功后,跳转主页面(为了测试token效果,和验证token的有效期,代码也是及其不规范的,大佬勿喷。┗( T﹏T )┛)

<template>

  <div v-if="userData">

    欢迎回来:{{ userData.username }}

    <h1>这是首页</h1>

  </div>

  <div v-else>加载中...</div>

</template>

<script>

import axios from "axios";

import { ref, onMounted } from "vue";

import { useRouter } from "vue-router";

export default {

  setup() {

    const router = useRouter();

    //后端接口地址

    const backendUrl = "http://localhost:8082/zwq";

    const userData = ref(null);

    //钩子

    onMounted(async () => {

      try {

        const token = localStorage.getItem("access-admin");

        if (!token) {

          // 处理没有 token 的情况

          return;

        }

        const response = await axios({

          method: "post",

          url: `${backendUrl}/getUserData`,

          headers: {

            Authorization: `Bearer ${token}`,

          },

        });

        userData.value = response.data; // 更新响应式引用的值

        if (response.data === "") {

          alert("token验证失败,请重新登录!");

          await router.push("/");

        }

      } catch (error) {

        console.error("Error fetching user data:", error);

        // 处理错误,比如显示错误消息或重定向到登录页面

      }

    });

    return {

      userData,

    };

  },

};

</script>

Demo 效果截图:

登录成功:

登陆成功,跳转主页面,可以看到, 请求头传了token

token过期,返回登陆页面,重新登录获取token,达到token验证的效果。

大体的流程就这样啦,JWT的token自动续期不如session那样方便,session有参数配置,JWT的token的自动续期,要通过双令牌,验证令牌,刷新令牌实现自动续期,具体下期再聊啦!

小白自学上路,有不对的地方欢迎指正,也在学习,只要每天学一点,我们就在进步喔,不做摆烂人✊。

  • 37
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值