天天看點

SpringBoot+Shiro+JWT+Redis+Mybatis-plus 前後端分離實戰項目前言JWT學習總結項目源碼(CodeChina平台)踩過的坑項目運作總結

SpringBoot+Shiro+JWT+Redis+Mybatis-plus 前後端分離實戰項目前言JWT學習總結項目源碼(CodeChina平台)踩過的坑項目運作總結

文章目錄

  • 前言
  • JWT學習總結
    • 什麼是JWT?
    • JWT的結構?
    • JWT整合SpringBoot的依賴
    • JWT核心代碼配置
      • JWTUtil
      • JWT攔截器
      • 全局攔截器配置
    • 登陸成功的時候生成JWT token 傳回給前端
    • 前端如何利用 JWT token
  • 項目源碼(CodeChina平台)
  • 踩過的坑
  • 項目運作
  • 總結

前言

這篇部落格是在我上篇發的 SpringBoot+Shiro+Redis+Mybatis-plus 實戰項目 之上添加了JWT認證和前後端分離,是以這篇部落格重點是貼出 JWT 學習總結的代碼,希望可以幫助到大家!

SpringBoot+Shiro+JWT+Redis+Mybatis-plus 前後端分離實戰項目前言JWT學習總結項目源碼(CodeChina平台)踩過的坑項目運作總結

JWT學習總結

什麼是JWT?

JWT 全稱就是 JSON WEB TOKEN,可以看作是一個獲得請求資格的令牌,我們有了這個令牌,才可以通路到網站的大部分功能(接口)。

JWT的結構?

JWT 分成三段

  1. header

    header 裡面主要是放 加密的算法名和類型

  2. payload(負載)

    payload 主要是放一些我們想傳遞給 token 中儲存的使用者部分資訊字段,比如 使用者名 使用者ID等

  3. sign(核心安全資訊)

    sign 是JWT 的核心安全資訊,它其實就是一個字元串,在公司中這個字元串一定不能被暴露出去。

以 . 号連接配接,有點像 IP 位址的格式

例如: header.payload.sign

SpringBoot+Shiro+JWT+Redis+Mybatis-plus 前後端分離實戰項目前言JWT學習總結項目源碼(CodeChina平台)踩過的坑項目運作總結

JWT整合SpringBoot的依賴

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

           

JWT核心代碼配置

JWTUtil

package com.jmu.shiro_demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.security.Signature;
import java.util.Calendar;
import java.util.Map;

public class JWTutil {

    private static final String SIGN = "jiachengren"; //JWT 簽名
    private static final int DEFAULT_JWT_EXPIRE_DAYS = 7; //預設JWT過期天數

    public static String getToken(Map<String,String> map) {
        JWTCreator.Builder builder = JWT.create();
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,DEFAULT_JWT_EXPIRE_DAYS);
        //1.header 預設
        //2.payload 周遊 map
       map.forEach((k,v) -> {
            builder.withClaim(k,v);
        });
        //3.設定過期時間和簽名後生成 token
        String token = builder
                .withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(SIGN));
        return token;
    }

    public static DecodedJWT verify(String token){
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    }

}

           

JWT攔截器

package com.jmu.shiro_demo.intercepetor;

import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jmu.shiro_demo.utils.JWTutil;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

public class JWTIntercepetor implements HandlerInterceptor {

    private final String TOKEN_NAME = "token";

    //JWT攔截器,所有請求都被這個攔截器攔截,校驗header中的token,token校驗通過再放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.token一般存放在 header 中,是以從 request.getHeader()中擷取 token
        String token = request.getHeader(TOKEN_NAME);
        Map<String,Object> map = new HashMap<String, Object>();
        try {
            JWTutil.verify(token);
            map.put("state",true);
            return true;
        } catch (AlgorithmMismatchException e) {
            map.put("msg","JWT算法不比對!");
        } catch (SignatureVerificationException e) {
            map.put("msg","JWT簽名不比對!");
        } catch (TokenExpiredException e) {
            map.put("msg","token(使用者資訊)已經過期,請重新登入!");
        } catch (Exception e) {
            map.put("msg","token無效!");
        }
        map.put("state",false);
        response.setContentType("application/json");
        String msg = new ObjectMapper().writeValueAsString(map);
        response.getWriter().println(msg);
        return false;
    }

}

           

全局攔截器配置

package com.jmu.shiro_demo.config;

import com.jmu.shiro_demo.intercepetor.JWTIntercepetor;
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 IntercepetorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTIntercepetor())
                .addPathPatterns("/**") //攔截所有請求
                .excludePathPatterns("/logout","/js/**","/index","/getAuthCode","/login","/toLogin","/user/**"); // 放行 登陸請求 下面的所有請求
    }

}

           

登陸成功的時候生成JWT token 傳回給前端

package com.jmu.shiro_demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jmu.shiro_demo.entity.Permission;
import com.jmu.shiro_demo.entity.Role;
import com.jmu.shiro_demo.entity.User;
import com.jmu.shiro_demo.mapper.UserMapper;
import com.jmu.shiro_demo.service.RoleService;
import com.jmu.shiro_demo.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jmu.shiro_demo.utils.JWTutil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * <p>
 *  服務實作類
 * </p>
 *
 * @author ${author}
 * @since 2021-04-20
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private RoleService roleService;

    @Override
    public String login(String username, String password, String code, HttpSession session, HttpServletResponse response, Model model) {
        //在校驗登陸之前先檢驗驗證碼的正确性
        String realAuthCode = (String) session.getAttribute("code");
        if(!realAuthCode.equalsIgnoreCase(code)) {
            model.addAttribute("msg","驗證碼錯誤!");
            return "login";
        }
        //1.擷取 Subject
        Subject subject = SecurityUtils.getSubject();
        //2.封裝 token
        UsernamePasswordToken shiroToken = new UsernamePasswordToken(username, password);
        //3.調用 subject.login(token) 方法
        try {
            subject.login(shiroToken);
            //登入成功之後傳回 token 給用戶端
            Map<String,String> map = new HashMap<String, String>();
            map.put("username",username);
            String token = JWTutil.getToken(map);  //登陸成功,生成JWT token
            response.setHeader("token",token); // 将 token 設定在 header 裡面
            model.addAttribute("token",token);
        }catch (UnknownAccountException e1) {
            //使用者名不存在
            model.addAttribute("msg","使用者名不存在!");
            return "/login";
        }catch (IncorrectCredentialsException e2) {
            model.addAttribute("msg","密碼錯誤!");
            return "/login";
        }
        return "/index";
    }

    @Override
    public User getUserByUserName(String username) {
        QueryWrapper<User> wrapper = new QueryWrapper<User>();
        wrapper.eq("username",username);
        return this.baseMapper.selectOne(wrapper);
    }

    @Override
    public List<Permission> getUserPermissionsByUserId(Integer userId) {
        List<Role> roles = getUserRoleByUserId(userId);
        List<Permission> permissions = new LinkedList<Permission>();
        roles.stream().forEach(role -> {
            permissions.addAll(roleService.getRolePermissionsByRoleId(role.getRoleid()));
        });
        return permissions;
    }

    @Override
    public List<Role> getUserRoleByUserId(Integer userId) {
        return this.baseMapper.getUserRoleByUserId(userId);
    }

    @Override
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "redirect:/index";
    }

}

           

前端如何利用 JWT token

這是 前後端分離的登陸界面, 登陸成功後在 success 裡面進行儲存 token 和跳轉頁面

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <script th:src="@{/js/jquery-3.6.0.slim.min.js}"></script>
</head>
<body>
<h3 style="color: red" th:text="${msg}"/>
<div>
    賬号: <input type="text" name="username" id="username"> <br/>
    密碼: <input type="password" name="password" id="password"> <br/>
    驗證碼: <input type="text" name="code" id="code"><img id="changeImg" src="/getAuthCode" alt=""><br/>
    <button id="loginBtn">登陸</button>
</div>
<script>

    $(function () {
        //點選更換驗證碼   拼接随機數
        $("#changeImg").click(function(){
            $("#changeImg").attr('src',"/getAuthCode?d="+Math.random());
        });
        $('#loginBtn').click(function () {
            let username = $('#username').val()
            let password = $('#password').val()
            let code = $('#code').val()
            $.ajax({
                url:"/user/login",
                data:{"username":username,"password":password,"code":code},
                method:"POST",
                success: function (data) {
                    window.localStorage.setItem("token",data.token) //将token設定在頁面的 localStorage 中
                    window.location = data.url //跳轉頁面
                    console.log(data)
                },
                error: function () {
                    alert("error!")
                }
            })
        })
    })
</script>
</body>
</html>

           

項目源碼(CodeChina平台)

shiro_demo

踩過的坑

  1. 前端報錯 $.ajax is not function

    解決:将壓縮版本的 jquery 更換成 非壓縮的

  2. jquery擷取不到 文本框中的值

    解決:使用 $(’#id’).val() 函數

  3. js被攔截

    解決:所有的攔截器的時候放行 /js/**

項目運作

這裡主要示範前後端分離下JWT的效果,shiro 和 redis 的效果在上篇實戰部落格中有發

示範說明,登陸的時候會儲存一個 jwt token 到 用戶端(浏覽器)的localstorage 中,然後每次發起一個背景請求的時候,在 請求頭(header)中就會攜帶這個 token, 背景如果校驗這個 token 通過,就響應請求,在這裡我的響應請求的方式,就是簡單的成功跳轉頁面。

SpringBoot+Shiro+JWT+Redis+Mybatis-plus 前後端分離實戰項目前言JWT學習總結項目源碼(CodeChina平台)踩過的坑項目運作總結

小彩蛋: 寫這篇部落格文章的時候,部落客是一邊聽趙雷(一個低調優秀的民謠歌手)的《鼓樓》這首民謠,真的好聽!推薦大家也去聽(不是廣告🤭)哈哈哈哈😄😄!

總結

JWT 是一種用于減緩認證處理業務過程中的繁瑣步驟,提高效率的技術,使用起來也不會很難,但是需要好好了解為什麼要用 JWT,為什麼會出現 token 令牌機制,最後留個問題給大家思考:JWTtoken需要儲存在資料庫中嗎?有啥想法,都可以在下面評論!希望學到的童鞋可以給部落客個三連!👍👍

堅持分享,堅持原創,喜歡部落客的靓仔靓女們可以看看部落客的首頁部落格!

您的點贊與收藏是我分享部落格的最大贊賞!

部落客部落格位址: https://blog.csdn.net/weixin_43967679

繼續閱讀