bypass!bypass!bypass!
@Configuration
@EnableWebSecurity //启用Web安全功能
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests() // 开启 HttpSecurity 配置
.antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必须具备ADMIN角色
.antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 该模式需要ADMIN或USER角色
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色
.anyRequest().authenticated() // 用户访问其它URL都必须认证后访问(登录后访问)
.and().formLogin().loginProcessingUrl("/login").permitAll() // 开启表单登录并配置登录接口
.and().csrf().disable(); // 关闭csrf
return http.build();
}
方法 | 描述 |
---|---|
access(String) | 如果给定的SpEL表达式计算结果为true,就允许访问 |
anonymous() | 允许匿名用户访问 |
authenticated() | 允许认证过的用户访问 |
denyAll() | 无条件拒绝所有访问 |
fullyAuthenticated() | 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问 |
hasAnyAuthority(String…) | 如果用户具备给定权限中的某一个的话,就允许访问 |
hasAnyRole(String…) | 如果用户具备给定角色中的某一个的话,就允许访问 |
hasAuthority(String) | 如果用户具备给定权限的话,就允许访问 |
hasIpAddress(String) | 如果请求来自给定IP地址的话,就允许访问 |
hasRole(String) | 如果用户具备给定角色的话,就允许访问 |
not() | 对其他访问方法的结果求反 |
permitAll() | 无条件允许访问 |
rememberMe() | 如果用户是通过Remember-me功能认证的,就允许访问 |
也可以通过集成WebSecurityConfigurerAdapter
类的方式来configure()方法来制定Web安全的细节。
1、configure(WebSecurity):通过重载该方法,可配置Spring Security的Filter链。
2、configure(HttpSecurity):通过重载该方法,可配置如何通过拦截器保护请求。
Spring Security 支持的所有SpEL表达式如下:
安全表达式 | 计算结果 |
---|---|
authentication | 用户认证对象 |
denyAll | 结果始终为false |
hasAnyRole(list of roles) | 如果用户被授权指定的任意权限,结果为true |
hasRole(role) | 如果用户被授予了指定的权限,结果 为true |
hasIpAddress(IP Adress) | 用户地址 |
isAnonymous() | 是否为匿名用户 |
isAuthenticated() | 不是匿名用户 |
isFullyAuthenticated | 不是匿名也不是remember-me认证 |
isRemberMe() | remember-me认证 |
permitAll | 始终true |
principal | 用户主要信息对象 |
configure(AuthenticationManagerBuilder):通过重载该方法,可配置user-detail(用户详细信息)服务。
方法 | 描述 |
---|---|
accountExpired(boolean) | 定义账号是否已经过期 |
accountLocked(boolean) | 定义账号是否已经锁定 |
and() | 用来连接配置 |
authorities(GrantedAuthority…) | 授予某个用户一项或多项权限 |
authorities(List) | 授予某个用户一项或多项权限 |
authorities(String…) | 授予某个用户一项或多项权限 |
credentialsExpired(boolean) | 定义凭证是否已经过期 |
disabled(boolean) | 定义账号是否已被禁用 |
password(String) | 定义用户的密码 |
roles(String…) | 授予某个用户一项或多项角色 |
1、使用基于内存的用户存储:通过inMemoryAuthentication()方法,我们可以启用、配置并任意填充基于内存的用户存储。并且,我们可以调用withUser()方法为内存用户存储添加新的用户,这个方法的参数是username。withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder,这个对象提供了多个进一步配置用户的方法,包括设置用户密码的password()方法以及为给定用户授予一个或多个角色权限的roles()方法。需要注意的是,roles()方法是authorities()方法的简写形式。roles()方法所给定的值都会添加一个ROLE_前缀,并将其作为权限授予给用户。因此上诉代码用户具有的权限为:ROLE_USER,ROLE_ADMIN。而借助passwordEncoder()方法来指定一个密码转码器(encoder),我们可以对用户密码进行加密存储。
@Configuration
@EnableWebSecurity //启用Web安全功能
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests() // 开启 HttpSecurity 配置
.antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必须具备ADMIN角色
.antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 该模式需要ADMIN或USER角色
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色
.anyRequest().authenticated() // 用户访问其它URL都必须认证后访问(登录后访问)
.and().formLogin().loginProcessingUrl("/login").permitAll() // 开启表单登录并配置登录接口
.and().csrf().disable(); // 关闭csrf
return http.build();
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("root").password("123").roles("ADMIN","DBA")
.and()
.withUser("admin").password("123").roles("ADMIN","USER")
.and()
.withUser("xxx").password("123").roles("USER");
}
}
2、基于数据库表进行认证:用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。为了配置Spring Security使用以JDBC为支撑的用户存储,我们可以使用jdbcAuthentication()方法,并配置他的DataSource,这样的话,就能访问关系型数据库了。
3、基于LDAP进行认证:为了让Spring Security使用基于LDAP的认证,我们可以使用ldapAuthentication()方法。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 配置 user-detail 服务
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 基于 LDAP 配置认证
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
.passwordCompare()
.passwordAttribute("password")
.passwordEncoder(new BCryptPasswordEncoder());
}
}
使用远程ldap
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
// 返回一个ContextSourceBuilder 对象
.contextSource()
// 指定远程 LDAP 服务器 的 地址
.url("ldap://xxx.com:389/dc=xxx,dc=com");
}
}
ldapAuthentication():表示,基于LDAP的认证。
userSearchBase():为查找用户提供基础查询
userSearchFilter():提供过滤条件,用于搜索用户。
groupSearchBase():为查找组指定了基础查询。
groupSearchFilter():提供过滤条件,用于组。
passwordCompare():希望通过 密码比对 进行认证。
passwordAttribute():指定 密码 保存的属性名字,默认:userPassword。
passwordEncoder():指定密码转换器。
http.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("admin")
.antMatchers("/user/**").hasAuthority("user")
.anyRequest().authenticated()
和
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
实际上这两个的效果都是一样的
package person.xu.vulEnv;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/test").access("hasRole('ADMIN')")
.antMatchers("/**").permitAll();
//.antMatchers("/**").access("anonymous");
// @formatter:on
return http.build();
}
// @formatter:off
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
// @formatter:on
}
绕过:http://127.0.0.1:8012/test/
mvcMatchers("/test").access("hasRole('ADMIN')")
或者使用 antMatchers("/test/**").access("hasRole('ADMIN')")
写法防止认证绕过。
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.regexMatchers("/test").access("hasRole('ADMIN')")
.antMatchers("/**").access("anonymous");
// @formatter:on
return http.build();
}
http://127.0.0.1:8012/test?
、http://127.0.0.1:8012/test/
Matchers没使用类似/test.*
的方式,在传入/test?
时候,正则会匹配不上,不会命中/test
的规则。
安全写法
.regexMatchers("/test.*?").access("hasRole('ADMIN')")
低版本 的 spring-webmvc 及其相关组件,包括:
spring-webmvc <= 5.2.4.RELEASE
spring-framework <= 5.2.6.RELEASE
spring-boot-starter-parent <= 2.2.5.RELEASE
在代码中定义的 useSuffixPatternMatch
配置默认值为 true
,表示使用后缀匹配模式匹配路径。
如 /path/abc
路由也会允许 /path/abcd.ef
、/path/abcde.f
等增加 .xxx
后缀形式的路径匹配成功。
使用高版本的 spring-webmvc 能有效避免问题。
https://www.jianshu.com/p/e6655328b211
Spring Security 5.5.x < 5.5.7
Spring Security 5.6.x < 5.6.4
Spring在加载的时候会来到DelegatingFilterProxy
,DelegatingFilterProxy根据targetBeanName从Spring 容器中获取被注入到Spring 容器的Filter实现类,在DelegatingFilterProxy配置时一般需要配置属性targetBeanName。DelegatingFilterProxy就是一个对于servlet filter的代理,用这个类的好处主要是通过Spring容器来管理servlet filter的生命周期,
还有就是如果filter中需要一些Spring容器的实例,可以通过spring直接注入,
另外读取一些配置文件这些便利的操作都可以通过Spring来配置实现。
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
//当Filter配置时如果没有设置targentBeanName属性,则直接根据Filter名称来查找
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
//从Spring容器中获取注入的Filter的实现类
this.delegate = initDelegate(wac);
}
}
}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//从Spring 容器中获取注入的Filter的实现类
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
//当Filter配置时如果没有设置targentBeanName属性,则直接根据Filter名称来查找
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
//从Spring容器中获取注入的Filter的实现类
this.delegate = initDelegate(wac);
}
}
}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//从Spring 容器中获取注入的Filter的实现类
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
从Spring 容器中获取注入的Filter的实现类,然后调用org.springframework.web.filter.DelegatingFilterProxy#invokeDelegate
方法
来到org.springframework.security.web.FilterChainProxy#doFilterInternal
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
if (filters != null && filters.size() != 0) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> {
return "Securing " + requestLine(firewallRequest);
}));
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
} else {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> {
return "No security for " + requestLine(firewallRequest);
}));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
}
}
this.firewall
默认装载的是StrictHttpFirewall
,而不是DefaultHttpFirewall
。反而DefaultHttpFirewall
的校验没那么严格
FirewalledRequest
是封装后的请求类,但实际上该类只是在HttpServletRequestWrapper
的基础上增加了reset方法。当spring security过滤器链执行完毕时,由FilterChainProxy
负责调用该方法,以便重置全部或者部分属性。
FirewalledResponse
是封装后的响应类,该类主要重写了sendRedirect、setHeader、addHeader以及addCookie四个方法,在每一个方法中都对其参数进行校验,以确保参数中不含有\r和\n。
在FilterChainProxy属性定义中,默认创建的HttpFirewall实例就是StrictHttpFirewall。
FilterChainProxy是在WebSecurity#performBuild方法中构建的,而WebSecurity实现了ApplicationContextAware接口,并实现了接口中的setApplicationContext方法,在该方法中,从spring容器中查找到HttpFirewall对并赋值给httpFirewall属性。最终在performBuild方法中,将FilterChainProxy对象构建成功后,如果httpFirewall不为空,就把httpFirewall配置给FilterChainProxy对象。
因此,如果spring容器中存在HttpFirewall实例,则最终使用spring容器提供的实例;如果不存在,则使用FilterChainProxy中默认定义的StrictHttpFirewall。
org.springframework.security.web.firewall.StrictHttpFirewall#getFirewalledRequest
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
this.rejectForbiddenHttpMethod(request);
this.rejectedBlocklistedUrls(request);
this.rejectedUntrustedHosts(request);
if (!isNormalized(request)) {
throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
} else {
String requestUri = request.getRequestURI();
if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters.");
} else {
return new StrictFirewalledRequest(request);
}
}
}
方法会判断请求的方法是否是可允许的
org.springframework.security.web.firewall.StrictHttpFirewall#rejectForbiddenHttpMethod
private void rejectForbiddenHttpMethod(HttpServletRequest request) {
if (this.allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) {
if (!this.allowedHttpMethods.contains(request.getMethod())) {
throw new RequestRejectedException("The request was rejected because the HTTP method \"" + request.getMethod() + "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
}
}
}
private static Set<String> createDefaultAllowedHttpMethods() {
Set<String> result = new HashSet();
result.add(HttpMethod.DELETE.name());
result.add(HttpMethod.GET.name());
result.add(HttpMethod.HEAD.name());
result.add(HttpMethod.OPTIONS.name());
result.add(HttpMethod.PATCH.name());
result.add(HttpMethod.POST.name());
result.add(HttpMethod.PUT.name());
return result;
}
org.springframework.security.web.firewall.StrictHttpFirewall#rejectedBlocklistedUrls
private void rejectedBlocklistedUrls(HttpServletRequest request) {
Iterator var2 = this.encodedUrlBlocklist.iterator();
String forbidden;
do {
if (!var2.hasNext()) {
var2 = this.decodedUrlBlocklist.iterator();
do {
if (!var2.hasNext()) {
return;
}
forbidden = (String)var2.next();
} while(!decodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
forbidden = (String)var2.next();
} while(!encodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
encodedUrlBlocklist = {HashSet@7373} size = 18
0 = "//"
1 = ""
2 = "%2F%2f"
3 = "%2F%2F"
4 = "%00"
5 = "%25"
6 = "%2f%2f"
7 = "%2f%2F"
8 = "%5c"
9 = "%5C"
10 = "%3b"
11 = "%3B"
12 = "%2e"
13 = "%2E"
14 = "%2f"
15 = "%2F"
16 = ";"
17 = "\"
decodedUrlBlocklist = {HashSet@7374} size = 16
0 = "//"
1 = ""
2 = "%2F%2f"
3 = "%2F%2F"
4 = "%00"
5 = "%"
6 = "%2f%2f"
7 = "%2f%2F"
8 = "%5c"
9 = "%5C"
10 = "%3b"
11 = "%3B"
12 = "%2f"
13 = "%2F"
14 = ";"
15 = "\"
private static boolean encodedUrlContains(HttpServletRequest request, String value) {
return valueContains(request.getContextPath(), value) ? true : valueContains(request.getRequestURI(), value);
}
private static boolean decodedUrlContains(HttpServletRequest request, String value) {
if (valueContains(request.getServletPath(), value)) {
return true;
} else {
return valueContains(request.getPathInfo(), value);
}
}
private static boolean valueContains(String value, String contains) {
return value != null && value.contains(contains);
}
优先从request.getContextPath()
里面取值,如果存在黑名单,即返回flase抛异常。
org.springframework.security.web.firewall.StrictHttpFirewall#rejectedUntrustedHosts
private void rejectedUntrustedHosts(HttpServletRequest request) {
String serverName = request.getServerName();
if (serverName != null && !this.allowedHostnames.test(serverName)) {
throw new RequestRejectedException("The request was rejected because the domain " + serverName + " is untrusted.");
}
}
org.springframework.security.web.firewall.StrictHttpFirewall#isNormalized(java.lang.String)
private static boolean isNormalized(String path) {
if (path == null) {
return true;
} else {
int slashIndex;
for(int i = path.length(); i > 0; i = slashIndex) {
slashIndex = path.lastIndexOf(47, i - 1);
int gap = i - slashIndex;
if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
return false;
}
if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
return false;
}
}
return true;
}
}
检查request.getRequestURI() request.getContextPath() request.getServletPath() request.getPathInfo() 不允许出现.
, /./
或者 /.
对 request.getRequestURI();
调用org.springframework.security.web.firewall.StrictHttpFirewall#containsOnlyPrintableAsciiCharacters
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
int length = uri.length();
for(int i = 0; i < length; ++i) {
char ch = uri.charAt(i);
if (ch < ' ' || ch > '~') {
return false;
}
}
return true;
}
不允许出现的特殊字符
!
"
#
$
%
&
'
(
)
*
+
,
-
.
/
:
;
<
=
>
?
@
[
\
]
^
_
`
{
|
}
~
获取filters,调用virtualFilterChain.doFilter
走入下面会遍历调用doFilter,走入 Filter执行链
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
if (FilterChainProxy.logger.isDebugEnabled()) {
FilterChainProxy.logger.debug(LogMessage.of(() -> {
return "Secured " + FilterChainProxy.requestLine(this.firewalledRequest);
}));
}
this.firewalledRequest.reset();
this.originalChain.doFilter(request, response);
} else {
++this.currentPosition;
Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
if (FilterChainProxy.logger.isTraceEnabled()) {
FilterChainProxy.logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(), this.currentPosition, this.size));
}
nextFilter.doFilter(request, response, this);
}
}
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
调用 super.beforeInvocation(filterInvocation);
org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource#getAttributes
org.springframework.security.web.util.matcher.RegexRequestMatcher#matches
进行正则匹配。
这里先换成漏洞的配置
@Configuration
@EnableWebSecurity
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.regexMatchers("/admin/.*").authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults());
return http.build();
}
使用regexMatchers即会用org.springframework.security.web.util.matcher.RegexRequestMatcher#matches
类来处理编写的规则
访问/admin/123
是命中这条规则的,在配置里面这个规则是需要走认证的。
但是访问``/admin/123%0d`这里的正则匹配是为flase,并没有命中这条规则,从而走到下一条规则从而实现绕过。
这里的问题就在于用了.*
的正则去匹配,而传入数据%0d的话是匹配不上的。Pattern默认的规则是不匹配\r\n
等的。
public class test {
public static void main(String[] args) {
String regex = "a.*b";
//输出true,指定Pattern.DOTALL模式,可以匹配换行符。
Pattern pattern1 = Pattern.compile(regex,Pattern.DOTALL);
boolean matches1 = pattern1.matcher("aaabbb").matches();
System.out.println(matches1);
boolean matches2 = pattern1.matcher("aa\nbb").matches();
System.out.println(matches2);
//输出false,默认点(.)没有匹配换行符
Pattern pattern2 = Pattern.compile(regex);
boolean matches3 = pattern2.matcher("aaabbb").matches();
boolean matches4 = pattern2.matcher("aa\nbb").matches();
System.out.println(matches3);
System.out.println(matches4);
}
}
//true
//true
//true
//false
但是如果加上Pattern.DOTALL
参数的话即便有\n,也会进行匹配。所以后面版本修复使用到了Pattern.DOTALL
https://github.com/spring-projects/spring-security/commit/70863952aeb9733499027714d38821db05654856
Spring Security的一个简单auth bypass和一些小笔记
Spring Security原理篇(四) FilterChainProxy
Spring Securit要比Shiro要安全不少,自带的StrictHttpFirewall
把一些可测试的危险字符限制比较死。