博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
yale-cas服务器端深度定制
阅读量:4047 次
发布时间:2019-05-25

本文共 17745 字,大约阅读时间需要 59 分钟。

    单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    耶鲁大学(yale)开发的单点登录(Single Sign On)系统称为CAS(Central Authentication Service)被设计成一个独立的Web应用程序(cas.war)。

    CAS在2004年12月成为Jasig项目,所以也叫JA-SIG CAS。

 

    本文中服务器版本基于3.5.2版本,对应的客户端版本为3.2.1;

    目前客户端已经出了3.3.0,但是官方服务器端项目中POM中使用的依赖cas-client-core仍然是3.2.1版本。

 

常见的Server端配置方式

1.简单搭建Server比较简单见;

2.cas提供的服务器只有简单认证的功能,所以和数据库进行交换需要。

    由于这种方式,包括如何添加DataSource网上的说明比较多,在这里就不再赘述了。值得注意的是service最后必须要部署在有ssl认证的服务器上,可以参考我另外的帖子注意文章中设置本机域名的原因和方式。

 

对Server进行深度定制

这里主要说明的是如何对Server端进行深度定制,要实现的功能有:

1.自定义登陆参数;2.不通过cas-server-support-jdbc,添加数据库操作;3.使用CXF实现JAX-RS技术,添加WebService接口。用于账户的维护功能;4.通过flyway自动创建账户表。

 

一. 导入项目源码

上述通过Maven导入war包的项目有两点缺陷,一不方便通过Maven确定依赖,及不方便对项目的管理;二不方便进行测试。所以采用从官网上下载源码,再进行修改的方式。

官网上下载地址为:http://www.jasig.org/cas/download,我在这里下的最新的CAS Server 3.5.2 Release的ZIP版本。下载下来是一个77.5M的ZIP文件。

 

因为该地址需要用goagent过去,我将 \cas-server-3.5.2\modules 目录下打包好的文件清空,从新打压缩包上传上来,以供参考。

 

我们将cas-server-webapp子项目提取出来,通过修改POM文件,将该项目修改为独立项目。

说明:

    1.依赖版本参考根目录下parent pom;

    2.或源cas-server-webapp通过maven导出的依赖树;

    3.添加各种Log桥接到slf4j,并检查依赖树排除log4j和commons-log,添加logback的配置文件,移除/webapp/WEB-INF/classes/log4j.xml和/webapp/WEB-INF/spring-configuration/log4jConfiguration.xml

 

 

二.自定义登陆参数

注意:要完成自定义登陆参数,及客户端返回参数的自定义。需要修改如下:

两个JSP

/webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp

/webapp/WEB-INF/view/jsp/protocol/2.0/casServiceValidationSuccess.jsp

两个配置文件

/webapp/WEB-INF/deployerConfigContext.xml

/webapp/WEB-INF/login-webflow.xml

三个properties文件中添加内容

/webapp/WEB-INF/classes/messages_en.properties

/webapp/WEB-INF/classes/messages_zh_CN.properties

/webapp/WEB-INF/cas.properties

 

1.修改登陆页面casLoginView.jsp

通过/my-cas-server/src/main/webapp/WEB-INF/classes/default_views.properties,可以看到casLoginView.url指向的是casLoginView.jsp,

修改class="row fl-controls-left"的DIV中username改为loginname(我这里定义登陆名用logginname)

${sessionScope.openIdLocalId}
 

在该JSP中class="row fl-controls-left"的DIV和class="row check"的DIV之间,添加如下代码

 

2.用于将参数返回客户端的casServiceValidationSuccess.jsp,在<cas:user>标签下加入如下代码,

为了传递中文KEY和VALUE的参数

1.需要将<%@ page session="false" %>修改为<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

2.如果部署的是tomcat服务器,修改/bin/catalina.bat文件,在下面第一行和第三行中间加入第二行,重点是-Dfile.encoding=UTF-8

rem ----- Execute The Requested Command --------------------------------------- set JAVA_OPTS=%JAVA_OPTS% -server -Dfile.encoding=UTF-8 -Xms512m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256mecho Using CATALINA_BASE:   "%CATALINA_BASE%"

 

<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
${fn:escapeXml(attr.value)}
 

 

3.在properties中添加字段为casLoginView.jsp添加国际化的显示添加内容

添加相关提示screen.welcome.label.custom和screen.welcome.label.custom.accesskey在

比如

messages_zh_CN.properties中添加

#Custom By sgq0085screen.welcome.label.custom=\u81ea\u5b9a\u4e49:screen.welcome.label.custom.accesskey=crequired.logingname=\u5fc5\u987b\u5f55\u5165\u7528\u6237\u540d\u3002required.custom=\u81EA\u5B9A\u4E49\u5B57\u6BB
messages_en.properties文件中添加
#Custom By sgq0085screen.welcome.label.custom=Custom:screen.welcome.label.custom.accesskey=c

或者使用JQuery Validate自己添加验证,以及登陆页面的其他样式等。

 

4.cas.properties中修改如下字段,用于单点登出。实际上数据源相关信息也可以放在这个文件中。我们在后面添加

 

cas.logout.followServiceRedirects=true
 

 

5.login-webflow.xml中修改如下内容

 

 

 

 

2.修改配置文件deployerConfigContext.xml

修改的目的:1.支持页面新增的输入参数;2.实现自定义的认证;3.实现自定义客户端认证后返回的参数;

 

五个实现类

(1)com.gqshao.cas.adaptors.MyAuthenticationHandler 中authenticateLoginnamePasswordInternal方法包含认证的实际位置,后续添加数据库持久之后,在这里完善认证

 

package com.gqshao.cas.adaptors;import javax.validation.constraints.NotNull;import org.jasig.cas.authentication.handler.AuthenticationException;import org.jasig.cas.authentication.handler.AuthenticationHandler;import org.jasig.cas.authentication.principal.Credentials;import com.gqshao.cas.principal.MyCredentials;public class MyAuthenticationHandler implements AuthenticationHandler {    private static final Class
DEFAULT_CLASS = MyCredentials.class; @NotNull private Class
classToSupport = DEFAULT_CLASS; private boolean supportSubClasses = true; public boolean supports(Credentials credentials) { return credentials != null && (this.classToSupport.equals(credentials.getClass()) || (this.classToSupport .isAssignableFrom(credentials.getClass())) && this.supportSubClasses); } public boolean authenticate(Credentials credentials) throws AuthenticationException { return authenticateLoginnamePasswordInternal((MyCredentials) credentials); } public final void setClassToSupport(final Class
classToSupport) { this.classToSupport = classToSupport; } /** * 认证的实际位置 * @param credentials * @return */ private boolean authenticateLoginnamePasswordInternal(MyCredentials credentials) { MyCredentials c = (MyCredentials) credentials; if (c.getLoginname().equals(c.getPassword())) { credentials.setId("select_id_from_table"); return true; } return false; }}

 

(2)com.gqshao.cas.principal.MyCredentials 与登陆参数一一对应

package com.gqshao.cas.principal;import javax.validation.constraints.NotNull;import javax.validation.constraints.Size;import org.jasig.cas.authentication.principal.Credentials;public class MyCredentials implements Credentials {    private static final long serialVersionUID = -7008439202352047770L;        private String id;    /** The username. */    @NotNull    @Size(min=1,message = "required.loginname")    private String loginname;    /** The password. */    @NotNull    @Size(min=1, message = "required.password")    private String password;        /** The Custom*/        @NotNull    @Size(min = 1, message = "required.custom")    private String custom;    public String getId() {        return id;    }    public void setId(String id) {        this.id = id;    }    public String getLoginname() {        return loginname;    }    public void setLoginname(String loginname) {        this.loginname = loginname;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }    public String getCustom() {        return custom;    }    public void setCustom(String custom) {        this.custom = custom;    }    public String toString() {        return "[loginname: " + this.loginname + "]";    }    @Override    public boolean equals(final Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        MyCredentials that = (MyCredentials) o;        if (password != null ? !password.equals(that.password) : that.password != null) return false;        if (loginname != null ? !loginname.equals(that.loginname) : that.loginname != null) return false;        if (custom != null ? !custom.equals(that.custom) : that.custom != null) return false;        return true;    }    @Override    public int hashCode() {        int result = loginname != null ? loginname.hashCode() : 0;        result = 31 * result + (password != null ? password.hashCode() : 0);        return result;    }}

 

(3).MyCredentialsToPrincipalResolver

package com.gqshao.cas.principal;import org.jasig.cas.authentication.principal.AbstractPersonDirectoryCredentialsToPrincipalResolver;import org.jasig.cas.authentication.principal.Credentials;public class MyCredentialsToPrincipalResolver extends AbstractPersonDirectoryCredentialsToPrincipalResolver {    public boolean supports(Credentials credentials) {        return credentials != null                && MyCredentials.class.isAssignableFrom(credentials                    .getClass());    }        @Override    protected String extractPrincipalId(Credentials credentials) {                final MyCredentials myCredentials = (MyCredentials) credentials;        return myCredentials.getId();    }}

(4)com.gqshao.cas.support.MyPersonImpl 将返回客户端参数封装成POJO

package com.gqshao.cas.support;import java.util.List;import java.util.Map;import org.jasig.services.persondir.IPersonAttributes;import org.jasig.services.persondir.support.BasePersonImpl;public class MyPersonImpl extends BasePersonImpl {    private static final long serialVersionUID = -6468711518798238482L;    public static final String DEFAULT_NAME_ATTRIBUTE = "loginname";    private final String nameAttribute;    public MyPersonImpl(Map
> attributes) { super(attributes); this.nameAttribute = DEFAULT_NAME_ATTRIBUTE; } public MyPersonImpl(String nameAttribute, Map
> attributes) { super(attributes); this.nameAttribute = nameAttribute; } public MyPersonImpl(IPersonAttributes personAttributes) { this(personAttributes.getName(), personAttributes.getAttributes()); } public String getName() { final Object attributeValue = this.getAttributeValue(this.nameAttribute); if (attributeValue == null) { return null; } return attributeValue.toString(); }}

 

(5)com.gqshao.cas.support.MyStubPersonAttributeDao 返回客户端的实际位置,这里对中文等进行测试。后续自定义返回有意义的参数。

package com.gqshao.cas.support;import java.util.Collections;import java.util.List;import java.util.Map;import java.util.Set;import org.jasig.services.persondir.IPersonAttributes;import org.jasig.services.persondir.support.StubPersonAttributeDao;import com.google.common.collect.Lists;import com.google.common.collect.Maps;public class MyStubPersonAttributeDao extends StubPersonAttributeDao {        /**     * 服务器返回值实际封装位置     */    @Override    public IPersonAttributes getPerson(String uid) {        Map
> attributes = Maps.newHashMap(); attributes.put("uid", Collections.singletonList((Object) uid)); attributes.put("custom", Collections.singletonList((Object) "custom_result")); List
lnarray = Lists.newArrayList(); lnarray.add("admin"); attributes.put("loginname", lnarray); attributes.put("中文KEY", Collections.singletonList((Object) "中文值")); for (String key : attributes.keySet()) { System.out.println(key + " : " + attributes.get(key)); } MyPersonImpl person = new MyPersonImpl(attributes); return person; }; @Override public Set
getPeopleWithMultivaluedAttributes(Map
> query) { return super.getPeopleWithMultivaluedAttributes(query); }}

 

其实到此为止,CAS服务器已经简单实现完成。可以独立运行了,通过MyAuthenticationHandler.authenticateLoginnamePasswordInternal(Credentials)这里实现自己的认证就可以了,现在只要是用户名和密码相同。即可实现认证。在实际中肯定不是这样。下面我们同时添加数据源、Flyway建库和CXF开放web service接口。

三. 功能扩展和完善

1.POM文件中添加依赖,这里使用H2做演示,用Spring JdbcTemplate做数据库做数据库持久层

需要引入

cxf 2.7.7

jackson 2.3.0
tomcat-jdbc 7.0.42

flyway 2.3.1

 

2.添加数据库配置

上面提到过,数据源配置可以配置到/webapp/WEB-INF/cas.properties中,这里采用数据库连接池选用tomcat-jdbc,数据库H2,并用log4jdbc展示SQL,所以内容如下

jdbc.driver=net.sf.log4jdbc.DriverSpyjdbc.url=jdbc:log4jdbc:h2:file:~/.h2/cas;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=TRUE;jdbc.username=sajdbc.password=jdbc.pool.maxIdle=10jdbc.pool.maxActive=50

 

3.添加CXF和Datasource的配置文件和flyway的配置

首先在web.xml中加入cxf的监听

CXFServlet
org.apache.cxf.transport.servlet.CXFServlet
CXFServlet
/cxf/*

然后添加数据源和CXF的配置

/src/main/resources/datasource/applicationContext-datasource.xml

维护账户信息用数据库连接信息

 

 

/src/main/resources/webservice/applicationContext-jaxrs-server.xml

Apache CXF的Restful Web Service配置

 

数据库初始化脚本/src/main/resources/dbmigrate/V0_0_1__account_schema.sql

---- 创建报告表-------create table rbac_account(   ID                   CHAR(32)                        not null,   login_name           VARCHAR2(50),   password             VARCHAR2(50),   salt                 VARCHAR2(50),   custom               VARCHAR2(50),   constraint pk_rbac_account primary key (ID));-- Add comments to the table comment on table rbac_account  is '账户表';-- Add comments to the columns comment on column rbac_account.id  is '主键ID';comment on column rbac_account.login_name  is '登录名';comment on column rbac_account.password  is '密码';comment on column rbac_account.salt  is '盐';comment on column rbac_account.custom  is '自定义字段';insert into rbac_account  (id, login_name, password, salt,custom)values  ('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345','admin','8b988cb8b23f880b96575b9fb8b4792b214b3152','fe3d7e30d8e116e2','scope1');

 

在/webapp/WEB-INF/deployerConfigContext.xml最后引入上述两个配置

 

3.实现类

这里基本上就完成了,实现类的这里把基于Jax-rx的web service接口展示一下

package com.gqshao.account.jaxrs;import javax.ws.rs.GET;import javax.ws.rs.POST;import javax.ws.rs.Path;import javax.ws.rs.PathParam;import javax.ws.rs.Produces;import javax.ws.rs.WebApplicationException;import javax.ws.rs.core.Response;import javax.ws.rs.core.Response.Status;import com.gqshao.account.domain.Account;import com.gqshao.account.domain.ResultDTO;import com.gqshao.account.service.AccountService;import com.gqshao.common.web.MediaTypes;/** * cxf在web.xml侦听/cxf, 在applicationContext.xml里侦听/jaxrx,完整访问路径为 /cxf/jaxrs/account/{loginName}/{custom} */@Path("/account")public class AccountJaxRsService {    private AccountService accountService;    /**     * 判断用户是否存在     * {@link http://localhost:8080/cas/cxf/jaxrs/account/admin/admin}     */    @GET    @Path("/{loginName}/{custom}")    @Produces(MediaTypes.JSON_UTF_8)    public ResultDTO query(@PathParam("loginName") String loginName, @PathParam("custom") String custom) {        Account account = accountService.findByLoginNameAndCustom(loginName, custom);        if (account == null) {            String message = "用户不存在(id:" + loginName + ")";            throw buildException(Status.NOT_FOUND, message);        }        ResultDTO res = new ResultDTO();        res.setSuccess(true);        res.setMsg("用户存在(id:" + loginName + ")");        return res;    }    @POST    @Path("/{loginName}/{password}/{custom}")    @Produces(MediaTypes.JSON_UTF_8)    public ResultDTO save(@PathParam("loginName") String loginName, @PathParam("password") String password,            @PathParam("custom") String custom) {        try {            boolean isSuccess = accountService.save(loginName, password, custom);            ResultDTO res = new ResultDTO();            res.setSuccess(isSuccess);            if (isSuccess) {                res.setMsg("创建" + loginName + "成功");            } else {                res.setMsg("创建" + loginName + "失败");            }            return res;        } catch (Exception e) {            String message = "创建" + loginName + "失败";            throw buildException(Status.EXPECTATION_FAILED, message);        }    }    public void setAccountService(AccountService accountService) {        this.accountService = accountService;    }    private WebApplicationException buildException(Status status, String message) {        return new WebApplicationException(Response.status(status).entity(message)                .type(MediaTypes.TEXT_PLAIN_UTF_8).build());    }}

4.完善认证

还记得com.gqshao.cas.adaptors.MyAuthenticationHandler中的authenticateLoginnamePasswordInternal方法么,可以在该类中引入账户的相关service,完成实际认证工作。

/**     * 认证的实际位置     * @param credentials     * @return     */    private boolean authenticateLoginnamePasswordInternal(MyCredentials credentials) {        credentials.setCustom(null);        BeanValidators.validateWithException(validator, credentials);        Account account = accountService.authenticate(credentials);        if (account == null) {            return false;        }        credentials.setId(account.getId());        return true;    }

 这里面用到了JSR303去判断credentials属性是否正确,因为前端会做验证,所以这里验证不通过并且抛出异常的都是非正常情况,不用考虑用户界面的友好型。

 

Restful风格的webservice 可以通过进行测试soapUI

转载请注明 :

至此,客户端定制完成。下一篇为服务器端接收参数,并与shiro进行整合。

再次提醒,服务器端应该部署在SSL认证的服务器上

 

你可能感兴趣的文章
令人神清气爽的shooow
查看>>
指导教师的shooow
查看>>
leetcode面试题01.06.字符串压缩,超出时间限制,样例通过31/32
查看>>
机器学习实战、第二章KNN算法详解、AttributeError: ‘dict‘ object has no attribute ‘iteritems‘
查看>>
leetcode 535 TinyURL 的加密与解密 暴力 年轻人不讲武德—shooter7的博客
查看>>
课程设计(毕业设计)—基于机器学习KNN算法手写数字识别系统—计算机专业课程设计(毕业设计)
查看>>
leetcode1792第232场周赛第三题,以及二维数组根据某一列进行排序——优先队列
查看>>
学生网上选课管理系统的设计与实现—计算机类专业课程设计(毕业设计)
查看>>
新建动态web工程项目红叉报错,以及Could not publish server configuration for Tomcat v9.0 Server at localhost.
查看>>
机器学习SVM的车牌识别系统—计算机专业课程设计(毕业设计)
查看>>
leetcode 80. 删除有序数组中的重复项 II
查看>>
课程设计(毕业设计)—学生宿舍管理系统—计算机类专业
查看>>
毕业设计(课程设计)—SpringBoot网上订餐系统的设计与实现—计算机类专业课程设计(毕业设计)
查看>>
毕业设计(课程设计)—个人博客系统(微博)的设计与实现—计算机类专业课程设计(毕业设计)
查看>>
牛客(中兴捧月)—B-切绳子
查看>>
剑指Offer 13.机器人的运动范围——DFS和BFS
查看>>
Java中GUI编程总结—AWT中的Frame容器、panel面板、布局管理
查看>>
剑指offer12.矩阵中的路径—DFS+剪枝
查看>>
Java中GUI编程总结—事件监听、TextField监听、画笔、(鼠标、窗口、键盘)监听
查看>>
Java中GUI编程总结—Swing(窗口、面板、弹窗、标签、按钮、列表、文本框)
查看>>