單點登錄(SSO)


SSO:單點登錄

前言:

本文采用兩個方法實現簡單單點登錄

1.方法1是cookie+filter實現

2.方法2是創建一個CAS服務器實現(底層依舊是cookie實現,所以兩個不同瀏覽器之前也是無法單點登錄的)

本文圖片沒有顯示,請自行下載附件的SSO.doc觀看

百度雲盤 資料地址:http://pan.baidu.com/s/1bnXrsS3

AUTHOR:zzm

1、使用Cookie解決單點登錄

       技術點:

       1、設置Cookie的路徑為setPath("/") .即Tomcat的目錄下都有效

2、設置Cookie的域setDomain(".itcast.com");即bbs.itcast.com,或是mail.itcast.com有效。即跨域。

3、設置Cookie的時間。即使用戶不選擇在幾天內自動登錄,也應該保存Cookie以保存在當前瀏覽器沒有關閉的情況下有效。

4、使用Filter自動登錄。

 

實現步驟:

1、首先要准備出幾個虛擬主機並配置hosts文件,即本機DNS。

       配置虛擬主機,主要通過修改tomcat_home/conf/server.xml文件完成:

 

增加幾個Host節點,通過Cookie實現自動登錄,必須配置的虛擬主頁滿足xxx.itcast.cn,即主域名必須保持一致。

 

2、先在bbs(或是mail)虛擬目錄下,開發一個可以自動登錄的程序,使用Filter:

1、登錄的主頁如下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<html>

  <head>

  </head>

  <body>

    <p>在同一台服務器上,多個站點自動登錄....>>:<%=session.getId()%></p>

    <c:if test="${empty sessionScope.user}">

        <form name="f" method="post" action="<c:url value='/login'/>">

              Name:<input type="text" name="name"/><br/>

              Pwd:<input type="text" name="pwd"/><br/>

              <input type="checkbox" name="chk" value="7">一周內自動登錄<br/>

              <input type="submit" value="登錄"/>        

        </form>

    </c:if>

    <c:if test="${not empty sessionScope.user}">

        歡迎你:${user}。<a href="<c:url value='/loginout'/>">安全退出</a>

    </c:if>

    <br/>

    相關站點:(只要在一邊登錄成功,即可以自動登錄到另一個程序)<br/>

    <a href="http://mail.itcast.com:7777">mail.itcast.com</a><br/>

    <a href="http://bbs.itcast.com:7777">bbs.itcast.com</a><br/>

  </body>

</html>

2、登錄的Servlet程序如下:

/**

 * 用戶登錄

 */

public class LoginServlet extends HttpServlet{

    public void doGet(HttpServletRequest req, HttpServletResponse resp)

           throws ServletException, IOException {

       doPost(req, resp);

    }

    public void doPost(HttpServletRequest req, HttpServletResponse resp)

           throws ServletException, IOException {

       String nm = req.getParameter("name");

       String pwd = req.getParameter("pwd");

       String chk = req.getParameter("chk");  //是否選中了7天自動登錄

       String forward = "/index.jsp";

       if(nm!=null && !nm.trim().equals("") && nm.startsWith("it")//用戶名是it開始,且密碼是pwd開始的可以登錄

              && pwd !=null && !pwd.trim().equals("") &&

              pwd.startsWith("pwd")){

           System.err.println("登錄成功。。。。。");

           forward = "/jsps/welcome.jsp";

           //無論如何,都要設置cookie,如果沒有選擇自動登錄,則只在當前頁面的跳轉時有效,否則設置有效期間為7天。

           Cookie cookie = new Cookie("autologin",nm+"@"+pwd);

           cookie.setPath("/");            //如果路徑為/則為整個tomcat目錄有用

           cookie.setDomain(".itcast.com");   //設置對所有*.itcast.com為后綴的域名效

           if(chk!=null){

              int time = 1*60*60*24*7; //1秒*60=1分*60分=1小時*24=1天*7=7天

              cookie.setMaxAge(time);

           }

           resp.addCookie(cookie);

           req.getSession().setAttribute("user", nm);

       }else{

           System.err.println("登錄不成功。。。。。。");

       }

       req.getRequestDispatcher(forward).forward(req, resp);

    }

}

 

3、自動登錄的Filter程序如下:

/**

 * 自動登錄

 */

public class AutoLogin implements Filter {

    public void destroy() {}

    public void doFilter(ServletRequest req, ServletResponse resp,

           FilterChain chain) throws IOException, ServletException {

        System.err.println("開始自動登錄驗證.....");//此類中應該對登錄的servlet直接放行。根據判斷url決定。

       HttpServletRequest requ = (HttpServletRequest) req;

       HttpSession s = requ.getSession();

       if (s.getAttribute("user") != null) {//如果用戶已經登錄則直接放行

           System.err.println("用戶已經登錄,沒有必須要再做自動登錄。。。。");

       } else {

           Cookie[] cookies = requ.getCookies();

           if (cookies != null) {

              for (Cookie ck : cookies) {

                  if (ck.getName().equals("autologin")) {// 是否是自動登錄。。。。

                     System.err.println("自動登錄成功。。。。。");

                     String val = ck.getValue();

                     String[] vals = val.split("@");

                     s.setAttribute("user", vals[0]);

                  }

              }

           }

       }

       chain.doFilter(req, resp);

    }

    public void init(FilterConfig filterConfig) throws ServletException {}

}

4、正常退出的Servlet如下

/**

 * 安全退出刪除Cookie

 */

public class LoginOutServlet extends HttpServlet {

    public void doGet(HttpServletRequest req, HttpServletResponse resp)

           throws ServletException, IOException {

       HttpSession s = req.getSession();      //獲取Session

       Cookie cookie = new Cookie("autologin","");//必須聲明一個完全相同名稱的Cookie

       cookie.setPath("/");//路徑也要完全相同

       cookie.setDomain(".itcast.com");//域也要完全相同

       cookie.setMaxAge(0);//設置時間為0,以直接刪除Cookie

       resp.addCookie(cookie);

       s.removeAttribute("user");

       System.err.println("安全退出。。。。。");

       resp.sendRedirect(req.getContextPath()+"/index.jsp");

    }

}

 

5、關於自動登錄的一些思考

為了讓單點登錄,變成可配置的功能,可以將保存Cookie的代碼,放到自動登錄的Filter中實現。

如果配置自動登錄的Filter則同時實現閃單點登錄,否則不加實現。

 

2、SSO示例程序配置

在CAS的主頁上,可以看到CAS服務器,和客戶端配置的完整過程,根據提示,完全可以配置成功服務器和客戶端。同時,在CAS上也可以找到服務器端的程序和客戶端的程序,都是已經配置好的,對於初步學習來說,完全可以直接取來配置測試。以下就是直接使用CAS官方提供的示例服務器和示例客戶端配置一個單點登錄的示例:

Jasig主頁:http://www.jasig.org/cas/download

 

 

第一步:先下載cas的服務器端支持包:cas-server3.xx.zip

      

第二步:下載cas有客戶端支持包。Cas-client.zip

      

 

第三步:下載客戶端的示例程序,這個不太好找。需要有點耐心

以下是下載的超連接:

https://wiki.jasig.org/display/CASC/JA-SIG+Java+Client+Simple+WebApp+Sample

 

第四步:解壓cas-server.zip如下圖,並從中找到服務器端war文件

服務器端程序一般不用我們完成,但需要做一點小小的修改,cas的服務器端程序由spring+Spring web flow+cas寫成。全部使用spring配置文件。它就是一個用戶登錄驗證、發售票據的中心。相當於Ticket Office(售票處)。

 

第五步:分別建立三個虛擬主機。

       一個是服務器主機,和兩個客戶端虛擬主機。

       1、修改etc/hosts文件:

      

       2、修改tomcat/conf/server.xml配置文件,增加虛擬主機目錄,同時將服務器的端口修改成80。

      

       3、在tomcat的根目錄下,分別建立三個目錄,即server、bbs、news。

              在三個目錄下,分別都建立一個ROOT(ROOT是tomcat的主默認主頁目錄)文件夾。

              將cas-server.xx.war解壓后散放到/tomcat/server/ROOT目錄下。如下圖:

              注意目錄結構,是散放到ROOT的目錄下。

第六步:先測試服務器是否可以正常使用

 啟動tomcat,在地址欄輸入:

 http://www.server.com

 

 登錄:

 

請先保證在單個服務器上登錄可以登錄成功。如果不能登錄成功,請重復前面的配置。

第七步:配置兩個客戶端

       將下載的文件分別解壓到tomcat/bbs/ROOT目錄下和tomcat/news/ROOT目錄下。注意是散放到ROOT目錄下。

由於在mywebapp.war中並沒有放置依賴的jar文件,所以,還需要我們添加它所依賴的jar文件,為此我為大家准備了已經放放置好的

mywebapp.war文件。

放置好的目錄結構如下:

(bbs程序同,不再上圖)

WEB-INF/lib目錄下的包如下:

這兩個包,在cas-client.rar文件中都可以找到。

此處,你可以啟動一個tomcat,如果啟動成功,則進入下一步。

第八步:修改客戶端的配置文件

       當使用登錄客戶端受保護的資源時,如果發現還沒有登錄,則會重定向到服務器(售票處)請求登錄驗證,登錄成功后即會獲取一張票據,服務器會攜帶這張票據再重定向到客戶端頁面。

修改客戶端的web.xml配置文件,讓它在登錄時,知道去哪台服務器:

注意將里面的https全部修改成http。

修改的部分主要分為兩塊:

1:修改登錄重定向過慮器,它用於保護受保護的資源,如果發面用戶在訪問受保護的資源時,用戶還沒有登錄,則會重定向到服務器,要求用戶登錄:

 

2、修改驗證過慮器,它的主要作用是接收服務器發送的Ticket,進行驗證

 

其他沒有說明的部分,請不要修改。

請參考上面的實現配置另一個項目的web.xml文件。

第九步:測試登錄

       目前還不能實現單點登錄。但可以對任意的一個客戶端進行登錄驗證。

1、  在地址欄輸入

http://www.news.com

 

點擊訪問受保護的頁面:got to protected area

將重定向到服務器請求登錄:

 

登錄成功后即重定回原請求頁面:

 

第十步:配置可以單點登錄

       Cas服務器都是用spring配置文件配置而成。且使用了cookie技術。在ticketGrantingTicketCookieGenerator.xml文件中,保存了cookie的生成方式及有效時間。

注意,這是在server服務器上的spring配置文件。

打開此文件,修改成以下內容:

 

說明:false是指支持http協議登錄。默認為true,支持https登錄。

        3600中cookie保存在本地的時間,默認為-1即瀏覽器緩存。

   cookiePath是cookie的path設置。

第十一步:單點登錄測試

       修改了上面文件后,即可測試是否可以從一個點的登錄,即可以訪問兩個網站時都顯示先登錄的姓名:

       先輸入http://www.news.com

 

       登錄

 

登錄成功:

 

在地址欄直接輸入:www.bbs.com

可以看到,顯示的是news用戶名,即之前在www.news.com上登錄的用戶名,即實現單點登錄。

 

好了,以上步驟,同學們先自己完成,如果可以配置成功,再進入下一步。

 

 

3、CAS單點登錄流程圖

4、在Eclipse環境中開發SSO

       為了文件書寫代碼,我們需要將cas-server導入到eclipse環境中:

1、建立一個新的web項目:

 

2、將cas-server.war文件中的文件放到Eclipse web項目的相關目錄下:

注意:src下的文件需要到cas-server/WEB-INF/classes下單獨copy.

 

3、重新設置虛擬目錄

將原來的虛擬主機目錄刪除。配置新的虛擬目錄,並通過<Context/>形式指定Eclipse下的項目為項目的根目錄,如下:

 

Host的name保先原地址不變,這樣就沒有必要修改hosts文件了。在tomcat的根目錄下,創建一個casServer目錄,里面什么也不用放即可。

通過<Context path=”/”/>指定根目錄為Eclipse中的項目。

測試是否可以訪問。

 

4、建立客戶端項目-兩個

項目中使用了jstl.jar包,應該放到WEB-INF/lib目錄下。

 

5、配置兩個項目的虛擬目錄

       將原來配置的兩個虛擬主機目錄刪除,然后使用<Context/>配置新的項目。如下:

 

6、准備cas-client.jar包

 

7、聲明受保護的資源並在web.xml中配置

       此處可以完全參考原有客戶端項目的配置。(略)

運行測試進入下一步。

5、在客戶端面獲取用戶的基本信息-name

       1、Cas在登錄成功后會通過socket向客戶端傳遞用戶信息。一般情況下就是用戶名而已。

       2、cas客戶端的request對象是經過包裝的org.jasig.cas.client.util.HttpServletRequestWrapperFilter$CasHttpServletRequestWrapper。

       3、在用戶成功后,客戶端的過慮器,會將用戶的信息封裝成java.security.Principal的子封裝到Assertion后放到Session中。

       關於Assertion對象的結構圖,請見后面的部分。

以下是在客戶端的頁面上如何獲取用戶名信息的多種方式:

 

那么,如何才可以返回更多用戶的信息呢。如返回用戶和用戶名和id?后面的章節將會講到。

 

6、修改服務器的登錄驗證規則

1、使用配置的用戶名和密碼

目前服務器的驗證方式為用戶和密碼相同即為登錄成功。這並不符合我們的業務需求,大多數應用都是通過查詢數據庫獲取用戶名和密碼的。那我們又如何設置從數據庫獲取用戶名和密碼呢?

       為了便於理解,我先將服務器的登錄方式修改為配置的。而后再修改成通過數據庫進行驗證的。

       修改配置文件:/WEB-INF/deployerConfigContext.xml。  -

    在這個配置文件中,保存了多個用戶認證登錄的驗證方式,只要有一種驗證通過即可以登錄成功。

       在<property name= "authenticationHandlers">...屬性內部,通過配置若干的AuthenticationHandler的子類,可以改變登錄認證方式。也可以增加認證方式,只要在用戶登錄時,有一種登錄方式是可行的,即可以登錄成功。

    認證類的繼承關系如下:

 

    默認已經配置了最后一個類即SimpleTestUsernamePasswordAuthenticationHandler。此類驗證用戶名和密碼是否一致。

    Cas為我們提供了可以直接配置用戶名和密碼類:即org.jasig.cas.adaptors.generic.AcceptUsersAuthenticationHandler。此類並沒有包含到默認的服務器代碼中,需要到cas-server/models/中查找名為:cas-server-support-generic-3.4.11.jar的jar文件,並添加到WEB-INF/lib目錄下。

如下圖所示,正是名為generic的jar包。(后面如果需要數據庫連接的,還需要jdbc的jar包)

 

將此jar包放到lib目錄下后,類的層次關系如下:

 

此類接收一個map當作用戶名和密碼的集合列表:

 

在/WEB-INF/deployerConfigContext.xml配置文件中的<property name= "authenticationHandlers">元素中,刪除原來的用戶名與密碼相同的認證,即:

<!-- <bean

              class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" /> -->

然后在相同的位置配置:AcceptUsersAuthenticationHandler類:

配置如下:

 

登錄測試:

 

直接在配置文件中配置用戶名和密碼:可選的加密

<!-- 通用的認證管理器,通過一個文件或是一個map配置用戶名和密碼 -->

              <bean class="org.jasig.cas.adaptors.generic.AcceptUsersAuthenticationHandler">

                  <property name="users">

                     <map>

                         <!-- 1234 MD5:4321 -->

                         <entry key="tom" value="81dc9bdb52d04dc20036dbd8313ed055"/>

                         <!-- 4321: md5 :  -->

                         <entry key="Jack" value="d93591bdf7860e1e4ee2fca799911215"></entry>

                     </map>

                  </property>

                  <!-- 可選的使用md5進行加密 -->

                  <property name="passwordEncoder">

                     <bean class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">

                         <constructor-arg value="MD5"></constructor-arg>

                         <property name="characterEncoding" value="UTF-8"></property>

                     </bean>

                  </property>

              </bean>      

2、自己書寫驗證規則-顛倒的用戶名和密碼

       1、所有的認證類都是AuthenticationHandler的子類,於是我們可以自己開發基於任何規則的驗證。

    2、基於用戶名和密碼的認證,則應該繼承AbstractUsernamePasswordAuthenticationHandler。

    3、代碼如下:

package cn.itcast.handler;

import org.jasig.cas.authentication.handler.AuthenticationException;

import org.jasig.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;

import org.jasig.cas.authentication.principal.UsernamePasswordCredentials;

/**

 * 顛倒的用戶名和密碼即可以登錄

 * @author 傳智播客

 * credentials:憑據

 */

public class MyReverseUsernamePasswordHandler

           extends AbstractUsernamePasswordAuthenticationHandler {

    protected boolean authenticateUsernamePasswordInternal(

           UsernamePasswordCredentials credentials)

           throws AuthenticationException {

       String name = credentials.getUsername();

       String pwd  = credentials.getPassword();

       if(name.equals(new StringBuffer(pwd).reverse().toString())){

           System.err.println("登錄成功.....");

           return true;

       }

       System.err.println("登錄不成功。。。");

       return false;

    }

}

    4、配置如下:

    將原有的配置用戶名和密碼的方式刪除,然后只配置上面的MyReverseUsernamePasswordHandler類:

 

 

 

7、配置使用JDBC的登錄 ,認證句柄: AuthenticationHandler

       這兒需要添加新的包:,需要同時添加mysql-connection.jar文件,以連接數據庫。

       添加此包以后,AuthenticationHandler的層次結構為:

 

通過配置可以,修改CAS的登錄認證方式,默認的登錄方式為用戶名與密碼一致即可以登錄。

       在<property name= "authenticationHandlers">...屬性內部,通過配置若干的AuthenticationHandler的子類,可以改變登錄認證方式。也可以增加認證方式,只要在用戶登錄時,有一種登錄方式是可行的,即可以登錄成功。

    所有的配置方式,在CAS的官方網站上均有詳細的說明。

    修改配置文件:/WEB-INF/deployerConfigContext.xml。

    在這個配置文件中,保存了多個用戶認證登錄的驗證方式,只要有一種驗證通過即可以登錄成功。

    以下是在配置文件中增加用戶名和密碼的登錄的方式,其中設置了MD5對密碼進行加密。默認的加密方式為PlainTextPasswordEncoder.即不加密。

1、使用數據庫指定表名,字段名的登錄認證方式

       A:創建數據庫:

create database sso character set UTF8;

use sso;

create table users(

   id varchar(32),

   name varchar(30),

   pwd varchar(32)

);

insert into users values('U001','Jack','1234');

insert into users values('U002','Rose','4321');

B:在/WEB-INF/deployerConfigContext.xml文件的最下面,創建數據連接:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">

    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>

    <property name="url" value="jdbc:mysql:///sso?characterEncoding=UTF-8"/>

    <property name="username" value="root"/>

    <property name="password" value="1234"/>

 </bean>

C:使用dataSource數據源,因為后面的查詢需要一個數據源的支持:

<!-- 使用數據庫驗證,且數據庫的密碼是經過md5加密的 -->

              <bean class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler">

                  <property name="dataSource" ref="dataSource"/>

                  <property name="tableUsers" value="users"/>

                  <property name="fieldUser" value="name"/>

                  <property name="fieldPassword" value="pwd"/>

                  <property name="passwordEncoder"> <!--可選的加密-->

                     <bean class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">

                         <constructor-arg value="MD5"/>

                         <property name="characterEncoding" value="UTF-8"></property>

                     </bean>

                  </property>

           </bean>

MySql的表結構如下:

 

 

2、使用查詢sql驗證用戶登錄的方式

<!-- 使用sql語句 -->

              <bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">

                  <property name="dataSource" ref="dataSource"/>

                  <property name="sql" value="select pwd from users where name=?"/>

                  <property name="passwordEncoder">

                     <bean class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">

                         <constructor-arg value="MD5"/>

                         <property name="characterEncoding" value="UTF-8"></property>

                     </bean>

                  </property>

              </bean>

表結構如下:

 

其實,上面的MD5驗證,完全可以使用同一個。

 

上面的程序非常簡單,完全可以通過查看源代碼和繼承結構圖來了解認證和登錄方式。且不需要書寫任何Java代碼。

8、返回用戶的更多信息

1、如返回用戶名和用戶ID

 

配置返回用戶的ID而不是用戶名,用戶憑據:Credentials和用戶對象(被代理人) :Principal。

              Credentials :用於定義用戶以什么樣的憑據登錄,普通的憑據為用戶名和密碼憑據。

              Principal :用戶登錄成功以后,使用Credentials轉換成Principal。Principal中包含了用戶的用戶名(默認)及一些其他屬性信息。

Credentials及Pincipal關系的圖示,及Principal內部結構圖示:

 

 

       
   

Principal (用戶對象,被代理人)

                     String getId() – 返回用戶的唯一標識

                Map  getAttributes() 用戶的其他有用信息

 
   
 

 

 

 

 

 

 

Principal的源代碼如下:(注意此Principal不是java.security.Principal類。而是由cas自己定義的一個接口)

package org.jasig.cas.authentication.principal;

import java.io.Serializable;

import java.util.Map;

public interface Principal extends Serializable {

    String getId();

    Map<String, Object> getAttributes();

}

為了可以返回用戶的ID,我們可以修改deployerConfigContext.xml文件中的<property name=’credentialsToPrincipalResolvers’>…中的<list/>元素中的Bean。

此時,為了可以返回用戶的ID,我們必須要手工創建一個CredentialsToPrincipalResolver的子類。

具體代碼如下:

package cn.itcast.pubs;

import javax.sql.DataSource;

import org.jasig.cas.authentication.principal.Credentials;

import org.jasig.cas.authentication.principal.CredentialsToPrincipalResolver;

import org.jasig.cas.authentication.principal.Principal;

import org.jasig.cas.authentication.principal.SimplePrincipal;

import org.jasig.cas.authentication.principal.UsernamePasswordCredentials;

import org.springframework.jdbc.core.JdbcTemplate;

public class MyCredentialsToPrincipalResolver implements CredentialsToPrincipalResolver {

    private DataSource dataSource;//查詢數據庫用

    @Override

    public Principal resolvePrincipal(Credentials credentials) {

       UsernamePasswordCredentials up = //強制類型轉換

              (UsernamePasswordCredentials) credentials;

       String name = up.getUsername();

       String pwd  = up.getPassword();

       String sql = "select id from users2 where u2_name=? and u2_pwd=?"; //查詢id - 一般只根據用戶查詢即可

       String id  = null;

       try{

           id=new JdbcTemplate(getDataSource()).queryForObject(sql, String.class, name,pwd);

           if(id!=null){

              Principal p = new SimplePrincipal(id);//封裝成包含id的Principal對象

              return p;

           }

       }catch(Exception e){

           e.printStackTrace();

       }

       return null;

    }

 

    @Override

    public boolean supports(Credentials credentials) {

       boolean boo =  //判斷是否是用戶和密碼憑據

               UsernamePasswordCredentials.class.isAssignableFrom(credentials.getClass());

       return boo;

    }

    public DataSource getDataSource() {

       return dataSource;

    }

 

    public void setDataSource(DataSource dataSource) {

       this.dataSource = dataSource;

    }

}

(在使用了ID的查詢以后,應該只使用數據庫進行驗證。)

在配置文件中配置如下:

       配置文件為:/WEB-INF/deployerConfigContext.xml

<property name="credentialsToPrincipalResolvers">

           <list>

              <bean class="cn.itcast.pubs.MyCredentialsToPrincipalResolver">

                  <property name="dataSource" ref="dataSource"></property>

              </bean>

              <!--

              <bean

              class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver" />

               -->

              <bean

              class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" />

           </list>

       </property>

測試並運行,此時返回的應該是用戶的id了。

2、返回更多用戶的信息

    服務器,在返回給客戶端用戶信息時,默認只返回用戶名(我們已經修改成ID).但有時我們需要更多的屬性信息,如用戶名。

則應做如下修改:

 

用戶登錄成功以后,CAS使用一個credentialsToPrincipalResolvers將credentials轉成Principal對象,此對象只有一個實現類如下:

 

SimplePrincipal的構造方法接收兩個參數,一個是用戶的id,一個為用戶的其他屬性。用戶的ID默認為用戶登錄時使用的用戶名,前面第4點已經講過如何將用戶的name換成用戶的id返回給客戶端。為了給客戶端返回更多的屬性,我們必須要給Principal的構造方法傳遞第二個參數,它是一個Map<String,Object>類型。

具體代碼如下:

 

上圖通過給SimplePrincipal傳遞第二個構造參數設置了更多的屬性。

但,它並不會馬上顯示到客戶端,如果要顯示到客戶端,因為服務器驗證成功以后,是通過xml形式將結果傳遞給客戶端的,xml的生成由casServiceValidationSuccess.jsp文件負責。它的具體構造應該是以下形式:

<cas:serviceResponse

    xmlns:cas='http://www.yale.edu/tp/cas'>

    <cas:authenticationSuccess>

       <cas:user>U001</cas:user>

       <cas:attributes>

              <cas:pwd>1234</cas:pwd>

              <cas:username>Jack</cas:username>

       </cas:attributes>

    </cas:authenticationSuccess>

</cas:serviceResponse>

在上面的代碼中,cas:attributes元素是我自己添加的,客戶端的的Filter在接收到上述的XML以后,會將css:attributes中的屬性解析出來,放到AttirubtePrincipal的attributes屬性中去(或是放到Asseration的attributes中去,兩個只會放一個)。

默認情況下,將所有屬性信息放到AttributePrincipal中去,所以在客戶端的頁面上可以通過以下方式獲取值:

 

 

所以,組成上面的<cas :attributes>元素中的內容,就成了如何傳遞更多屬性的關鍵,在修改了MyCredentialsToPrincipalResolver的代碼以后,然后還必須要修改casServiceValidationSuccess.jsp的代碼如下:

以下是源代碼:

<%@ page session="false" contentType="text/xml; charset=UTF-8"%><%@ taglib

    prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%@ taglib

    uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%><cas:serviceResponse

    xmlns:cas='http://www.yale.edu/tp/cas'>

    <cas:authenticationSuccess>

    <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>

       <c:if test="${not empty pgtIou}">

           <cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>

       </c:if>

       <c:if test="${fn:length(assertion.chainedAuthentications) > 1}">

           <cas:proxies>

              <c:forEach var="proxy" items="${assertion.chainedAuthentications}"

                  varStatus="loopStatus" begin="0"

                  end="${fn:length(assertion.chainedAuthentications)-2}" step="1">

                  <cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>

              </c:forEach>

           </cas:proxies>

       </c:if>

       <cas:attributes>

           <c:forEach

              items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"

              var="attr">

              <cas:${attr.key}>${attr.value}</cas:${attr.key}>

           </c:forEach>

       </cas:attributes>

    </cas:authenticationSuccess>

</cas:serviceResponse>

 

然后修改deployerConfigContext.xml文件,將最后一個配置項:serviceRegistryDao中的所有屬性全部刪除或是注銷。

這個bean中的RegisteredServiceImpl的ignoreAttributes屬性將決定是否添加attributes屬性內容,默認為false:不添加,只有去掉這個配置,
cas server才會將獲取的用戶的附加屬性添加到認證用的Principal的attributes中去。

 

然后即可以在頁面上通過以下方式獲取用戶的其他屬性:

    <%

       Assertion assertion = AssertionHolder.getAssertion();

       AttributePrincipal ap =  assertion.getPrincipal();   //獲取AttributePrincipal對象,這是客戶端對象

       String name = ap.getName();

       Map<String,Object> att = ap.getAttributes();     //獲取屬性值,為一個Map類型。

       out.print("<br/>"+name);

       out.print("<br/>"+att);

%>

9、處理中文

casServiceValidationSuccess.jsp頁面默認編碼格式為ISO-8859-1,且在表單提交到客戶端頁面時,也使用IS0進行編碼,為了處理中文,可以在頁面上使用URLEncoder對需要傳遞的中文時行UTF-8編碼,然后從客戶端取得數據時,再做URLDecoder解碼:

casServiceValidationSuccess.jsp頁面,真是一個奇怪的頁面,由於cas使用手工解析(沒有使用任何dom解析,硬編碼識別標標簽的開始和標簽的結束)xml的方式解析xml文件,所有,在修改此文件時,一定要加以注意:

 

上圖的紅框部分,必須要緊湊一些,否則會出現解析錯誤。

以下是源代碼:

<%@ page session="false" contentType="text/xml; charset=UTF-8" import="java.net.URLEncoder"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>

    <cas:authenticationSuccess>

    <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>

       <c:if test="${not empty pgtIou}">

           <cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>

       </c:if>

       <c:if test="${fn:length(assertion.chainedAuthentications) > 1}">

           <cas:proxies>

              <c:forEach var="proxy" items="${assertion.chainedAuthentications}"

                  varStatus="loopStatus" begin="0"

                  end="${fn:length(assertion.chainedAuthentications)-2}" step="1">

                  <cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>

              </c:forEach>

           </cas:proxies>

       </c:if>

        <cas:attributes>

           <c:forEach items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}" var="attr">

                  <c:set var="val" value="${attr.value}"/>

                  <cas:${attr.key}><%=URLEncoder.encode((String)pageContext.getAttribute("val"),"UTF-8")%></cas:${attr.key}>

           </c:forEach>

       </cas:attributes>

    </cas:authenticationSuccess>

</cas:serviceResponse>

 

經過編碼以后的XML數據如下:

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>

    <cas:authenticationSuccess>

       <cas:user>U003</cas:user>

        <cas:attributes>

                  <cas:pwd>1111</cas:pwd>

                  <cas:username>%E5%BC%A0%E4%B8%89</cas:username>

       </cas:attributes>

    </cas:authenticationSuccess>

</cas:serviceResponse>

 

可見,對中文進行了UTF-8編碼。

 

在客戶端使用URLDecoder進行解碼:

以下:

    <%

       Assertion assertion = AssertionHolder.getAssertion();

       AttributePrincipal ap =  assertion.getPrincipal();

        String id = ap.getName();

       Map<String,Object> att = ap.getAttributes();

       out.print("<br/>"+id);

       out.print("<br/>"+att);

       String name = URLDecoder.decode(""+att.get("username"), "UTF-8");

       out.println("<br/>"+name);

    %>

顯示效果如下:

 

 

10、通過監聽器將從服務器上返回的數據封裝成自己的對象放到Session中去

   

    從服務器返回信息成功后,將以_const_cas_assertion_為key將Assertion對象放到Session中去。知道了這一點,即可以監聽Session的屬性添加事件:

源代碼:

package cn.itcast.listener;

import java.net.URLDecoder;

import java.util.Map;

import javax.servlet.http.HttpSessionAttributeListener;

import javax.servlet.http.HttpSessionBindingEvent;

import org.jasig.cas.client.util.AbstractCasFilter;

import org.jasig.cas.client.validation.Assertion;

/**

 * 通過監聽器將從服務器上返回的信息放到Session中

 * @author 傳智播客

 */

public class AssertionListener implements HttpSessionAttributeListener {

    public void attributeAdded(HttpSessionBindingEvent se) {

       if (se.getName().equals(AbstractCasFilter.CONST_CAS_ASSERTION)) {

           System.err.println("添加了某屬性.....");

           //不可以使用工具類,只可以使用session獲取對象

           Assertion ass = (Assertion)se.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);

           Map<String, Object> user = ass.getPrincipal().getAttributes();

           se.getSession().setAttribute("user", user);

           try {

              for (String key : user.keySet()) {

                  //因為服務器上轉碼,所以此外解碼

                  user.put(key,

                         URLDecoder.decode("" + user.get(key), "UTF-8"));

              }

           } catch (Exception e) {

              e.printStackTrace();

           }

       }

    }

    public void attributeRemoved(HttpSessionBindingEvent se) {

    }

    public void attributeReplaced(HttpSessionBindingEvent se) {

    }

}

配置到客戶端的web.xml中去:

<listener>

       <listener-class>cn.itcast.listener.AssertionListener</listener-class>

</listener>

然后即可以在客戶端的頁面上通過以下方式獲取值:

 

 

11、認定對象:Assertion。

       里面又包含了Principal對象及創建時間、過期時間、和用戶的ID或是名稱。

       首先,用戶的ID或是用戶名,在CAS的客戶端程序中,可以通過request.getRemoteUser()的方式獲取得到。Spring是通過在過慮器中,包裝HttpRequest的方式實現的,其實,仍然是從Assertion中獲取得到的數據。

       在CAS服務端,用戶注冊成功以后,CAS服務器端是通過POST方式給客戶端傳遞一個XML數據的方式獲取得到數據的。

Assertion的源代碼如下:

public interface Assertion extends Serializable {

 

    /**

     * The date from which the assertion is valid from,有效時間從什么時間開始

     */

    Date getValidFromDate();

 

    /**

     * The date which the assertion is valid until,在效時間,到什么時間結束。

     */

    Date getValidUntilDate();

 

    /**

     * The key/value pairs associated with this assertion,一組屬性值

     */

    Map getAttributes();

 

    /**

     * The principal for which this assertion is valid,被代理的對象,此對象中,又包含了一個ID和一組屬性值

     */

    AttributePrincipal getPrincipal();

}

通過上面的源代碼,可以知道Assertion和Principal的關系如下:

 
   

 

 

 

 

 

 

 

 

從Assertion中獲取信息,可以查看示例客戶端的getpt.jsp頁面上的代碼:

方法1、從Session中獲取Assertion對象:

Assertion assertion1 = (Assertion) session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);

方法2、通過AssertionHolder的靜態方法獲取Assertion

Assertion assertion2 = AssertionHolder.getAssertion();

 

12、單點注銷

在一個客戶端注銷以后,應該將其他所有站點的登錄Session全部注銷:

 1、發出注銷申請

 
   

 

 

 

 

 

 

 

 

 

 

 

 

在CAS的客戶端,CAS使用一個Map維護了所有登錄用戶的Session和TG(憑據)。CAS服務器將依次將客戶端發送請求,被CAS客戶端的注銷過慮器攔截到,注銷過慮器完成客戶端Session的注銷工作。

所以,為了實現單點注銷,必須要將客戶端的單點注銷過慮器也打開:

       找到cas客戶端web.xml文件,啟用以下代碼:

       <!-- Sign out not yet implemented,單點注銷 -->

       <filter>

              <filter-name>CAS Single Sign Out Filter</filter-name>

              <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>

       </filter>

       <filter-mapping>

              <filter-name>CAS Single Sign Out Filter</filter-name>

              <url-pattern>/*</url-pattern>

       </filter-mapping>

只要打開上面的過慮器,即可以實現單點注銷。

 

測試:在CAS服務器端輸入 :http://localhost/casServer/logout即可以完成單點注銷。

注意,應該是服務器的地址,即http://服務器地址/logout

 

單點注銷:

TicketGrantingTicketImpl.logOutOfService方法將會獲取所有Ticket,通過遍歷,然后調用下面的方法。

AbstractWebApplicationService. logOutOfService方法將會出送一段XML文本給每一個客戶端。

 

以下是TicketGrantingTicketImpl的片段代碼:

private void logOutOfServices() {

        for (final Entry<String, Service> entry : this.services.entrySet()) {//遍歷所有注冊過的客戶端

 

            if (!entry.getValue().logOutOfService(entry.getKey())) {  //調用注銷服務

                LOG.warn("Logout message not sent to [" + entry.getValue().getId() + "]; Continuing processing...");  

            }

        }

}

以下是AbstractWebApplicationService的片段代碼:

public synchronized boolean logOutOfService(final String sessionIdentifier) {

        if (this.loggedOutAlready) {

            return true;

        }

 

        LOG.debug("Sending logout request for: " + getId());

       //組織一段XML文本

        final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""

            + GENERATOR.getNewTicketId("LR")

            + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()

            + "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"

            + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";

       

        this.loggedOutAlready = true;

       

        if (this.httpClient != null) {

            return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest, true);  //發送請求

        }

       

        return false;

}

 

13、CAS單點登錄流程圖

 

 

 

 

 

 

 

 

 

14、完成自己的登錄頁面

在開始之前,讀者必須了解Spring的MVC框架和Spring WebFlow(頁面流)。

    然后將完整的cas-server.war包,部署到MyEclipe工作區中,以便於更新和修改。為此,應該將原來classes目錄下的cas的原類打包,放到lib目錄下。

修改以下頁面,可以實現自定義登錄頁面的需求。具體jsp的寫法略。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM