文档章节

SSO:单点登录

天池番薯
 天池番薯
发布于 2014/10/10 16:15
字数 6971
阅读 216
收藏 1

SSO:单点登录

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则同时实现闪单点登录,否则不加实现。

 

2SSO示例程序配置

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的根目录下,分别建立三个目录,即serverbbsnews

在三个目录下,分别都建立一个ROOT(ROOTtomcat的主默认主页目录)文件夹。

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登录。

 3600cookie保存在本地的时间,默认为-1即浏览器缓存。

   cookiePathcookiepath设置。

第十一步:单点登录测试

修改了上面文件后,即可测试是否可以从一个点的登录,即可以访问两个网站时都显示先登录的姓名:

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

 

登录

 

登录成功:

 

在地址栏直接输入:www.bbs.com

可以看到,显示的是news用户名,即之前在www.news.com上登录的用户名,即实现单点登录。

 

好了,以上步骤,同学们先自己完成,如果可以配置成功,再进入下一步。

 

 

3CAS单点登录流程图

4、在Eclipse环境中开发SSO

为了文件书写代码,我们需要将cas-server导入到eclipse环境中:

1、建立一个新的web项目:

 

2、将cas-server.war文件中的文件放到Eclipse web项目的相关目录下:

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

 

3、重新设置虚拟目录

将原来的虚拟主机目录删除。配置新的虚拟目录,并通过<Context/>形式指定Eclipse下的项目为项目的根目录,如下:

 

Hostname保先原地址不变,这样就没有必要修改hosts文件了。在tomcat的根目录下,创建一个casServer目录,里面什么也不用放即可。

通过<Context path=//>指定根目录为Eclipse中的项目。

测试是否可以访问。

 

4、建立客户端项目-两个

项目中使用了jstl.jar包,应该放到WEB-INF/lib目录下。

 

5、配置两个项目的虚拟目录

将原来配置的两个虚拟主机目录删除,然后使用<Context/>配置新的项目。如下:

6、准备cas-client.jar

 

7、声明受保护的资源并在web.xml中配置

此处可以完全参考原有客户端项目的配置。()

运行测试进入下一步。

5、在客户端面获取用户的基本信息-name

1Cas在登录成功后会通过socket向客户端传递用户信息。一般情况下就是用户名而已。

2cas客户端的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.jarjar文件,并添加到WEB-INF/lib目录下。

如下图所示,正是名为genericjar包。(后面如果需要数据库连接的,还需要jdbcjar)

 

将此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转换成PrincipalPrincipal中包含了用户的用户名(默认)及一些其他属性信息。

CredentialsPincipal关系的图示,及Principal内部结构图示:

 

 

 

 

 

 

 

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);//封装成包含idPrincipal对象

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使用一个credentialsToPrincipalResolverscredentials转成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以后,会将cssattributes中的属性解析出来,放到AttirubtePrincipalattributes属性中去(或是放到Asserationattributes中去,两个只会放一个)。

默认情况下,将所有属性信息放到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中的RegisteredServiceImplignoreAttributes属性将决定是否添加attributes属性内容,默认为false:不添加,只有去掉这个配置,
cas server才会将获取的用户的附加属性添加到认证用的Principalattributes中去

 

然后即可以在页面上通过以下方式获取用户的其他属性:

<%

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();

}

通过上面的源代码,可以知道AssertionPrincipal的关系如下:

 

 

 

 

 

 

 

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维护了所有登录用户的SessionTG(凭据)。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;

}

 

13CAS单点登录流程图

 

 

 

 

 

 

 

 

 

14、完成自己的登录页面

在开始之前,读者必须了解SpringMVC框架和Spring WebFlow(页面流)

然后将完整的cas-server.war包,部署到MyEclipe工作区中,以便于更新和修改。为此,应该将原来classes目录下的cas的原类打包,放到lib目录下。

修改以下页面,可以实现自定义登录页面的需求。具体jsp的写法略。

 


© 著作权归作者所有

共有 人打赏支持
天池番薯
粉丝 40
博文 59
码字总数 67966
作品 0
天津
程序员
私信 提问
加载中

评论(1)

0x0bject
0x0bject
Good one.Thx for sharing!
单点登录(SSO)看这一篇就够了

背景 在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。 但随着企业的发展,用到的系统随之增多,运营人员在操作...

小忽悠
09/06
0
0
分布式系统框架Spring+Redis+SSO

课程介绍 该课程以实战方式实现一套经典的分布式系统架构; 讲解如何进行系统拆分架构: 1、传统ssm框架搭建、 2、独立restful服务工程搭建、 3、服务接口底层访问、 4、redis实现业务缓存、...

小红牛
07/18
0
0
单点登录原理与简单实现

一、单系统登录机制 1、http无状态协议 web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过...

余平的余_余平的平
2017/11/03
0
0
分布式单点登录框架 - xxl-sso

分布式单点登录框架 XXL-SSO XXL-SSO 是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码,...

许雪里
04/04
0
0
前端需要了解的 SSO 与 CAS 知识

不管是什么公司,只要产品数量大于一个,那么单点登录势必是绕不过去的一个问题。作为前端程序员,我们对其虽然接触不多,但适当的了解还是必要的。本文就来谈谈单点登录相关的问题。 前置知...

丁香园F2E
2017/11/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

缓存

并发情况下发生的缓存问题: 缓存一致性: 缓存穿透:是指在高并发场景下,如果某一个key被高并发的访问,缓存没有命中,出于容错性的考虑,会去数据库获取数据,从而导致大量请求访问数据库...

wuyiyi
6分钟前
1
0
eclipse 和idea 快捷键对照

分类 功能点 Eclipse快捷键 IDEA快捷键 搜索 搜索文本 Ctrl + F Ctrl + F Ctrl + R 查找替换 Alt + P/A 逐个/全部替换 Alt + F3 查找当前选中词 继续搜索 Ctrl + K 向前 Ctrl + Shift + K 向...

郭恩洲_OSC博客
7分钟前
1
0
PowerHA IP 替换方式与IP 别名方式的区别

IPAT via replacement (IP替换) The service IP label replaces the boot IP address on the interface. The heartbeat IP alias address remains. IPAT via aliasing (IP别名) The servic......

突突突酱
11分钟前
2
0
Kafka 几个重要的配置总结

注意:配置基于Kafka 0.8.2.1 broker配置 #非负整数,用于唯一标识broker broker.id 0 #kafka持久化数据存储的路径,可以指定多个,以逗号分隔 log.dirs /tmp/kafka-logs #broker接收连接请求...

hblt-j
11分钟前
1
0
开发函数计算的正确姿势 —— 排查超时问题

写不尽的 code,查不完的 bug 通常我们写 bug,哦,不对,写代码时总不会一帆风顺,往往各种 bug 充斥其中,即使测试有较高的代码覆盖率往往也会有漏网之鱼。能写出一些比较隐蔽或者看起来像...

阿里云官方博客
16分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部