编写你自己的单点登录(SSO)服务 2
3.2 WEB-SSO代码讲解
3.2.1身份认证服务代码解析
Web-SSO的源代码可以从网站地址http://211.151.94.21/blog/yutoujava/resource/websso_src.zip下载。身份认证服务是一个标准的web应用,包括一个名为SSOAuth的Servlet,一个login.jsp文件和一个failed.html。身份认证的所有服务几乎都由SSOAuth的Servlet来实现了;login.jsp用来显示登录的页面(如果发现用户还没有登录过);failed.html是用来显示登录失败的信息(如果用户的用户名和密码与信息数据库中的不一样)。
package DesktopSSO;
import java.io.*;
import .*;
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();
}
}
.....
}
|
从代码很容易看出,SSOAuth就是一个简单的Servlet。其中有两个静态成员变量:accounts和SSOIDs,这两个成员变量都使用了JDK1.5中线程安全的MAP类: ConcurrentMap ,所以这个样例一定要 JDK1.5 才能运行。 Accounts 用来存放用户的用户名和密码,在 init() 的方法中可以看到我给系统添加了三个合法的用户。在实际应用中, accounts 应该是去数据库中或 LDAP 中获得,为了简单起见,在本样例中我使用了 ConcurrentMap
在内存中用程序创建了三个用户。而
SSOIDs
保存了在用户成功的登录后所产生的
cookie
和用户名的对应关系。它的功能显而易见:当用户成功登录以后,再次访问别的系统,为了鉴别这个用户请求所带的
cookie
的有效性,需要到
SSOIDs
中检查这样的映射关系是否存在。
在主要的请求处理方法 processRequest()
中,可以很清楚的看到
SSOAuth
的所有功能
-
如果用户还没有登录过,是第一次登录本系统,会被跳转到 login.jsp 页面(在后面会解释如何跳转)。用户在提供了用户名和密码以后,就会用 handlerFromLogin()
这个方法来验证。
-
如果用户已经登录过本系统,再访问别的应用的时候,是不需要再次登录的。因为浏览器会将第一次登录时产生的 cookie 和请求一起发送。效验 cookie 的有效性是 SSOAuth
的主要功能之一。
-
SSOAuth 还能直接效验非 login.jsp 页面过来的用户名和密码的效验请求。这个功能是用于非 web 应用的 SSO ,这在后面的桌面 SSO 中会用到。
-
SSOAuth 还提供 logout 服务。
下面看看几个主要的功能函数:
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();
}
}
}
|
handlerFromLogin() 这个方法是用来处理来自 login.jsp 的登录请求。它的逻辑很简单:将用户输入的用户名和密码与预先设定好的用户集合(存放在 accounts 中)相比较,如果用户名或密码不匹配的话,则返回登录失败的页面( failed.html ),如果登录成功的话,需要为用户当前的 session 创建一个新的 ID ,并将这个 ID 和用户名的映射关系存放到 SSOIDs 中,最后还要将这个 ID 设置为浏览器能够保存的 cookie 值。
登录成功后,浏览器会到哪个页面呢?那我们回顾一下我们是如何使用身份认证服务的。一般来说我们不会直接访问身份服务的任何 URL ,包括 login.jsp 。身份服务是用来保护其他应用服务的,用户一般在访问一个受 SSOAuth 保护的 Web 应用的某个 URL 时,当前这个应用会发现当前的用户还没有登录,便强制将也页面转向 SSOAuth 的 login.jsp ,让用户登录。如果登录成功后,应该自动的将用户的浏览器指向用户最初想访问的那个 URL 。在 handlerFromLogin()
这个方法中,我们通过接收“
goto”
这个参数来保存用户最初访问的
URL
,成功后便重新定向到这个页面中。
另外一个要说明的是,在设置cookie的时候,我使用了一个setMaxAge(6000) 的方法。这个方法是用来设置 cookie 的有效期,单位是秒。如果不使用这个方法或者参数为负数的话,当浏览器关闭的时候,这个 cookie 就失效了。在这里我给了很大的值( 1000 分钟),导致的行为是:当你关闭浏览器(或者关机),下次再打开浏览器访问刚才的应用,只要在 1000 分钟之内,就不需要再登录了。我这样做是下面要介绍的桌面 SSO 中所需要的功能。
其他的方法更加简单,这里就不多解释了。
3.2.2具有SSO功能的web应用源代码解析
要实现 WEB-SSO 的功能,只有身份认证服务是不够的。这点很显然,要想使多个应用具有单点登录的功能,还需要每个应用本身的配合:将自己的身份认证的服务交给一个统一的身份认证服务- SSOAuth
。
SSOAuth
服务中提供的各个方法就是供每个加入
SSO
的
Web
应用来调用的。
一般来说, Web 应用需要 SSO 的功能,应该通过以下的交互过程来调用身份认证服务的提供的认证服务:
-
Web 应用中每一个需要安全保护的 URL 在访问以前,都需要进行安全检查,如果发现没有登录(没有发现认证之后所带的 cookie ),就重新定向到 SSOAuth 中的 login.jsp 进行登录。
-
登录成功后,系统会自动给你的浏览器设置 cookie ,证明你已经登录过了。
-
当你再访问这个应用的需要保护的 URL 的时候,系统还是要进行安全检查的,但是这次系统能够发现相应的 cookie 。
-
有了这个 cookie ,还不能证明你就一定有权限访问。因为有可能你已经 logout, 或者 cookie 已经过期了,或者身份认证服务重起过,这些情况下,你的 cookie 都可能无效。应用系统拿到这个 cookie ,还需要调用身份认证的服务,来判断 cookie 时候真的有效,以及当前的 cookie 对应的用户是谁。
-
如果 cookie 效验成功,就允许用户访问当前请求的资源。
以上这些功能,可以用很多方法来实现:
-
在每个被访问的资源中( JSP 或 Servlet )中都加入身份认证的服务,来获得 cookie ,并且判断当前用户是否登录过。不过这个笨方法没有人会用 :-) 。
-
可以通过一个 controller ,将所有的功能都写到一个 servlet 中,然后在 URL 映射的时候,映射到所有需要保护的 URL 集合中(例如 *.jsp , /security/* 等)。这个方法可以使用,不过,它的缺点是不能重用。在每个应用中都要部署一个相同的 servlet 。
-
Filter 是比较好的方法。符合 Servlet2.3 以上的 J2EE 容器就具有部署 filter 的功能。( Filter 的使用可以参考 JavaWolrd 的文章 http://www./javaworld/jw-06-2001/jw-0622-filters.html ) Filter 是一个具有很好的模块化,可重用的编程 API ,用在 SSO 正合适不过。本样例就是使用一个 filter 来完成以上的功能。
package SSO;
import java.io.*;
import .*;
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.:8080/SSOAuth/SSOAuth";
private String SSOLoginPage= "http://wangyu.prc.: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");
}
.....
.....
}
|
以上的初始化的源代码有两点需要说明:一是有两个需要配置的参数 SSOServiceURL
和
SSOLoginPage
。因为当前的
Web
应用很可能和身份认证服务(
SSOAuth
)不在同一台机器上,所以需要让这个
filter
知道身份认证服务部署的
URL
,这样才能去调用它的服务。另外一点就是由于身份认证的服务调用是要通过
http
协议来调用的(在本样例中是这样设计的,读者完全可以设计自己的身份服务,使用别的调用协议,如
RMI
或
SOAP
等等),所有笔者引用了
apache
的
commons
工具包(详细信息情访问
apache
的网站
http://jakarta./commons/index.html
),其中的“
httpclient”
可以大大简化
http
调用的编程。
下面看看 filter 的主体方法 doFilter():
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);
}
}
}
|
doFilter() 方法的逻辑也是非常简单的,在接收到请求的时候,先去查找是否存在期望的 cookie 值,如果找到了,就会调用 SSOService(cookieValue)
去效验这个
cookie
的有效性。如果
cookie
效验不成功或者
cookie
根本不存在,就会直接转到登录界面让用户登录;如果
cookie
效验成功,就不会做任何阻拦,让此请求进行下去。在配置文件中,有下面的一个节点表示了此
filter
的
URL
映射关系:只拦截所有的
jsp
请求。
<filter-mapping> <filter-name>SSOFilter</filter-name> <url-pattern>*.jsp</url-pattern> </filter-mapping>
下面还有几个主要的函数需要说明:
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();
}
}
|
这两个函数主要是利用 apache 中的 httpclient 访问 SSOAuth 提供的认证服务来完成效验 cookie 和 logout 的功能。
其他的函数都很简单,有很多都是我的 IDE ( NetBeans )替我自动生成的。
4 当前方案的安全局限性
当前这个WEB-SSO的方案是一个比较简单的雏形,主要是用来演示SSO的概念和说明SSO技术的实现方式。有很多方面还需要完善,其中安全性是非常重要的一个方面。
我们说过,采用SSO技术的主要目的之一就是加强安全性,降低安全风险。因为采用了SSO,在网络上传递密码的次数减少,风险降低是显然的,但是当前的方案却有其他的安全风险。由于cookie是一个用户登录的唯一凭据,对cookie的保护措施是系统安全的重要环节:
-
cookie的长度和复杂度 在本方案中,cookie是有一个固定的字符串(我的姓名)加上当前的时间戳。这样的cookie很容易被伪造和猜测。怀有恶意的用户如果猜测到合法的cookie就可以被当作已经登录的用户,任意访问权限范围内的资源
-
cookie的效验和保护 在本方案中,虽然密码只要传输一次就够了,可cookie在网络中是经常传来传去。一些网络探测工具(如sniff, snoop,tcpdump等)可以很容易捕获到cookie的数值。在本方案中,并没有考虑cookie在传输时候的保护。另外对cookie的效验也过于简单,并不去检查发送cookie的来源究竟是不是cookie最初的拥有者,也就是说无法区分正常的用户和仿造cookie的用户。
-
当其中一个应用的安全性不好,其他所有的应用都会受到安全威胁 因为有SSO,所以当某个处于 SSO的应用被黒客攻破,那么很容易攻破其他处于同一个SSO保护的应用。
这些安全漏洞在商业的SSO解决方案中都会有所考虑,提供相关的安全措施和保护手段,例如Sun公司的Access Manager,cookie的复杂读和对cookie的保护都做得非常好。另外在OpneSSO (https://opensso.dev.)的架构指南中也给出了部分安全措施的解决方案。
5 当前方案的功能和性能局限性
除了安全性,当前方案在功能和性能上都需要很多的改进:
-
当前所提供的登录认证模式只有一种:用户名和密码,而且为了简单,将用户名和密码放在内存当中。事实上,用户身份信息的来源应该是多种多样的,可以是来自数据库中,LDAP中,甚至于来自操作系统自身的用户列表。还有很多其他的认证模式都是商务应用不可缺少的,因此SSO的解决方案应该包括各种认证的模式,包括数字证书,Radius, SafeWord ,MemberShip,SecurID等多种方式。最为灵活的方式应该允许可插入的JAAS框架来扩展身份认证的接口
-
我们编写的Filter只能用于J2EE的应用,而对于大量非Java的Web应用,却无法提供SSO服务。
-
在将Filter应用到Web应用的时候,需要对容器上的每一个应用都要做相应的修改,重新部署。而更加流行的做法是Agent机制:为每一个应用服务器安装一个agent,就可以将SSO功能应用到这个应用服务器中的所有应用。
-
当前的方案不能支持分别位于不同domain的Web应用进行SSO。这是因为浏览器在访问Web服务器的时候,仅仅会带上和当前web服务器具有相同domain名称的那些cookie。要提供跨域的SSO的解决方案有很多其他的方法,在这里就不多说了。Sun的Access Manager就具有跨域的SSO的功能。
-
另外,Filter的性能问题也是需要重视的方面。因为Filter会截获每一个符合URL映射规则的请求,获得cookie,验证其有效性。这一系列任务是比较消耗资源的,特别是验证cookie有效性是一个远程的http的调用,来访问SSOAuth的认证服务,有一定的延时。因此在性能上需要做进一步的提高。例如在本样例中,如果将URL映射从“.jsp”改成“/*”,也就是说filter对所有的请求都起作用,整个应用会变得非常慢。这是因为,页面当中包含了各种静态元素如gif图片,css样式文件,和其他html静态页面,这些页面的访问都要通过filter去验证。而事实上,这些静态元素没有什么安全上的需求,应该在filter中进行判断,不去效验这些请求,性能会好很多。另外,如果在filter中加上一定的cache,而不需要每一个cookie效验请求都去远端的身份认证服务中执行,性能也能大幅度提高。
-
另外系统还需要很多其他的服务,如在内存中定时删除无用的cookie映射等等,都是一个严肃的解决方案需要考虑的问题。
|