前言
來啦老鐵削彬!
最近在練習(xí)搭建一個前后端霎匈,主要用于開發(fā)一些日常用的小工具斋配,其中后端用的 Spring Boot3硕舆,鑒權(quán)方面由于之前已經(jīng)學(xué)習(xí)過單純用 Spring Security 的模式了夸盟,這次改用 Spring Security + jwt(json web token)蛾方,特此記錄一下學(xué)習(xí)過程~
學(xué)習(xí)路徑
- 添加依賴;
- 添加 jwt 配置上陕;
- 編寫 jwt 生成類桩砰;
- 編寫 JwtAuthenticationFilter 過濾器類;
- 自定義統(tǒng)一的錯誤處理類释簿;
- 編寫 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 文件內(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)注监透!
謝謝!