前言
来啦老铁!
最近在练习搭建一个前后端,主要用于开发一些日常用的小工具,其中后端用的 Spring Boot3,鉴权方面由于之前已经学习过单纯用 Spring Security 的模式了,这次改用 Spring Security + jwt(json web token),特此记录一下学习过程~
学习路径
- 添加依赖;
- 添加 jwt 配置;
- 编写 jwt 生成类;
- 编写 JwtAuthenticationFilter 过滤器类;
- 自定义统一的错误处理类;
- 编写 SecurityConfig 配置类;
- 编写 MyUserDetailsService 类;
- 编写登录 controller、service;
- 前端部分;
- 功能演示;
- 彩蛋,拦截器;
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,到此基本就结束了,能力有限,欢迎批评指正~
如果本文对您有帮助,麻烦点赞、关注!
谢谢!