java实现简单的单点登录(3)

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 的所有功能
  1. 如果用户还没有登录过,是第一次登录本系统,会被跳转到login.jsp页面(在后面会解释如何跳转)。用户在提供了用户名和密码以后,就会用handlerFromLogin()这个方法来验证。
  2. 如果用户已经登录过本系统,再访问别的应用的时候,是不需要再次登录的。因为浏览器会将第一次登录时产生的cookie和请求一起发送。效验cookie的有效性是SSOAuth的主要功能之一。
  3. SSOAuth还能直接效验非login.jsp页面过来的用户名和密码的效验请求。这个功能是用于非web应用的SSO,这在后面的桌面SSO中会用到。
  4. 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效验成功,就允许用户访问当前请求的资源。
以上这些功能,可以用很多方法来实现:
  • 在每个被访问的资源中(JSPServlet)中都加入身份认证的服务,来获得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.htmlFilter是一个具有很好的模块化,可重用的编程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>
 
下面还有几个主要的函数需要说明:
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 )替我自动生成的。

猜你喜欢

转载自cn-done.iteye.com/blog/1061275