Spring Boot3 中集成 Spring Security + jwt

前言

來啦老鐵削彬!

最近在練習(xí)搭建一個前后端霎匈,主要用于開發(fā)一些日常用的小工具斋配,其中后端用的 Spring Boot3硕舆,鑒權(quán)方面由于之前已經(jīng)學(xué)習(xí)過單純用 Spring Security 的模式了夸盟,這次改用 Spring Security + jwt(json web token)蛾方,特此記錄一下學(xué)習(xí)過程~

學(xué)習(xí)路徑

  1. 添加依賴;
  2. 添加 jwt 配置上陕;
  3. 編寫 jwt 生成類桩砰;
  4. 編寫 JwtAuthenticationFilter 過濾器類;
  5. 自定義統(tǒng)一的錯誤處理類释簿;
  6. 編寫 SecurityConfig 配置類亚隅;
  7. 編寫 MyUserDetailsService 類;
  8. 編寫登錄 controller庶溶、service煮纵;
  9. 前端部分懂鸵;
  10. 功能演示;
  11. 彩蛋行疏,攔截器匆光;

1. 添加依賴;

在后端項目 pom.xml 下增加如下配置酿联,并加載 maven 依賴终息;

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <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.1</version>
        </dependency>

2. 添加 jwt 配置;

在 application.yml 文件內(nèi)添加 jwt 配置贞让,值自定周崭;

jwt:
  secret: abc-123
  expiration: 86400000 # 1天,單位:毫秒

3. 編寫 jwt 生成類震桶;

(沒有對 token 校驗做很充足的校驗休傍,有需要征绎,請自行優(yōu)化哈)

package priv.dylan.space.util;

import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtTokenProvider {
    private String secret;
    private long expiration;

    /**
     * 生成 token
     * */
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 判斷 token 是否有效
     * */
    public boolean validateToken(String token) {
        try {
            return !isTokenExpired(token);
        } catch (ExpiredJwtException | IllegalArgumentException e) {
            return false;
        }
    }

    /**
     *  判斷 token 是否過期
     * */
    public boolean isTokenExpired(String token) {
        try {
            Date expiration = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody()
                    .getExpiration();
            return expiration.before(new Date());
        } catch (SignatureException e) {
            return true;
        }
    }

   /**
    * 從 token 中獲取 claims
    * */
    public Claims getClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }
}

4. 編寫 JwtAuthenticationFilter 過濾器類蹲姐;

這個類很重要,我們對所有請求人柿,拿其 header 中的 Authorization 字段值進行 token 的校驗柴墩。

package priv.dylan.space.util;

import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    // 假設(shè)你有一個工具類來處理JWT的驗證和解析
    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        // 獲取HTTP請求頭中的認(rèn)證令牌
        String token = request.getHeader("Authorization");
        boolean isTokenValid = jwtTokenProvider.validateToken(token);
        // 當(dāng)請求中沒有 token header 或者 token無效,則認(rèn)證失敗
        if (null == token || token.isEmpty() || !isTokenValid) {
            chain.doFilter(request, response);
            return;
        }
        Claims claims = jwtTokenProvider.getClaimsFromToken(token);
        // 當(dāng)沒找到 token凫岖,則認(rèn)證失敗
        if (null == claims) {
            chain.doFilter(request, response);
            return;
        }
        String username = claims.getSubject();
        // 當(dāng) token 沒綁定用戶江咳,則認(rèn)證失敗
        if (null == username || username.isEmpty()) {
            chain.doFilter(request, response);
            return;
        }
        // 認(rèn)證成功
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }
}

5. 自定義統(tǒng)一的錯誤處理類;

默認(rèn)情況下哥放,401 和 403 是沒有 response body 的歼指,我們可以挨個接口處理,但這樣很麻煩甥雕,更好的做法是踩身,我們自定義 401 和 403 的錯誤處理類,然后在 spring security 的 SecurityConfig 配置類中社露,添加我們聲明的自定義的 401 和 403 的錯誤處理類挟阻,這樣所有的 401 和 403 的錯誤,都能自動返回我們自定義的錯誤了峭弟,不用每個接口都人為介入處理附鸽。

自定義錯誤處理類如下:

  • 401
package priv.dylan.space.util;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import priv.dylan.space.constant.Constants;

import java.io.IOException;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"status\":\"" + Constants.STATUS_FAIL + "\",\"error\":\"Unauthorized\",\"message\":\"您沒有訪問權(quán)限,需要進行身份認(rèn)證\"}");
    }
}
  • 403
package priv.dylan.space.util;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import priv.dylan.space.constant.Constants;

import java.io.IOException;

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"status\":\"" + Constants.STATUS_FAIL + "\",\"error\":\"Forbidden\",\"message\":\"您無權(quán)訪問此資源\"}");
    }
}

6. 編寫 MyUserDetailsService 類;

package priv.dylan.space.service;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import priv.dylan.space.domain.MySysUserDetails;
import priv.dylan.space.domain.SysUser;

import java.util.Objects;

@Component
public class MyUserDetailsService implements UserDetailsService {
    /*
     * UserDetailsService:提供查詢用戶功能瞒瘸,如根據(jù)用戶名查詢用戶坷备,并返回UserDetails
     * UserDetails,SpringSecurity定義的類情臭, 記錄用戶信息击你,如用戶名玉组、密碼、權(quán)限等
     * */
    // @Autowired
    // private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根據(jù)用戶名從數(shù)據(jù)庫中查詢用戶
        //SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
        //                .eq(username != null, SysUser::getUsername, username));
        SysUser sysUser = new SysUser();
        sysUser.setId(1);
        sysUser.setUsername("dylanz");
        sysUser.setPassword("$2a$10$zXvx0oV2xxxxxxxxLrcdysrUFoBR6DnjWzIp322KEx/bLa");
        /* if (sysUser == null) {
            throw new UsernameNotFoundException("用戶不存在");
        }*/
        if (!Objects.equals(sysUser.getUsername(), username)) {
            throw new UsernameNotFoundException("用戶不存在");
        }
        return new MySysUserDetails(sysUser);
    }
}

注:

  • 該類用于登錄時校驗用戶名丁侄、密碼惯雳;
  • 我們在這個地方需要從數(shù)據(jù)庫等地方查詢出是否有登錄的賬號,本例就沒對接數(shù)據(jù)庫了鸿摇,寫死一個在 loadUserByUsername 方法內(nèi)了石景;

7. 編寫 SecurityConfig 配置類;

package priv.dylan.space.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import priv.dylan.space.service.MyUserDetailsService;
import priv.dylan.space.util.CustomAccessDeniedHandler;
import priv.dylan.space.util.CustomAuthenticationEntryPoint;
import priv.dylan.space.util.JwtAuthenticationFilter;
import priv.dylan.space.util.JwtTokenProvider;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 關(guān)閉csrf
        httpSecurity.csrf(AbstractHttpConfigurer::disable);
        httpSecurity.authorizeHttpRequests(it -> it.requestMatchers("/login", "/user/login", "/user/logout").permitAll()
                .anyRequest().authenticated()).exceptionHandling(exceptions -> exceptions.accessDeniedHandler(new CustomAccessDeniedHandler())
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
        );
        httpSecurity.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //將編寫的UserDetailsService注入進來
        provider.setUserDetailsService(myUserDetailsService);
        //將使用的密碼編譯器加入進來
        provider.setPasswordEncoder(passwordEncoder);
        //將provider放置到AuthenticationManager 中
        return new ProviderManager(provider);
    }

    /*
     * 在security安全框架中拙吉,提供了若干密碼解析器實現(xiàn)類型潮孽。
     * 其中BCryptPasswordEncoder 叫強散列加密】昵可以保證相同的明文往史,多次加密后,
     * 密碼有相同的散列數(shù)據(jù)佛舱,而不是相同的結(jié)果椎例。
     * 匹配時,是基于相同的散列數(shù)據(jù)做的匹配请祖。
     * Spring Security 推薦使用 BCryptPasswordEncoder 作為密碼加密和解析器订歪。
     * */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

注:

  • 這個配置類非常重要;
  • 我們在 securityFilterChain 方法中聲明了不需要校驗的接口肆捕,例如:"/login", "/user/login", "/user/logout"
  • 我們在 securityFilterChain 方法中添加了2個自定義的錯誤處理類刷晋,分別處理401和403錯誤;
  • 我們還在 securityFilterChain 方法中添加了 JwtAuthenticationFilter慎陵,將 token 管理過濾器引入眼虱,這樣就能利用 JwtAuthenticationFilter 進行接口校驗了;
  • 同時席纽,我們還需要在 SecurityConfig 中編寫 authenticationManager 和 passwordEncoder 方法捏悬,在 authenticationManager 設(shè)置密碼的加密方式為 passwordEncoder 提供的 BCryptPasswordEncoder 方式;

8. 編寫登錄 controller胆筒、service邮破;

  • controller:
package priv.dylan.space.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import priv.dylan.space.constant.Constants;
import priv.dylan.space.domain.LoginResponse;
import priv.dylan.space.domain.SysUser;
import priv.dylan.space.service.SysUserService;

@RestController
@RequestMapping("user")
@ResponseBody
@Slf4j
@Tag(name = "用戶相關(guān)接口")
public class UserController {
    @Autowired
    private SysUserService sysUserService;

    @PostMapping("/login")
    @Operation(summary = "登錄")
    public LoginResponse login(@RequestBody SysUser sysUser) {
        LoginResponse loginResponse = new LoginResponse();
        loginResponse.setToken(sysUserService.login(sysUser));
        loginResponse.setStatus(Constants.STATUS_SUCCESS);
        loginResponse.setMessage(Constants.LOGIN_SUCCESS_MESSAGE);
        return loginResponse;
    }
}
  • service:
package priv.dylan.space.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import priv.dylan.space.domain.MySysUserDetails;
import priv.dylan.space.domain.SysUser;
import priv.dylan.space.util.JwtTokenProvider;

@Service
public class SysUserService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    // 登錄接口的具體實現(xiàn)
    public String login(SysUser sysUser) {
        String username = sysUser.getUsername();
        // 傳入用戶名和密碼
        UsernamePasswordAuthenticationToken usernamePassword =
                new UsernamePasswordAuthenticationToken(username, sysUser.getPassword());
        // 是實現(xiàn)登錄邏輯,此時就會去調(diào)用LoadUserByUsername方法
        Authentication authenticate = authenticationManager.authenticate(usernamePassword);
        // 獲取返回的用戶信息
        Object principal = authenticate.getPrincipal();
        // 強轉(zhuǎn)為MySysUserDetails類型
        MySysUserDetails mySysUserDetails = (MySysUserDetails) principal;
        // 輸出用戶信息
        System.err.println(mySysUserDetails);
        // 返回jwt
        return jwtTokenProvider.generateToken(username);
    }
}

當(dāng)然仆救,除了登錄抒和,我們還可以添加一個用于鑒權(quán)的接口,如:/auth

  • controller
package priv.dylan.space.controller;

import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import priv.dylan.space.constant.Constants;
import priv.dylan.space.domain.CommonResponse;
import priv.dylan.space.util.JwtTokenProvider;

@RestController
@RequestMapping("/auth")
@ResponseBody
@Slf4j
@Tag(name = "鑒權(quán)接口")
public class AuthController {
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @GetMapping("")
    public CommonResponse checkIfAuthorized(@RequestHeader(value = "Authorization", required = false) String jwt) {
        CommonResponse commonResponse = new CommonResponse();
        if (null == jwt || jwt.isEmpty()) {
            commonResponse.setStatus(Constants.STATUS_FAIL);
            commonResponse.setMessage(Constants.TOKEN_INVALID_MESSAGE);
            commonResponse.setError(Constants.TOKEN_EMPTY_ERROR);
            return commonResponse;
        }
        boolean isTokenExpired = jwtTokenProvider.isTokenExpired(jwt);
        if (isTokenExpired) {
            commonResponse.setStatus(Constants.STATUS_FAIL);
            commonResponse.setMessage(Constants.TOKEN_INVALID_MESSAGE);
            commonResponse.setError(Constants.TOKEN_EXPIRED_ERROR);
        } else {
            commonResponse.setStatus(Constants.STATUS_SUCCESS);
            commonResponse.setMessage(Constants.TOKEN_VALID_MESSAGE);
        }
        return commonResponse;
    }
}

9. 前端部分彤蔽;

  • 登錄頁面:
<template>
    <div align="center" class="login-container">
        <div style="margin-top: 100px">
            <h2 style="color: white">Sign in to SPACE</h2>
            <el-card style="width: 380px; height: 290px; padding: 10px">
                <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm">
                    <div align="left">
                        <span>Username</span>
                    </div>
                    <el-form-item prop="username">
                        <el-input v-model="ruleForm.username" autocomplete="off" clearable />
                    </el-form-item>

                    <div align="left">
                        <span>Password</span>
                    </div>

                    <el-form-item prop="password">
                        <el-input type="password" v-model="ruleForm.password" autocomplete="off" clearable />
                    </el-form-item>

                    <el-form-item>
                        <el-button type="primary" @click="login('ruleForm')" style="width: 100%">Sign in</el-button>
                    </el-form-item>
                </el-form>

                <el-row>
                    <el-col :span="12" align="left"><el-link href="/#/resetPassword" target="_blank">Forgot
                            password?</el-link>
                    </el-col>
                    <el-col :span="12" align="right">
                        <el-link href="/#/createAccount" target="_blank">Create an account.</el-link>
                    </el-col>
                </el-row>
            </el-card>
        </div>
    </div>
</template>
  
<script>
import { login } from "../api/user";
import { ElMessage } from "element-plus";

export default {
    name: "LoginPage",
    data() {
        var validateUsername = (rule, value, callback) => {
            if (value === "") {
                callback(new Error("Please input your username"));
            } else {
                callback();
            }
        };
        var validatePassword = (rule, value, callback) => {
            if (value === "") {
                callback(new Error("Please input your password"));
            } else {
                callback();
            }
        };
        return {
            ruleForm: {
                username: "",
                password: "",
            },
            rules: {
                username: [{ validator: validateUsername, trigger: "blur" }],
                password: [{ validator: validatePassword, trigger: "blur" }],
            },
        };
    },
    methods: {
        login(formName) {
            const from = this.$route.query.from || "home";
            this.$refs[formName].validate((valid) => {
                if (!valid) {
                    return false;
                }
                login(this.ruleForm.username, this.ruleForm.password).then((res) => {
                    this.ruleForm.username = "";
                    this.ruleForm.password = "";
                    if (res.status == 1) {
                        ElMessage({
                            message: res.message,
                            type: "success",
                            duration: 5 * 1000,
                        });
                        // 登錄成功后在 sessionStorage 中設(shè)置 token
                        window.sessionStorage.setItem("token", res.token);

                        this.$router.push({ path: `/${from}` });
                    }
                });
            });
        },
    },
};
</script>
  
<style rel="stylesheet/scss" lang="scss">
$bg: #2d3a4b;
$dark_gray: #889aa4;
$light_gray: #eee;

.login-container {
    position: fixed;
    height: 100%;
    width: 100%;
    background-color: $bg;

    input {
        background: transparent;
        border: 0px;
        -webkit-appearance: none;
        border-radius: 0px;
        padding: 12px 5px 12px 15px;
        height: 47px;
    }

    /* .el-input {
        height: 47px;
        width: 85%;
    } */

    .tips {
        font-size: 14px;
        color: #fff;
        margin-bottom: 10px;
    }

    .svg-container {
        padding: 6px 5px 6px 15px;
        color: $dark_gray;
        vertical-align: middle;
        width: 30px;
        display: inline-block;

        &_login {
            font-size: 20px;
        }
    }

    .title {
        font-size: 26px;
        font-weight: 400;
        color: $light_gray;
        margin: 0px auto 40px auto;
        text-align: center;
        font-weight: bold;
    }

    .login-form {
        position: absolute;
        left: 0;
        right: 0;
        width: 400px;
        padding: 35px 35px 15px 35px;
        margin: 120px auto;
    }

    .el-form-item {
        border: 1px solid rgba(255, 255, 255, 0.1);
        background: rgba(117, 137, 230, 0.1);
        border-radius: 5px;
        color: #454545;
    }

    .show-pwd {
        position: absolute;
        right: 10px;
        top: 7px;
        font-size: 16px;
        color: $dark_gray;
        cursor: pointer;
        user-select: none;
    }

    .thirdparty-button {
        position: absolute;
        right: 35px;
        bottom: 28px;
    }
}

.title-container {
    position: relative;

    .title {
        font-size: 26px;
        font-weight: 400;
        color: $light_gray;
        margin: 0px auto 40px auto;
        text-align: center;
        font-weight: bold;
    }

    .set-language {
        color: #fff;
        position: absolute;
        top: 5px;
        right: 0px;
    }
}
</style>

注意:
登錄成功后將 token 設(shè)置到 sessionStorage 這一步很重要:

// 登錄成功后在 sessionStorage 中設(shè)置 token
window.sessionStorage.setItem("token", res.token);

當(dāng)然摧莽,我們也可以設(shè)置在別的地方,例如 cookie 等顿痪,也不一定要用 Window 對象哈镊辕,此處僅作簡單演示~

  • request 中的攔截:
import axios from "axios";
import { ElMessage } from "element-plus";

// 創(chuàng)建 axios 實例
const service = axios.create({
    baseURL: "http://localhost:8080", // api的base_url
    timeout: 10000, // 請求超時時間
});

// request 攔截器
service.interceptors.request.use(config => {
    // 從 sessionStorage 中獲取 token
    const token = window.sessionStorage.getItem("token");
    // 讓每個請求攜帶自定義 token 請根據(jù)實際情況自行修改
    if (token) {
        config.headers['Authorization'] = token;
    }
    return config;
}, error => {
    // Do something with request error
    // console.log(error) // for debug
    Promise.reject(error);
});

// respone 攔截器
service.interceptors.response.use(
    (response: { data: any; }) => {
        return response.data;
    },
    error => {
        if (error.response && (error.response.status === 403 || error.response.status === 401)) {
            ElMessage({
                message: "您沒有訪問權(quán)限",
                type: "error",
                duration: 5 * 1000,
            });
            window.location.href = "/#/login";
            return error.response.data;
        }
        ElMessage({
            message: `服務(wù)端錯誤:${error.message}`,
            type: "error",
            duration: 5 * 1000,
        });
        window.location.href = "/#/login";
        return Promise.reject(error);
    }
);

export default service;

注意油够,此處有2個攔截,一個是請求前的攔截征懈,一個是請求后的攔截石咬,:
a. 請求前的攔截主要是自動將 sessionStorage 中的 token 以 header 的形式帶到每個請求上,header 名為 Authorization卖哎;
b. 請求后的攔截主要是為了統(tǒng)一提示錯誤鬼悠,如 401、403 等錯誤亏娜;

10. 功能演示焕窝;

  • 登錄頁面:
登錄頁面
  • 登錄失敗:
登錄失敗
  • 登錄成功:
登錄成功

彩蛋维贺,攔截器它掂;

攔截器可能很多人都知道了,筆者也知道溯泣,就是得備忘一下虐秋,例如我想為每個接口打印 url、參數(shù)等发乔,我們可以這么做:

  • 編寫 MyRequestWrapper熟妓;

由于 HttpServletRequest request 的 request.getReader() 只能被調(diào)用一次雪猪,否則會報錯栏尚,因此,我們需要一個 MyRequestWrapper 來緩存 request只恨;

package priv.dylan.space.util;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class MyRequestWrapper extends HttpServletRequestWrapper {
    private static final int BUFFER_SIZE = 1024 * 8;
    private final byte[] body;

    public MyRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        BufferedReader reader = request.getReader();
        try (StringWriter writer = new StringWriter()) {
            int read;
            char[] buf = new char[BUFFER_SIZE];
            while ((read = reader.read(buf)) != -1) {
                writer.write(buf, 0, read);
            }
            this.body = writer.getBuffer().toString().getBytes(StandardCharsets.UTF_8);
        }
    }

    public String getBody() {
        return new String(this.body, StandardCharsets.UTF_8);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }
}
  • 編寫攔截器译仗;
package priv.dylan.space.util;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;

@Component
public class LoggingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        System.out.println("===>" + new Date());
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("Request Method: " + request.getMethod());
        Map<String, String[]> parameters = request.getParameterMap();
        if (!parameters.isEmpty()) {
            System.out.println("Request Parameters: ");
            parameters.forEach((key, values) -> {
                System.out.println(key + ":" + Arrays.toString(values));
            });
        }
        // 如果請求類型為非 GET,打印請求體
        if (!"GET".equalsIgnoreCase(request.getMethod())) {
            String body = getRequestBody(request);
            System.out.println("Request Body: " + body);
        }
        return true;
    }

    private String getRequestBody(HttpServletRequest request) {
        try {
            MyRequestWrapper myRequestWrapper = new MyRequestWrapper(request);
            return myRequestWrapper.getBody();
        } catch (IOException e) {
            return "";
        }
    }
}
  • 在 WebMvcConfig 中使用攔截器:
package priv.dylan.space.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import priv.dylan.space.util.LoggingInterceptor;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoggingInterceptor loggingInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedOriginPatterns("*")
                .maxAge(3600);
    }

    /*@Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home.html").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello.html").setViewName("hello");
        registry.addViewController("/login.html").setViewName("login");
    }*/

    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor).addPathPatterns("/**");
    }
}
  • 同時官觅,JwtAuthenticationFilter 中需要使用緩存的 request 對象纵菌,
    JwtAuthenticationFilter 類改為:
package priv.dylan.space.util;

import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    // 假設(shè)你有一個工具類來處理JWT的驗證和解析
    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        HttpServletRequest myRequestWrapper = new MyRequestWrapper(request);
        // 獲取HTTP請求頭中的認(rèn)證令牌
        String token = myRequestWrapper.getHeader("Authorization");
        boolean isTokenValid = jwtTokenProvider.validateToken(token);
        // 當(dāng)請求中沒有 token header 或者 token無效,則認(rèn)證失敗
        if (null == token || token.isEmpty() || !isTokenValid) {
            chain.doFilter(myRequestWrapper, response);
            return;
        }
        Claims claims = jwtTokenProvider.getClaimsFromToken(token);
        // 當(dāng)沒找到 token休涤,則認(rèn)證失敗
        if (null == claims) {
            chain.doFilter(myRequestWrapper, response);
            return;
        }
        String username = claims.getSubject();
        // 當(dāng) token 沒綁定用戶咱圆,則認(rèn)證失敗
        if (null == username || username.isEmpty()) {
            chain.doFilter(myRequestWrapper, response);
            return;
        }
        // 認(rèn)證成功
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(myRequestWrapper, response);
    }
}
  • 重點是這句:HttpServletRequest myRequestWrapper = new MyRequestWrapper(request);
  • 如此,每個請求就都會被打印出來了~
打印請求

當(dāng)然功氨,我們也可以使用攔截器這種做法序苏,做其他事情~

好了,關(guān)于在 Spring Boot3 中集成 Spring Security + jwt捷凄,到此基本就結(jié)束了忱详,能力有限,歡迎批評指正~

如果本文對您有幫助跺涤,麻煩點贊匈睁、關(guān)注监透!

謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末航唆,一起剝皮案震驚了整個濱河市胀蛮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糯钙,老刑警劉巖醇滥,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異超营,居然都是意外死亡鸳玩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門演闭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來不跟,“玉大人,你說我怎么就攤上這事米碰∥迅铮” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵吕座,是天一觀的道長虐译。 經(jīng)常有香客問我,道長吴趴,這世上最難降的妖魔是什么漆诽? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮锣枝,結(jié)果婚禮上厢拭,老公的妹妹穿的比我還像新娘。我一直安慰自己撇叁,他們只是感情好供鸠,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陨闹,像睡著了一般楞捂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趋厉,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天寨闹,我揣著相機與錄音,去河邊找鬼觅廓。 笑死鼻忠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播帖蔓,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼矮瘟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了塑娇?” 一聲冷哼從身側(cè)響起澈侠,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎埋酬,沒想到半個月后哨啃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡写妥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年拳球,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片珍特。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡祝峻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扎筒,到底是詐尸還是另有隱情莱找,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布嗜桌,位于F島的核電站奥溺,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏骨宠。R本人自食惡果不足惜浮定,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诱篷。 院中可真熱鬧壶唤,春花似錦雳灵、人聲如沸棕所。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琳省。三九已至,卻和暖如春针贬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拢蛋。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工桦他, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谆棱。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓快压,卻偏偏與公主長得像圆仔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蔫劣,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355