1单点登陆需求
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。使用“单点登录”整合后,只需要登录一次就可以进入多个系统,而不需要重新登录,这不仅仅带来了更好的用户体验,更重要的是降低了安全的风险和管理的消耗
2 单点登陆的技术实现机制
单点登录的机制,当用户第一次访问应用系统1(如下图)的时候,因为还没有登录,会被引导到认证系统中进行登录
(1);根据用户提供的登录信息,认证系统进行身份效验,如果通过效验,应该返回给用户一个认证的凭据--ticket
(2);用户再访问别的应用的时候(3,5)就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行效验,检查ticket的合法性(4,6)。如果通过效验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。
从上面的视图可以看出,要实现SSO,需要以下主要的功能: 所有应用系统共享一个身份认证系统。
统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行效验,判断其有效性。 所有应用系统能够识别和提取ticket信息
要实现SSO的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。 有两点需要指出的是:
单一的用户信息数据库并不是必须的,有许多系统不能将所有的用户信息都集中存储,应该允许用户信息放置在不同的存储中,如下图所示。事实上,只要统一认证系统,统一ticket的产生和效验,无论用户信息存储在什么地方,都能实现单点登录。
3 WEB-SSO的实现
在同一个应用中也需要保存用户的登录身份信息。例如用户在访问页面1的时候进行了登录,但护应用的状态。C是刚才也提到,客户端的每个请求都是单独的连接,当客户再次访问页面2的时候,如何才能告诉Web服务器,客户刚才已经登录过了呢?浏览器和服务器之间有约定:通过使用cookie技术来维ookie是可以被Web服务器设置的字符串,并且可以保存在浏览器中。如下图所示,当浏览器访问了页面1时,web服务器设置了一个cookie,并将这个cookie和页面1一起返回给浏览器,浏览器接到cookie之后,就会保存起来,在它访问页面2的时候会把这个cookie也带上,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状态。
Web-SSO完全可以利用Cookie结束来完成用户登录信息的保存,将浏览器中的Cookie和上文中的Ticket结合起来,完成SSO的功能。 3.2.1身份认证服务代码解析 身份认证服务是一个标准的web应用,包括一个名为SSOAuth的Servlet,一个login.jsp文件和一个failed.html。身份认证的所有服务几乎都由SSOAuth的Servlet来实现了;login.jsp用来显示登录的页面(如果发现用户还没有登录过);failed.html是用来显示登录失败的信息(如果用户的用户名和密码与信息数据库中的不一样)。
SSOAuth的代码如下面的列表显示,结构非常简单,先看看这个Servlet的主体部分: 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\accounts.put(\"paul\
accounts.put(\"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分钟之内,就不需要再登录了
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,并且判断当
前用户是否登录过。不过这个笨方法没有人会用:-)。
Filter是比较好的方法。符合Servlet2.3以上的J2EE容器就具有部署filter的功能。(Filter的使用可以参考JavaWolrd的文章
http://www.javaworld.com/javaworld/jw-06-2001/jw-0622-filters.html)Filter是一个具有很好的模块化,可重用的编程API,用在SSO正合适不过。本样例就是使用一个filter来完成以上的功能。
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\"); } ..... ..... }
以上的初始化的源代码有两点需要说明:一是有两个需要配置的参数SSOServiceURL和
SSOLoginPage。因为当前的Web应用很可能和身份认证服务(SSOAuth)不在同一台机器上,所以需要让这个filter知道身份认证服务部署的URL,这样才能去调用它的服务。另外一点就是由于身份认证的服务调用是要通过http协议来调用的(在本样例中是这样设计的,读者完全可以设计自己的身份服务,使用别的调用协议,如RMI或SOAP等等),所有笔者引用了apache的commons工具包(详细信息情访问apache 的网站http://jakarta.apache.org/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.getName().equals(cookieName)){ cookieValue = diskCookies.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\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请求。 SSOFilter *.jsp 下面还有几个主要的函数需要说明: 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)替我自动生成的。
另外,Filter的性能问题也是需要重视的方面。因为Filter会截获每一个符合URL映射规则的请求,获得cookie,验证其有效性。这一系列任务是比较消耗资源的,特别是验证cookie有效性是一个远程的http的调用,来访问SSOAuth的认证服务,有一定的延时。因此在性能上需要做进一步的提高。例如在本样例中,如果将URL映射从“.jsp”改成“/*”,也就是说filter对所有的请求都起作用,整个应用会变得非常慢。这是因为,页面当中包含了各种静态元素如gif图片,css样式文件,和其他html静态页面,这些页面的访问都要通过filter去验证。而事实上,这些
静态元素没有什么安全上的需求,应该在filter中进行判断,不去效验这些请求,性能会好很多。另外,如果在filter中加上一定的cache,而不需要每一个cookie效验请求都去远端的身份认证服务中执行,性能也能大幅度提高。
另外系统还需要很多其他的服务,如在内存中定时删除无用的cookie映射等等,都是一个严肃的解决方案需要考虑的问题。
用用户名和密码登录的方法要复杂一些,通过Callback的机制和屏幕输入输出进行信息交互,完成用户登录信息的获取;获取信息以后通过userAuth方法来调用远端SSOAuth的服务来判定当前登录的有效性。
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\//
// 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(); }
CookieAuth和userAuth方法都是利用apahce的httpclient工具包和远程的SSOAuth进行http连接,获取服务。
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;
}
因篇幅问题不能全部显示,请点此查看更多更全内容