系统防护变成纸老虎
请注意,下文中所有的代码示例我都省略了无关代码,仅仅保留关键代码。如果对代码详情有更多的兴趣,可以查阅相关源码。
我喜欢在文章结尾说一些关于程序员的题外话,帮助程序员成长,有兴趣可以看一看。
这个故事要从前两天公司发现一个漏洞说起。
我们都知道,在浏览网站时需要登录。当你获得相应的授权以后,才可以进行进一步操作。比如类似http://xxx/page/administrator这种接口,在未登录的情况下,访问会返回这种结果:
最近发现的漏洞就是,对于部署在jetty上的服务。通过加反斜杠可以绕过漏洞,即通过http://xxx//page/administrator可以正常返回数据(出于安全考虑,这里不展示正常的返回数据了),不需要鉴权!!!
为什么鉴权会被绕过
jetty在匹配filter时,使用了相对严格的匹配模式,对于/page/administrat路径,会命中/page/*的规则,从而被鉴权过滤器(比如SSOFilter)拦截进行鉴权。
然而,//page/administrat这种有两个反斜杠的路径,不属于上述命中规则,因此也不会被拦截,从而长驱直入。这就很恐怖!!
解决方法
方法一
直接将鉴权过滤器的匹配规则设置成/*,直接拦截所有访问。这样就不存在绕过的可能性了,这样简单粗暴,不用费脑子。
这个方法有点小问题,因为我们知道大公司里的服务,一般都会有 健康检查。监控系统会去访问健康检查路径,系统返回http返回码200 OK证明服务正常。监控系统访问健康检查路径是不会去带上鉴权信息的。
因此,你得在拦截器中单独排除健康检查路径,这就很不优雅。要是不排除健康检查的路径,就会一直因为鉴权失败而无法返回200 OK,导致监控系统认为你的服务一直运行不起来,然后发出警报。于是有了方法二。
方法二
在正常的访问中,是不会存在//XXX/YY这种连续出现多个反斜杠的非法请求的。所以,我在鉴权拦截器前边再加一个拦截器,这个拦截器直接过滤这种非法请求不就可以了?
public void doFilter(ServletRequest request,ServletResponse response, FilterChain filterChain)
{
String url= request.getRequestURL();
if(!url.indexOf("//")<0)
{ //很显然,公司代码里当然没有这么写,不过意思差不多
response.getWriter().write("别跟爷爷闹,乖乖的输入地址")
}else{
....
}
}
response.getWriter().write("别跟爷爷闹,乖乖的输入地址")才是精髓。
**你狂任你狂,我直接板砖破武术!**逻辑很简单,但是很有效。只要你格式不对,就压根不给你访问。有什么花招尽管使出来,你绕过来我算你赢!
不过你可能会问,url.indexOf("//")<0只能判断//,那///或者////等等这种呢?朋友,///里边不是有一个//嘛?肯定会检测出来噻!
要不要被扒裤子
上边我们确实解决问题了,安全隐患也暂时被解决了。但是你有没有仔细想过一个问题?
当黑客小哥哥的浏览器看到这句别跟爷爷闹,乖乖的输入地址的时候,我们的SSO到底生效了吗?或者说,他的访问请求到底是在绕过了SSOFilter之后,被我们的格式校验filter拦截。还是先被格式校验filter拦截,压根连SSOFilter摸都没摸到?
这中间是有质的区别的。前者是他扒了你的裤子,但是你以光速重新提上了。后者则是你系紧了腰带,让他随便扒也扒不下来,并用嘲讽的眼光揶揄他。两者虽然都暂时没有走光的风险,但明显后者安全感max!
那么,我们的系统到底是前者还是后者?**取决于filter的在系统中的加载顺序!**要谈起这个问题,就属于孩子没娘,说来话长了。我们今天好好掰扯一下这个问题。
Filter的前世今生
作用
web中的filter一般被称为过滤器,也可以叫拦截器。听名字就知道差不多知道它的作用。
实际上,Filter和servlet很类似,可以认为是servlet的另一种特殊形态。和servlet一样,也是对请求进行处理,只不过filter并不生成返回给用户的响应结果。
Filter在web应用中有举足轻重的作用,很多功能都需要靠Filter完成。比如常见的:鉴权、访问日志、编码解码、拦截特殊请求等等,都可以通过filter进行实现。
配置与实例
在web应用中配置一个Filter有两种方法,一种是通过web.xml,另一种的通过注解方式。
下边我们自定义一个Filter,这个Filter实现一个简单的功能:当有访问来时,我们的控制台输出有人访问啦这句话。通过这个例子来分别展示两种配置。
定义一个Filter
package quLunBianCheng;
public class DemoFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("容器启动时,会调用init方法对DemoFilter进行实例化哦");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("有人来访问");
chain.doFilter(request, response);
}
public void destroy() {
System.out.println("DemoFilter要被销毁时,会先调用destroy方法");
}
}
xml配置
<filter>
<filter-name>DemoFilter</filter-name>
<filter-class>quLunBianCheng.DemoFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>DemoFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
这个配置我们应该很熟悉,总是能在web.xml中看到这种配置。 <url-pattern>/*</url-pattern> 意味所有访问都将被DemoFilter拦截。因此,每当有访问时,你的控制台上都会输出有人来访问啦
由此,你可以衍生出更加复杂的功能。比如,你可以统计你的网页被访问的次数,只需要稍微改写一下DemoFilter的doFilter方法即可。
public void doFilter(ServletRequest request,ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("有人来访问");
//访问次数+1
Constant.visitCount++;
System.out.println("你的网页已经被"+Constant.visitCount+"人访问过啦");
chain.doFilter(request, response);
}
@WebFilter注解
注解形式有很多好处,比如看起来比较舒服、代码比较整洁、免去xml配置文件等等。但是,本质上和xml没有区别,只是换了一种方式而已。
注解方式中无需配置xml文件,只需要把DemoFilter做一个小小的改动就可以。
package quLunBianCheng;
@WebFilter(filterName = "DemoFilter",urlPatterns = "/*")
public class DemoFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("容器启动时,会调用init方法对DemoFilter进行实例化哦");
}
//为了简洁起见,我这里吧dofilter和destory方法省略了,没有写出来。
}
只用加上@WebFilter(filterName = "DemoFilter",urlPatterns = "/*")就可以完成配置。
实际上,@WebFilter注解的可选配置参数还有很多,已经列在文章末尾—>@webFilter属性附录
过滤器虽然重要,但是你不要怕,实际上就这么点事儿。什么?你想知道Filter的初始化例程?这个下篇文章吧,这篇文章写不下了。因为下边还要讲挺多东西的,篇幅不太够了。
一个鉴权Filter的诞生
本文是以一个鉴权问题为开头的,那么我们接着讲鉴权。前边讲Filter就是为了引出鉴权Filter。
登录鉴权过滤器
实际上,鉴权filter本身就是一个Filter,没有什么神秘的。分分钟实现一个鉴权filter。
把上边的DemoFilter过滤器改一改,就可以变成登录鉴权浏览器。
package quLunBianCheng;
public class LoginFilter implements Filter {
//省略非关键代码
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//获得请求
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//拿到cookie
Cookie[] cookies = request.getCookies();
//遍历cookie,找到用户名和密码
String username = "";
String password = "";
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("username")) {
username = cookie.getValue();
} else if (cookie.getName().equals("password")) {
password = cookie.getValue();
}
}
}
//如果第一次登录没有cookie或者用户名、密码不一致,则让他重新登录
if (username.equals("") || password.equals("")||(!username.equals("XXX")) ||(!password.equals("XXXXXXX")) {
//重定向,让他登录
response.sendRedirect("login.jsp");
return ;
} else{
//如果通过就可以放行了
chain.doFilter(request,response);
}
}
}
上述代码中的!username.equals("XXX")) ||(!password.equals("XXXXXXX")仅仅是一个简单示范,实际上真实项目中这个都是从数据库查询的,而不是XXXXXX
Filter的配置在这里就不细说了,和前边是一致的,你可以用web.xml或者@webFilter注解两种方式去配置(但是,…….请不要同时用两种方法……)
可以看出,实现一个简单的登录过滤器是非常简单的。但是真实的生产环境中,是不会这么做的,为啥?因为不安全啊!(**程序员小哥哥为了你的数据安全真的是煞费苦心,所以答应我不要把密码设置成aaa123或者123456789这种,可以吗?在线跪地磕头!!!! **)。
不瞎聊了,我们看看上边的做法为啥不安全。因为我们在cookie中直接存放了用户名和密码,cookie是存在用户浏览器中,很容易破解。用户很难有专业能力去保护cookie信息。cookie中包含大量的个人重要信息(不仅仅是密码,还有可能存放你在物理世界的一些信息)是很危险的行为。
怎么办?假如我们在cookie里放一个没啥意义的标识码,你每次请求带上这个标识码,服务器通过标识码寻找存在服务器中的用户信息(称为session),这个标识码就是sessionId。即便黑客拿到cookie,他也无法从cookie里直接解出你的个人信息,这样就相对安全多了(毕竟服务器抗攻击的能力远远强于用户侧,如果保护得当,黑客想从服务器中获取敏感信息的难度就大的多了)。
因此,LoginFilter经过一些改变,就可以变成下边的SSOFilter。
ssoFilter
package quLunBianCheng;
public class SsoFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
/*初始化方法 接收一个FilterConfig类型的参数 该参数是对Filter的一些配置*/
}
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {
/*这里就不重复放代码了,用伪代码说一下把*/
1:和上边一样,从Cookies中过滤出ssoId
2:再拿着这个ssoId去服务器里查询出对应的登录名和密码
3:如果登录名和密码匹配,就放行。如果不符合,就重新登录
}
}
这样,我们就从一个漏洞开始,完整的讲述了Filter—>鉴权Filter->SSOFilter。并且每一步都给出了详细的例子,甚至包括配置信息。
但是这些就足够了吗?NO,还是不太Ok的。我在!!!一文中,我曾经讲过:做技术的人必须要学会刨根问底。
再进一步
这一小节中主要以不同的声明方式为引子,抛出更多关于过滤器的类,并试图详细解释它们的原理。
如果没有特殊说明。这一小节中提到的SsoFilter就是上小节中的SsoFilter
你有没有在web.xml中见过这样的配置?
<filter>
<filter-name>SsoFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SsoFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
或者这样的配置?
@Bean
public FilterRegistrationBean<Filter> LoginFilter() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SsoFilter());
registration.addUrlPatterns("/*");
registration.setName("SsoFilter");
registration.setOrder(0);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
return registration;
}
亦或者这样的配置?
@Bean
public DelegatingFilterProxyRegistrationBean ssoFilterProxyRegistrationBean() {
DelegatingFilterProxyRegistrationBean filterRegistration = new DelegatingFilterProxyRegistrationBean("ssoFilter");
filterRegistration.addInitParameter("targetFilterLifecycle", "true");
filterRegistration.addUrlPatterns("/*");
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
filterRegistration.setOrder(2);
return filterRegistration;
}
大概率你是见过的,只是你没有注意或者关注为什么这么写而已。其实,这三种方式实现的功能和之前的ssoFilter一模一样,只不过是使用了代理而已。
我好端端的,为啥非要去代理一下子?因为,**通过代理可以让我们自己定义的ssoFilter与容器完美融合,享用spring容器功能,用spring来管理Filter的生命周期,Filter中如果用到某些bean可以直接通过注入的方法获取。**此外还有很多的便利之处。所以,我们很有必要了解一下这些代理方法。
我们看到这三种方式中,出现了三个新的代理类DelegatingFilterProxy、FilterRegistrationBean和DelegatingFilterProxyRegistrationBean。
本来想在这篇文章把后边的也讲了,但是感觉篇幅太长了,所以打算分成两期。
我们下一篇讲这三个代理类的原理,并且回答前文提到的扒裤子的问题。
附录
健康检查
所谓的健康检查,就是controller有一个访问接口,一般路径都是xxx/alive。访问这个路径如果返回的HTTP响应码是200,则我们可以任务服务已经运行起来了。如果不能返回200,则这个系统现在肯定是有问题。
@webFilter属性附录
| 属性名 | 类型 | 描述 |
|---|---|---|
| filterName | String | 指定过滤器的 name 属性 |
| value | String[] | 该属性等价于 urlPatterns 属性。但是两者不应该同时使用 |
| urlPatterns | String[] | 指定一组过滤器的URL匹配模式。等价于 标签 |
| servletNames | String[] | 指定过滤器将应用于哪些 servlet。取值是@webServlet中的name属性的取值,或者是web.xml中 取值 |
| dispatcherTypers | DispatcherType | 指定过滤器的转发模式,具体取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST。 |
| initParams | WebInitParam[] | 指定一组过滤器初始化参数,等价于标签 |
| asyncSupported | boolean | 声明过滤器是否支持异步操作模式,等价于标签 |
| description | String | 该过滤器的描述信息,等价于标签 |
| displayName | String | 该过滤器的显示名,通常配合工具使用,等价于标签 |
实际上,任何一个注解的参数,我们都可以不必通过百度去搜索。因为,对于一个技术人员,获取第一手的资料是一项非常重要的能力。
你直接command+鼠标左键点击注解,你就会跳转到对应注解的定义。@webFilter的定义如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebFilter {
String description() default "";
String displayName() default "";
WebInitParam[] initParams() default {};
String filterName() default "";
String smallIcon() default "";
String largeIcon() default "";
String[] servletNames() default {};
String[] value() default {};
String[] urlPatterns() default {};
DispatcherType[] dispatcherTypes() default {DispatcherType.REQUEST};
boolean asyncSupported() default false;
}
你可以清楚的看见有哪些参数,并且每一个都有清晰、明了的注释。自己动手,丰衣足食!
语录
语录:
如果一个人不会游泳,那么无论换哪一个游泳池,他都不会游。
解析:
看有些程序员总是抱怨自己没有好的机会,说自己维护着一些屎一样的代码,没有挑战,只有冗杂和毫无意义的事情。实际上,没有任何一件事事毫无意义的。如果你能从不同的角度去看待一个问题,用不同的心态去面对一些境遇,你就不会有这个抱怨。
给任何一个人一个好的机会,结果都不会太差。但那些真正超越别人的人,绝不是靠命运的垂青才脱颖而出的。那些抱怨机会不行的人,大概是不想付出太多努力,而又期望一夜暴富。工地上把墙面砌的最整齐的农民工,总是能获得包工头的赏识。把任何一件事做到极致,都不是一件毫无意义的事情。更何况看我这篇文章的人,处境和起点要比底层体力劳动人民好无数倍。