3.2 WEB-SSO
代码讲解
3.2.1
身份认证服务代码解析
Web-SSO
的源代码可以从网站地址
http://gceclub.sun.com.cn/wangyu/web-sso/websso_src.zip
下载。身份认证服务是一个标准的
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", "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.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[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>
<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
)替我自动生成的。