会话管理存在问题:
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 请求中将其作为头部信息发送回去。