會話管理存在問題:
1.服務集群部署或者是分布式服務如何實現會話共享
2.會話的不同存儲地方的安全性問題
答:
會話共享 可以使用后端集中管理(redis)或者客戶端管理 (jwt);
存儲安全性 這個還真的沒有太好的方式,需要配合各種防護策略進行防,特別是基于cookie的前端管理,就算策略很難被攻破,但是有存在用戶禁用cookie無法完成會話正常傳遞問題;所以cookie這種方式就不考慮,基于localStorage雖然可以避免csrf的直接攻擊,但是又存在被XSS攻擊的可能,所以還要對入參進行檢驗,防腳本攻擊。
package com.example.security.filter;import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;import java.io.IOException;@Component
public class XssFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;XssHttpServletRequestWrapper wrappedRequest = new XssHttpServletRequestWrapper(httpRequest);chain.doFilter(wrappedRequest, response);}
}
?2. 創建 Request 包裝類用于轉義參數
package com.example.security.filter;import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import java.util.HashMap; import java.util.Map;public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {public XssHttpServletRequestWrapper(HttpServletRequest request) {super(request);}@Overridepublic String[] getParameterValues(String parameter) {String[] values = super.getParameterValues(parameter);if (values == null) return null;int count = values.length;String[] encodedValues = new String[count];for (int i = 0; i < count; i++) {encodedValues[i] = cleanXSS(values[i]);}return encodedValues;}@Overridepublic String getParameter(String parameter) {String value = super.getParameter(parameter);return value == null ? null : cleanXSS(value);}@Overridepublic Map<String, String[]> getParameterMap() {Map<String, String[]> map = new HashMap<>(super.getParameterMap());Map<String, String[]> cleanedMap = new HashMap<>();for (Map.Entry<String, String[]> entry : map.entrySet()) {String[] values = entry.getValue();if (values != null) {String[] cleanedValues = new String[values.length];for (int i = 0; i < values.length; i++) {cleanedValues[i] = cleanXSS(values[i]);}cleanedMap.put(entry.getKey(), cleanedValues);}}return cleanedMap;}private String cleanXSS(String value) {// 簡單的 XSS 轉義,也可以使用 OWASP 的 AntiSamy 或 Jsoupreturn value.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\$", "(").replaceAll("\$", ")").replaceAll("'", "'").replaceAll("\"", """);} }
3.注冊 Filter
package com.example.config;import com.example.security.filter.XssFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class WebConfig {@Beanpublic FilterRegistrationBean<XssFilter> xssFilterRegistration() {FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();// 創建過濾器實例registration.setFilter(new XssFilter());// 設置過濾路徑,/* 表示攔截所有請求registration.addUrlPatterns("/*");// 設置過濾器名稱registration.setName("XssFilter");// 設置加載順序,數字越小優先級越高registration.setOrder(1);return registration;}
}
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;public class CsrfFilter implements Filter {private String csrfToken;private Set<String> excludedPaths = new HashSet<>();@Overridepublic void init(FilterConfig filterConfig) throws ServletException {// 生成 TokencsrfToken = UUID.randomUUID().toString();// 從配置中讀取要排除的路徑String excludedUrls = filterConfig.getInitParameter("excludedUrls");if (excludedUrls != null && !excludedUrls.isEmpty()) {excludedPaths.addAll(Arrays.asList(excludedUrls.split(",")));}}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;HttpServletResponse httpResponse = (HttpServletResponse) response;String uri = httpRequest.getRequestURI();// 如果是免校驗路徑,直接放行if (isExcludedPath(uri)) {chain.doFilter(request, response);return;}// 只對敏感方法(POST/PUT/DELETE)做 CSRF 校驗if (isSensitiveMethod(httpRequest.getMethod())) {String clientToken = httpRequest.getHeader("X-CSRF-TOKEN");if (clientToken == null || !csrfToken.equals(clientToken)) {httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid CSRF Token");return;}}// 返回當前 token 給前端(可選)httpResponse.setHeader("X-CSRF-TOKEN", csrfToken);chain.doFilter(request, response);}private boolean isExcludedPath(String uri) {return excludedPaths.stream().anyMatch(uri::startsWith);}private boolean isSensitiveMethod(String method) {return "POST".equalsIgnoreCase(method) ||"PUT".equalsIgnoreCase(method) ||"DELETE".equalsIgnoreCase(method);}}
?注冊 CsrfFilter
@Bean
public FilterRegistrationBean<CsrfFilter> csrfFilterRegistration() {FilterRegistrationBean<CsrfFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new CsrfFilter());registration.addUrlPatterns("/*");registration.addInitParameter("excludedUrls", "/login,/register");registration.setName("CsrfFilter");registration.setOrder(1);return registration;
}
在登錄成功后生成并設置 Token
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpSession session) {String csrfToken = UUID.randomUUID().toString();session.setAttribute("X-CSRF-TOKEN", csrfToken);return ResponseEntity.ok().header("X-CSRF-TOKEN", csrfToken).build();
}
3. 前端處理
為了讓 CSRF Token 被正確發送,前端需要從響應頭中提取 X-CSRF-TOKEN 并在后續的 POST 請求中將其作為頭部信息發送回去。