XSS(Cross-site scripting)跨站脚本攻击是一种常见的Web攻击,攻击者往Web页面里插入恶意的脚本代码,使得用户在浏览页面或提交表单的时候,执行恶意脚本,从而达到攻击目的。为了防止XSS攻击,Springboot提供了多种方式去过滤用户输入的数据,保证系统的安全稳定。
一、 Springboot过滤器 1、过滤器介绍 过滤器是用于拦截请求与响应,可以用于URL、Servlet、JSP、静态文件等资源的拦截。Springboot的过滤器实现了javax.servlet.Filter接口,提供了过滤器的常见功能,是一种非常常用的过滤方式。
2、过滤器示例 在Springboot中,过滤器是通过@Bean把过滤器加入到过滤器链中的,可以通过注解@WebFilter实现。以下示例会构建一个过滤器,对所有请求进行XSS过滤:
@WebFilter(urlPatterns = "/*", filterName = "xssFilter")
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
}
@Override
public void destroy() { }
}
在过滤器中,我们通过构建XssHttpServletRequestWrapper来过滤请求。这个Wrapper里重写了getParameter方法,对请求中的参数进行了过滤。XssHttpServletRequestWrapper的实现如下:
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value == null) {
return null;
}
return HtmlUtils.htmlEscape(value);
}
}
在getParameter方法中,我们使用了org.springframework.web.util.HtmlUtils的htmlEscape方法对请求参数进行了过滤,把所有的HTML标签都转换成了实体,防止了XSS攻击。
二、 Springboot注解 1、注解介绍 Springboot通过注解的方式来处理XSS过滤,可以直接在Controller层的方法参数上使用@Validated注解,在参数中使用@NotBlank或其他JSR303注解,并配合@RequestBody、@RequestParam可以过滤掉所有HTML标签,非常方便易用。
2、注解示例 以下例子演示了如何使用@RequestBody和@RequestParam结合JSR303注解来实现对控制器请求参数的过滤:
@RestController
public class UserController {
@PostMapping("/user")
public void addUser(@Validated @RequestBody UserDTO userDTO) {
// Todo
}
@GetMapping("/users")
public List getUsers(@RequestParam @NotBlank String name) {
// Todo
}
}
在addUser方法中,我们使用了@Validated注解,同时在UserDTO的name字段上使用了@NotBlank注解,来实现对请求参数的过滤。在getUsers方法中,我们也显示的用@RequestParam声明了参数name,并使用了@NotBlank注解来过滤请求。
三、Springboot过滤器加上自定义注解 1、自定义注解介绍 自定义注解可以让我们更好的管理XSS过滤,使得代码更加可读、易用、易维护,可以在Springboot过滤器的基础上添加自定义注解,来实现更加灵活的过滤。
2、自定义注解示例 以下代码演示了如何在过滤器中添加自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface XssSecurity {
String[] excludes() default {};
}
@Aspect
@Component
public class XssSecurityAspect {
private static final Logger logger = LoggerFactory.getLogger(XssSecurityAspect.class);
@Pointcut("@annotation(cn.example.demo.annotation.XssSecurity)")
public void xssPointCut() {
}
@Before("xssPointCut() && @annotation(xssSecurity)")
public void doBefore(JoinPoint joinPoint, XssSecurity xssSecurity) throws Throwable {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (servletRequestAttributes == null) {
return;
}
HttpServletRequest request = servletRequestAttributes.getRequest();
if (checkUrlExclude(request.getRequestURI(), xssSecurity.excludes())) {
return;
}
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request);
request.setAttribute("body", xssRequest.getBody());
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof XssHttpServletRequestWrapper || arg instanceof MultipartFile) {
continue;
}
Object o = xssSecurity(xssRequest, arg);
Method method = reflectMethod(joinPoint, arg);
if (null != method) {
method.invoke(joinPoint.getTarget(), o);
}
}
}
public Object xssSecurity(HttpServletRequest request, Object param) {
if (param == null) {
return null;
}
if (param instanceof String) {
return HtmlUtils.htmlEscape((String) param);
} else if (param instanceof Map) {
Map paramMap = new HashMap<>();
Map objectMap = (Map) param;
for (Map.Entry entry : objectMap.entrySet()) {
paramMap.put(entry.getKey(), (!isSkip(entry.getKey().toString())) ? xssSecurity(request, entry.getValue()) : entry.getValue());
}
return paramMap;
} else if (param instanceof Object[]) {
Object[] paramObj = (Object[]) param;
for (Object object : paramObj) {
if (object instanceof XssHttpServletRequestWrapper || object instanceof MultipartFile) {
continue;
}
xssSecurity(request, object);
}
return paramObj;
} else if (param instanceof Collection) {
Collection objs = (Collection) param;
for (Object obj : objs) {
if (obj instanceof XssHttpServletRequestWrapper || obj instanceof MultipartFile) {
continue;
}
xssSecurity(request, obj);
}
return objs;
} else if (param.getClass().isArray()) {
int len = Array.getLength(param);
for (int i = 0; i < len; ++i) {
Object object = Array.get(param, i);
if (object instanceof XssHttpServletRequestWrapper || object instanceof MultipartFile) {
continue;
}
xssSecurity(request, object);
}
return param;
}
return param;
}
private boolean isSkip(String key) {
boolean flag = false;
String param = null;
if (StringUtils.isNotEmpty(key)) {
param = key.toLowerCase();
}
for (String exclude : XSSConstant.exclude) {
if (exclude.equals(param)) {
flag = true;
break;
}
}
return flag;
}
private Method reflectMethod(JoinPoint joinPoint, Object arg) {
Method m = null;
Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
String methodName = joinPoint.getSignature().getName();
if (null != arg) {
for (Method method : methods) {
if (method.getName().equals(methodName)) {
if (method.getParameterTypes() != null && method.getParameterTypes().length == 1
&& method.getParameterTypes()[0].isAssignableFrom(arg.getClass())) {
m = method;
break;
}
}
}
}
return m;
}
}
@XssSecurity
@PostMapping("/user")
public void addUser(@RequestBody UserDTO userDTO) {
// Todo
}
在这个示例中,我们使用了AOP的方式来处理XSS过滤,通过自定义注解@XssSecurity来实现。在XssSecurityAspect的实现中,我们通过@Pointcut("@annotation(cn.example.demo.annotation.XssSecurity)")注解来定义Pointcut。在doBefore方法中,主要是通过反射的方式,对请求进行过滤。我们通过checkUrlExclude方法来判断当前请求是否在排除列表中,如果在就不进行过滤。
四、Spring-boot-starter-validation 1、Spring-boot-starter-validation介绍 Spring-boot-starter-validation是Springboot提供的JSR303验证依赖库,可以非常方便的对请求数据进行校验,包括数据类型、参数是否为空、参数是否在指定的范围内等等。使用起来非常方便简单。
2、Spring-boot-starter-validation示例 以下代码演示了如何使用Spring-boot-starter-validation的@NotBlank注解进行参数的非空判断:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String name;
}
@RestController
public class UserController {
@PostMapping("/user")
public void addUser(@Validated @RequestBody UserDTO userDTO) {
// Todo
}
}
在这个示例中,我们使用了@NotBlank(message = "用户名不能为空")注解对UserDTO中的name字段进行了非空判断。如果请求中的name为空,Springboot会返回错误信息"用户名不能为空",并返回400响应码。
五、Spring Security过滤器链 1、Spring Security介绍 Spring Security是Spring官方的安全框架,提供了一系列的安全服务,包括权限认证、资源保护等,Spring Security内置了很多过滤器,可以通过配置来加入过滤器链进行XSS过滤。
2、Spring Security示例 以下代码演示了如何在Spring Security配置中添加XSS过滤器来保护应用:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public FilterRegistrationBean xssFilterRegistrationBean() {
FilterRegistrationBean
registrationBean = new FilterRegistrationBean
();
registrationBean.setFilter(new XssFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setName("xssFilter");
return registrationBean;
}
}
我们在SecurityConfig中通过@Bean注解,加入了一个名为xssFilter的过滤器,把这个过滤器添加到了Spring Security的过滤器链中。这个过滤器实现方式与第一部分中的过滤器是一样的,都是通过XssHttpServletRequestWrapper来过滤请求。
六、小结 通过以上介绍,我们可以看出Springboot提供了多种方式进行XSS过滤,包括过滤器、注解、自定义注解和结合Spring Security等。不同的过滤方式有各自的优缺点和使用场景,开发人员可以结合具体需求来选择最合适的方式进行过滤。XSS攻击是一种非常常见且危险的攻击方式,合理的XSS过滤能够保障系统的安全稳定。