Spring Boot3 中集成 Spring Security + jwt

前言

来啦老铁!

最近在练习搭建一个前后端,主要用于开发一些日常用的小工具,其中后端用的 Spring Boot3,鉴权方面由于之前已经学习过单纯用 Spring Security 的模式了,这次改用 Spring Security + jwt(json web token),特此记录一下学习过程~

学习路径

  1. 添加依赖;
  2. 添加 jwt 配置;
  3. 编写 jwt 生成类;
  4. 编写 JwtAuthenticationFilter 过滤器类;
  5. 自定义统一的错误处理类;
  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 文件内添加 jwt 配置,值自定;

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

3. 编写 jwt 生成类;

(没有对 token 校验做很充足的校验,有需要,请自行优化哈)

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 {
    // 假设你有一个工具类来处理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请求头中的认证令牌
        String token = request.getHeader("Authorization");
        boolean isTokenValid = jwtTokenProvider.validateToken(token);
        // 当请求中没有 token header 或者 token无效,则认证失败
        if (null == token || token.isEmpty() || !isTokenValid) {
            chain.doFilter(request, response);
            return;
        }
        Claims claims = jwtTokenProvider.getClaimsFromToken(token);
        // 当没找到 token,则认证失败
        if (null == claims) {
            chain.doFilter(request, response);
            return;
        }
        String username = claims.getSubject();
        // 当 token 没绑定用户,则认证失败
        if (null == username || username.isEmpty()) {
            chain.doFilter(request, response);
            return;
        }
        // 认证成功
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }
}

5. 自定义统一的错误处理类;

默认情况下,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\":\"您没有访问权限,需要进行身份认证\"}");
    }
}
  • 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\":\"您无权访问此资源\"}");
    }
}

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:提供查询用户功能,如根据用户名查询用户,并返回UserDetails
     * UserDetails,SpringSecurity定义的类, 记录用户信息,如用户名、密码、权限等
     * */
    // @Autowired
    // private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名从数据库中查询用户
        //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);
    }
}

注:

  • 该类用于登录时校验用户名、密码;
  • 我们在这个地方需要从数据库等地方查询出是否有登录的账号,本例就没对接数据库了,写死一个在 loadUserByUsername 方法内了;

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 {
        // 关闭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安全框架中,提供了若干密码解析器实现类型。
     * 其中BCryptPasswordEncoder 叫强散列加密。可以保证相同的明文,多次加密后,
     * 密码有相同的散列数据,而不是相同的结果。
     * 匹配时,是基于相同的散列数据做的匹配。
     * 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 设置密码的加密方式为 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 = "用户相关接口")
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;

    // 登录接口的具体实现
    public String login(SysUser sysUser) {
        String username = sysUser.getUsername();
        // 传入用户名和密码
        UsernamePasswordAuthenticationToken usernamePassword =
                new UsernamePasswordAuthenticationToken(username, sysUser.getPassword());
        // 是实现登录逻辑,此时就会去调用LoadUserByUsername方法
        Authentication authenticate = authenticationManager.authenticate(usernamePassword);
        // 获取返回的用户信息
        Object principal = authenticate.getPrincipal();
        // 强转为MySysUserDetails类型
        MySysUserDetails mySysUserDetails = (MySysUserDetails) principal;
        // 输出用户信息
        System.err.println(mySysUserDetails);
        // 返回jwt
        return jwtTokenProvider.generateToken(username);
    }
}

当然,除了登录,我们还可以添加一个用于鉴权的接口,如:/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 = "鉴权接口")
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 中设置 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 设置到 sessionStorage 这一步很重要:

// 登录成功后在 sessionStorage 中设置 token
window.sessionStorage.setItem("token", res.token);

当然,我们也可以设置在别的地方,例如 cookie 等,也不一定要用 Window 对象哈,此处仅作简单演示~

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

// 创建 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 请根据实际情况自行修改
    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: "您没有访问权限",
                type: "error",
                duration: 5 * 1000,
            });
            window.location.href = "/#/login";
            return error.response.data;
        }
        ElMessage({
            message: `服务端错误:${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. 请求后的拦截主要是为了统一提示错误,如 401、403 等错误;

10. 功能演示;

  • 登录页面:
登录页面
  • 登录失败:
登录失败
  • 登录成功:
登录成功

彩蛋,拦截器;

拦截器可能很多人都知道了,笔者也知道,就是得备忘一下,例如我想为每个接口打印 url、参数等,我们可以这么做:

  • 编写 MyRequestWrapper;

由于 HttpServletRequest request 的 request.getReader() 只能被调用一次,否则会报错,因此,我们需要一个 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 {
    // 假设你有一个工具类来处理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请求头中的认证令牌
        String token = myRequestWrapper.getHeader("Authorization");
        boolean isTokenValid = jwtTokenProvider.validateToken(token);
        // 当请求中没有 token header 或者 token无效,则认证失败
        if (null == token || token.isEmpty() || !isTokenValid) {
            chain.doFilter(myRequestWrapper, response);
            return;
        }
        Claims claims = jwtTokenProvider.getClaimsFromToken(token);
        // 当没找到 token,则认证失败
        if (null == claims) {
            chain.doFilter(myRequestWrapper, response);
            return;
        }
        String username = claims.getSubject();
        // 当 token 没绑定用户,则认证失败
        if (null == username || username.isEmpty()) {
            chain.doFilter(myRequestWrapper, response);
            return;
        }
        // 认证成功
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(myRequestWrapper, response);
    }
}
  • 重点是这句:HttpServletRequest myRequestWrapper = new MyRequestWrapper(request);
  • 如此,每个请求就都会被打印出来了~
打印请求

当然,我们也可以使用拦截器这种做法,做其他事情~

好了,关于在 Spring Boot3 中集成 Spring Security + jwt,到此基本就结束了,能力有限,欢迎批评指正~

如果本文对您有帮助,麻烦点赞、关注!

谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342