介绍
CAS(Center Authentication Service)是耶鲁大学研究的一款开源的单点登录项目,主要为web项目提供单点登录实现,属于Web SSO。
CAS登录等系统分为CAS Server和CAS Client
CAS应用登录介绍
- 用户访问请求资源,浏览器如果存在TGC(Ticket Granted Cookie),则会携带TGC访问
- 客户端后台会判断会话中是否有_const_cas_assertion_,如果有,代表已经认证,直接返回认证通过,如果没有,就重定向到Cas Server
- CAS Server会对请求做认证,验证是否有TGC(Ticket Granted Cookie),如果没有则跳转到登录界面,用户输入用户名密码。认证通过后客户端会缓存一个TGC,并且后台会生成TGT,用于绑定访问信息。并且会生成唯一的票据ST(ST只能使用一次,使用完后就失效)
- 如果存在TGC,则服务端会去找对应的TGT,如果存在,则直接生成ST
- 重定向到跳转地址,在地址后面会跟上生成的ST
- 客户端收到访问请求以后,判断如果是存在ST票据,则重新向服务端请求,验证ST的有效性,如果有效则从请求中获取相关的用户信息,并记录到session._const_cas_assertion_中,就完成了登录过程
注意要点:
- TGT(Ticket Granded Ticket),就是存储认证凭据的Cookie,有TGT说明已经通过认证
- ST(Service Ticket),是由CAS认证中心生成的一个唯一的不可伪装的票据,用于认证的
- 没登录过的或者TGT失效的,访问时候也跳转到认证中心,发现没有TGT,说明没有通过认证,直接重定向登录页面,输入账号密码后,再次重定向到认证中心,验证通过后,生成ST,返回客户端保存到TGC
- 登录过的而且TGT没有失效的,直接带着去认证中心认证,认证中心发现有TGT,重定向到客户端,并且带上ST,客户端再带ST去认证中心验证
CAS应用登出介绍
- CAS服务端注销,清除TGT和TGC
- 获取TGT对应所有客户端URL
- 发送通知
- 客户端监听到服务端发出的注销请求,客户端执行注销过程,清理Session,系统会话结束
注意要点:
- 注销请求一般都是访问 http://xxx/cas/logout 的时候就会触发
- 特别要注意客户端的注销监听,服务器发送注销请求的时候要能接受到,不然会注销失败。
CAS-Client原理介绍
客户端只要引入cas-client包就行,具体下面介绍的类就是client里面关于认证,票据认证相关的代码。
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>${cas.client.core.version}</version>
</dependency>
Bean处理方式
CAS配置类,用于配置CAS相关参数
@Configuration
@Conditional(CasCondition.class)
@Lazy(false)
public class CasConfig {
static final Logger logger = LoggerFactory.getLogger(CasConfig.class);
@Bean
CustomCasFilter casFilter() {
String adminPath = Global.getConfig(KeyConsts.ADMIN_PATH) == null ? DefaultValueConsts.DEFAULT_ADMIN_PATH : Global.getConfig(KeyConsts.ADMIN_PATH);
CustomCasFilter customCasFilter = new CustomCasFilter();
customCasFilter.setFailureUrl(adminPath + "/error");
return customCasFilter;
}
//不能和ShiroConfig中的logoutFilter()同名,否则运行时会调用该方法,造成循环依赖
@Bean
LogoutFilter casLogoutFilter() {
String casServerUrl = Global.getConfig(KeyConsts.CAS_SERVER_URL);
String casProjectUrl = Global.getConfig(KeyConsts.CAS_PROJECT_URL);
if (StringUtils.isEmpty(casServerUrl)) {
String msg = "CAS初始化失败,请配置CAS服务端地址";
logger.error(msg);
throw new RuntimeException(msg);
}
if (StringUtils.isEmpty(casProjectUrl)) {
String msg = "CAS初始化失败,请配置CAS客户端地址";
logger.error(msg);
throw new RuntimeException(msg);
}
CasLogoutFilter casLogoutFilter = new CasLogoutFilter();
//修改LogoutFilter的登出路径,跳转到CAS SERVER完成退出登录
casLogoutFilter.setRedirectUrl(casServerUrl + "/logout?service=" + casProjectUrl);
return casLogoutFilter;
}
@Bean
SystemAuthorizingCasRealm casRealm() {
String casServerUrl = Global.getConfig(KeyConsts.CAS_SERVER_URL);
String casProjectUrl = Global.getConfig(KeyConsts.CAS_PROJECT_URL);
String adminPath = Global.getConfig(KeyConsts.ADMIN_PATH) == null ? DefaultValueConsts.DEFAULT_ADMIN_PATH : Global.getConfig(KeyConsts.ADMIN_PATH);
if (StringUtils.isEmpty(casServerUrl)) {
String msg = "SystemAuthorizingCasRealm初始化失败,请配置CAS服务端地址";
logger.error(msg);
throw new RuntimeException(msg);
}
if (StringUtils.isEmpty(casProjectUrl)) {
String msg = "SystemAuthorizingCasRealm初始化失败,请配置CAS客户端地址";
logger.error(msg);
throw new RuntimeException(msg);
}
SystemAuthorizingCasRealm systemAuthorizingCasRealm = new SystemAuthorizingCasRealm();
systemAuthorizingCasRealm.setCachingEnabled(true);
systemAuthorizingCasRealm.setAuthenticationCachingEnabled(true);
systemAuthorizingCasRealm.setAuthenticationCacheName("authenticationCache");
systemAuthorizingCasRealm.setAuthorizationCachingEnabled(true);
systemAuthorizingCasRealm.setAuthenticationCacheName("authorizationCache");
systemAuthorizingCasRealm.setCasServerUrlPrefix(casServerUrl);
systemAuthorizingCasRealm.setCasService(casProjectUrl + adminPath + "/cas-login");
return systemAuthorizingCasRealm;
}
@Configuration
@Conditional(CasCondition.class)
@Lazy(false)
static class AfterConfig{
@Autowired
CustomCasFilter casFilter;
@Autowired
LogoutFilter casLogoutFilter;
@Autowired
SystemAuthorizingCasRealm casRealm;
@Autowired
DefaultWebSecurityManager defaultWebSecurityManager;
@Autowired
CustomShiroFilterFactoryBean customShiroFilterFactoryBean;
@PostConstruct
void addCasConfigs() {
String casServerUrl = Global.getConfig(KeyConsts.CAS_SERVER_URL);
String casProjectUrl = Global.getConfig(KeyConsts.CAS_PROJECT_URL);
String adminPath = Global.getConfig(KeyConsts.ADMIN_PATH) == null ? DefaultValueConsts.DEFAULT_ADMIN_PATH : Global.getConfig(KeyConsts.ADMIN_PATH);
if (StringUtils.isEmpty(casServerUrl)) {
String msg = "CAS初始化失败,请配置CAS服务端地址";
logger.error(msg);
throw new RuntimeException(msg);
}
if (StringUtils.isEmpty(casProjectUrl)) {
String msg = "CAS初始化失败,请配置CAS客户端地址";
logger.error(msg);
throw new RuntimeException(msg);
}
//SecurityManager配置
//添加Realm
ShiroSecurityUtils.addRealm2SecurityManager(casRealm,defaultWebSecurityManager);
//ShiroFilterFactoryBean配置
customShiroFilterFactoryBean.setLoginUrl(casServerUrl + "login?service=" + casProjectUrl + adminPath + "/cas-login");
//添加过滤器CasFilter
ShiroSecurityUtils.addPathAndFilterNameAndFilterEntry(adminPath + "/cas-login", "cas", casFilter,customShiroFilterFactoryBean);
//添加CasLogoutFilter
ShiroSecurityUtils.addPathAndFilterNameAndFilterEntry(adminPath +"/logout", "logout", casLogoutFilter,customShiroFilterFactoryBean);
}
}
}
public class CustomCasFilter extends CasFilter {
private Logger logger = LoggerFactory.getLogger(CustomCasFilter.class);
// the name of the parameter service ticket in url
private static final String TICKET_PARAMETER = "ticket";
/**
* 登录成功,增加会话内容
* @param token
* @param subject
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
CasToken casToken = (CasToken)token;
if (casToken != null) {
// 内网情况下,直接根据用户名去查询用户对应信息
String userName = casToken.getPrincipal().toString();
logger.info("customCasfilter类.......");
logger.info("userName:{}",userName);
User user = UserUtils.getUser(userName);
if (user != null) {
HttpServletRequest httpRequest = (HttpServletRequest)request;
//会话保存已经认证的用户信息
HttpSession session = httpRequest.getSession();
session.setAttribute("userid_", user.getId());
session.setAttribute("username_", userName);
session.setAttribute("staffName_", user.getStaffName());
session.setAttribute("password_", user.getPassWord());
session.setAttribute("systemUser_", user.getSystemUser());
session.setAttribute("fingerFlag", false);
session.setAttribute("publicUser", false);
logger.info("username:{}, password:{}",user.getUsername(),user.getPassWord());
}else{
logger.info("user is null");
}
}
return super.onLoginSuccess(token, subject, request, response);
}
@Override
public void setLoginUrl(String loginUrl) {
// TODO Auto-generated method stub
super.setLoginUrl(loginUrl);
}
private static final SingleSignOutHandler handler = new SingleSignOutHandler();
@Override
public void setFilterConfig(FilterConfig filterConfig) {
super.setFilterConfig(filterConfig);
handler.init();
}
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
HttpServletRequest req = (HttpServletRequest) request;
if (handler.isTokenRequest(req)) {
handler.recordSession(req);
} else if(handler.isLogoutRequest(req)) {
String a = req.getContextPath();
handler.destroySession(req);
return;
}
}
}
public class SystemAuthorizingCasRealm extends CasRealm {
private static final Logger loggger = LoggerFactory.getLogger(SystemAuthorizingCasRealm.class);
//执行认证的逻辑,这里可以重写业务逻辑代码
@SuppressWarnings({ "null" })
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
CasToken casToken = (CasToken) token;
if (token == null) {
return null;
}
String ticket = (String)casToken.getCredentials();
if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
return null;
}
//票据验证器,验证Service Ticket(ST)
TicketValidator ticketValidator = ensureTicketValidator();
try {
//验证cas服务和ST
// contact CAS server to validate service ticket
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
// get principal, user id and attributes
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
if(loggger.isDebugEnabled()){
loggger.debug("casPrincipal userName:{}",casPrincipal.getName());
}
//URLDecoder使用UTF8将字符串转成字符串
String userName = URLDecoder.decode(casPrincipal.getName(),"utf-8");
//todo 有些CAS服务器返回的字符串是utf-8编码的字节流,服务器使用gbk去生成字符串,就会导致乱码,在这种情况下,就需要先对字符串编码得到字节流,然后,使用utf-8编码得到正确的字符串
// String userName=new String(casPrincipal.getName().getBytes("gbk"),"utf-8");
if(loggger.isDebugEnabled()){
loggger.debug("{} decoded userName:{}","utf8",userName);
}
loggger.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}",
ticket, getCasServerUrlPrefix(), userName
);
// refresh authentication token (user id + remember me)
casToken.setUserId(userName);
String rememberMeAttributeName = getRememberMeAttributeName();
Map<String, Object> attributes = casPrincipal.getAttributes();
String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
if (isRemembered) {
casToken.setRememberMe(true);
}
User user = null;
try {
if(loggger.isDebugEnabled()){
loggger.debug("调用用户查询服务");
}
user = UserUtils.getUser(userName);
} catch (Exception e) {
loggger.error("查询用户失败:",e);
e.printStackTrace();
}
if (user != null) {
//byte[] salt = Encodes.decodeHex(user.getPassWord().substring(0, 16));
return new SimpleAuthenticationInfo(new Principal(user), /*user.getPassWord().substring(16), ByteSource.Util.bytes(salt)*/
ticket,
getName());
} else {
String msg=String.format("查询不到用户:%s",userName);
loggger.error(msg);
if(StringUtils.isEmpty(userName)){
throw new AuthenticationException(msg);
// throw new AuthenticationException("msg:账号或密码不能为空!");
}
return null;
}
// create simple authentication info
// List<Object> principals = CollectionUtils.asList(userId, attributes);
// PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
// return new SimpleAuthenticationInfo(principalCollection, ticket);
} catch (TicketValidationException e) {
loggger.error("解析Ticket失败",e);
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
} catch (UnsupportedEncodingException e) {
loggger.error("未能正确解析用户名称:",e);
throw new CasAuthenticationException(String.format("未能正确解析用户登录名称:%s",e.getMessage()));
}
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
//return super.doGetAuthorizationInfo(principals);
// retrieve user information
SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
List<Object> listPrincipals = principalCollection.asList();
Principal principal = (Principal) listPrincipals.get(0);
//Map<String, String> attributes = (Map<String, String>) listPrincipals.get(0);
//create simple authorization info
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermission("user");
List<String> list = UserUtils.getPrivileges(principal.getUsername());
if (null != list) {
simpleAuthorizationInfo.addStringPermissions(list);
}
return simpleAuthorizationInfo;
}
public void clearAuthorization() {
//this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
if (cache != null) {
for (Object key : cache.keys()) {
cache.remove(key);
}
}
}
@Override
protected TicketValidator createTicketValidator() {
String urlPrefix = getCasServerUrlPrefix();
if ("saml".equalsIgnoreCase(getValidationProtocol())) {
Saml11TicketValidator saml11TicketValidator=new Saml11TicketValidator(urlPrefix);
//设置utf8编码
saml11TicketValidator.setEncoding("utf8");
return saml11TicketValidator;
}
Cas20ServiceTicketValidator cas20ServiceTicketValidator=new Cas20ServiceTicketValidator(urlPrefix);
//设置utf8编码
cas20ServiceTicketValidator.setEncoding("utf8");
return cas20ServiceTicketValidator;
}
}
XML配置方式(Web.xml)
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://192.168.5.200:9080/cas</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<filter>
<filter-name>CASFilter</filter-name>
<!-- 自定义认证过滤器,可以继承AbstractCasFilter,重写initInternal逻辑 -->
<filter-class>com.xxxxx.conf.RealEstateAuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<!--外网域名-->
<param-value>http://192.168.5.200:9080/cas/login</param-value>
<!-- 内网-->
<!--<param-value>http://59.202.30.26:9080/cas/login</param-value>-->
<!--这里的server是服务端的IP-->
</init-param>
<init-param>
<param-name>serverName</param-name>
<!--外网域名-->
<!--<param-value>http://xxxxx:8080</param-value>-->
<!-- 内网-->
<!--<param-value>http://xxxxx:8080</param-value>-->
<!--本地-->
<param-value>http://192.168.5.200:8085</param-value>
<!--外网测试-->
<!--<param-value>http://59.202.30.122:8079</param-value>-->
<!--内网测试-->
<!--<param-value>http://59.202.30.122:8080</param-value>-->
</init-param>
<!--cas-client-core升级到3.3.3版本以上,支持排除部分url可以跳过单点验证路径分隔符采用 “|”-->
<init-param>
<param-name>ignorePattern</param-name>
<param-value></param-value>
</init-param>
<init-param>
<param-name>ignoreUrlPatternType</param-name>
<param-value>REGEX</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<!--外网SLB-->
<param-value>http://192.168.5.200:9080/cas</param-value>
<!-- 内网-->
<!--<param-value>http://59.202.30.26:9080/cas</param-value>-->
</init-param>
<init-param>
<param-name>serverName</param-name>
<!--外网SLB域名-->
<!--<param-value>http://bdc.zjzwfw.gov.cn:8080</param-value>-->
<!-- 内网-->
<!--<param-value>http://govbdc.zjzwfw.gov.cn:8080</param-value>-->
<!--本地-->
<param-value>http://192.168.5.200:8085</param-value>
<!--外网测试-->
<!--<param-value>http://59.202.30.122:8079</param-value>-->
<!--内网测试-->
<!--<param-value>http://59.202.30.122:8080</param-value>-->
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--
该过滤器负责实现HttpServletRequest请求的包裹,
比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。
-->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
AuthenticationFilter(可重写,认证核心类)
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
if (this.isRequestUrlExcluded(request)) {
this.logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
HttpSession session = request.getSession(false);
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
//这里判断session是否存在,如果存在就直接跳出,也就是说第二次访问的时候,就不会再去验证cas了,这里也是一个隐藏的风险点
//这里可以对判断逻辑进行重写,按照自己需要的方式并且_const_cas_assertion_里面包含了所有的用户信息,也可以做自定义处理
if (assertion != null) {
filterChain.doFilter(request, response);
} else {
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = this.retrieveTicketFromRequest(request);
boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
//如果ticket为空,则跳转到登录页面,并加上回调地址,http://cas/login?service=http://yewu.service
if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
this.logger.debug("no ticket and no assertion found");
String modifiedServiceUrl;
if (this.gateway) {
this.logger.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
} else {
filterChain.doFilter(request, response);
}
}
}
TicketValidator(票据验证)
public final Assertion validate(String ticket, String service) throws TicketValidationException {
String validationUrl = this.constructValidationUrl(ticket, service);
this.logger.debug("Constructing validation url: {}", validationUrl);
try {
this.logger.debug("Retrieving response from server.");
String serverResponse = this.retrieveResponseFromServer(new URL(validationUrl), ticket);
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
} else {
this.logger.debug("Server response: {}", serverResponse);
return this.parseResponseFromServer(serverResponse);
}
} catch (MalformedURLException var5) {
throw new TicketValidationException(var5);
}
}
SingleSignOutHttpSessionListener(单点登出)
public void sessionDestroyed(HttpSessionEvent event) {
if (this.sessionMappingStorage == null) {
this.sessionMappingStorage = getSessionMappingStorage();
}
HttpSession session = event.getSession();
//移除session信息
this.sessionMappingStorage.removeBySessionById(session.getId());
}
CAS服务端相关原理介绍
整体结构介绍
login-webflow.xml
路径: classes\webflow\login\login-webflow.xml
系统登录相关配置,包括自定义的登录页面等 以及登录成功、失败等处理方法
一般来说,认证这块的逻辑每个业务系统都是不一样的,比如加入指纹登录,加密狗登录等,都需要基于CAS二次扩展,相对来说,CAS也提供了比较灵活的二次扩展手段
<var name="credential"
class="xxxxx.platform.security.cas.server.custom.UsernamePasswordCredentialExtendFingerAndSoftdog" />
<on-start>
<evaluate expression="initialFlowSetupAction" />
</on-start>
<!--这里定义的id都是 cas-servlet.xml定义的bean,指定用哪些方法处理-->
<action-state id="ticketGrantingTicketCheck">
<evaluate expression="ticketGrantingTicketCheckAction" />
<transition on="notExists" to="gatewayRequestCheck" />
<transition on="invalid" to="terminateSession" />
<transition on="valid" to="hasServiceCheck" />
</action-state>
public class UsernamePasswordCredentialExtendFingerAndSoftdog extends UsernamePasswordCredential {
private static final long serialVersionUID = 3198836306626671289L;
private String finger;
private String fingerPassWord;
private String softdog;
private String rsaPassword;
private String telCode;
private String idCode;
private Boolean warn = false;
public String getTelCode() {
return telCode;
}
public void setTelCode(String telCode) {
this.telCode = telCode;
}
public String getFinger() {
return finger;
}
public void setFinger(String finger) {
this.finger = finger;
}
public String getFingerPassWord() {
return fingerPassWord;
}
public void setFingerPassWord(String fingerPassWord) {
this.fingerPassWord = fingerPassWord;
}
public String getSoftdog() {
return softdog;
}
public void setSoftdog(String softdog) {
this.softdog = softdog;
}
public String getRsaPassword() {
return rsaPassword;
}
public void setRsaPassword(String rsaPassword) {
this.rsaPassword = rsaPassword;
}
public String getIdCode() {
return idCode;
}
public void setIdCode(String idCode) {
this.idCode = idCode;
}
public Boolean getWarn() {
return warn;
}
public void setWarn(Boolean warn) {
this.warn = warn;
}
}
logout-webflow.xml
路径: classes\webflow\logout\logout-webflow.xml
同上,一些注销事项的定义
cas-servlet.xml
核心的一些启动配置和相关自定义认证处理,都是在 cas-servlet.xml
例如启动类InitialFlowSetupAction
<bean id="initialFlowSetupAction" class="org.jasig.cas.web.flow.InitialFlowSetupAction"
p:argumentExtractors-ref="argumentExtractors"
p:warnCookieGenerator-ref="warnCookieGenerator"
p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"
p:servicesManager-ref="servicesManager"
p:enableFlowOnAbsentServiceRequest="${create.sso.missing.service:true}" />
deployerConfigContext.xml
一般是处理自定义验证方法,比如用户密码的验证,密码的加密算法等
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://ip:port/platform?characterEncoding=utf8"/>
<property name="username" value="username"/>
<property name="password" value="password"/>
<property name="initialSize" value="30"/>
<property name="minIdle" value="30"/>
<property name="maxActive" value="200"/>
<property name="maxWait" value="60000"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="select 'x' from dual"/>
<property name="testWhileIdle" value="true"/>
</bean>
<!--自定义密码认证器, 一般业务系统都是会有的-->
<bean id="passwordEncoder" class="xxxxx.platform.security.cas.server.custom.CustomPasswordEncoder"></bean>
public class CustomPasswordEncoder implements PasswordEncoder {
private static final int HASH_INTERATIONS = 1024;
byte[] salt;
public String encode(String password) {
byte[] hashPassword = Digests.sha1(password.getBytes(), salt, HASH_INTERATIONS);
String encryptedPassword = Hex.encodeToString(salt) + Hex.encodeToString(hashPassword);
return encryptedPassword;
}
protected void setSalt(byte[] salt) {
this.salt = salt;
}
}
相关原理介绍
页面打开的时候
如果浏览器中不存在TGC cookie 则直接跳转到登录页面
如果存在 cookie就,校验TGC是否合法(InitialFlowSetupAction.doExecute)
@Override
protected Event doExecute(final RequestContext context) throws Exception {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
final String contextPath = context.getExternalContext().getContextPath();
final String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/";
if (StringUtils.isBlank(warnCookieGenerator.getCookiePath())) {
logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath);
this.warnCookieGenerator.setCookiePath(cookiePath);
} else {
logger.debug("Warning cookie path is set to {} and path {}", warnCookieGenerator.getCookieDomain(),
warnCookieGenerator.getCookiePath());
}
if (StringUtils.isBlank(ticketGrantingTicketCookieGenerator.getCookiePath())) {
logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath);
this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
} else {
logger.debug("TGC cookie path is set to {} and path {}", ticketGrantingTicketCookieGenerator.getCookieDomain(),
ticketGrantingTicketCookieGenerator.getCookiePath());
}
WebUtils.putTicketGrantingTicketInScopes(context,
this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
WebUtils.putWarningCookie(context,
Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
final Service service = WebUtils.getService(this.argumentExtractors, context);
if (service != null) {
logger.debug("Placing service in context scope: [{}]", service.getId());
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
logger.debug("Placing registered service [{}] with id [{}] in context scope",
registeredService.getServiceId(),
registeredService.getId());
WebUtils.putRegisteredService(context, registeredService);
final RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy();
if (accessStrategy.getUnauthorizedRedirectUrl() != null) {
logger.debug("Placing registered service's unauthorized redirect url [{}] with id [{}] in context scope",
accessStrategy.getUnauthorizedRedirectUrl(),
registeredService.getServiceId());
WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
}
}
} else if (!this.enableFlowOnAbsentServiceRequest) {
logger.warn("No service authentication request is available at [{}]. CAS is configured to disable the flow.",
WebUtils.getHttpServletRequest(context).getRequestURL());
throw new NoSuchFlowExecutionException(context.getFlowExecutionContext().getKey(),
new UnauthorizedServiceException("screen.service.required.message", "Service is required"));
}
WebUtils.putService(context, service);
return result("success");
}
其中: WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request)) -->obtainCookieValue; 用于验证TGC
TGC生成是由 TGT + IP + UserAgent三个部分组成的
这时候校验TGC就是验证这三个部分有没有问题,其中IP保证是来着同一个域的,指的是CAS的服务端的域,UserAgent指的是浏览器的代理,这时候会发现,如果是浏览器的谷歌模式和IE模式,这个值就是不一样的,会照成验证不一致。
三段组成
登录过程
参考login-webflow, 以及cas-servlet
login-webflow
<action-state id="realSubmit">
<evaluate
expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)" />
<transition on="warn" to="warn" />
<!-- To enable AUP workflows, replace the 'success' transition with the
following: <transition on="success" to="acceptableUsagePolicyCheck" /> -->
<transition on="success" to="sendTicketGrantingTicket" />
<transition on="successWithWarnings" to="showMessages" />
<transition on="authenticationFailure"
to="handleAuthenticationFailure" />
<transition on="error" to="generateLoginTicket" />
</action-state>
cas-servlet
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
p:centralAuthenticationService-ref="centralAuthenticationService"
p:warnCookieGenerator-ref="warnCookieGenerator"/>
最后可以定位到 AuthenticationViaFormAction
/**
* Handle the submission of credentials from the post.
*
* @param context the context
* @param credential the credential
* @param messageContext the message context
* @return the event
* @since 4.1.0
*/
public final Event submit(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
if (!checkLoginTicketIfExists(context)) {
return returnInvalidLoginTicketEvent(context, messageContext);
}
if (isRequestAskingForServiceTicket(context)) {
return grantServiceTicket(context, credential);
}
return createTicketGrantingTicket(context, credential, messageContext);
}
其中 checkLoginTicketIfExists 验证登录携带的信息是否正确
当前版本登录需要携带 lt和execution两个属性信息 这两个属性在打开登录页的时候会自动生成,后续如果要模拟登录,这两个字段是重点
其中 isRequestAskingForServiceTicket 判断地址是都有renew参数 代表这个请求,服务端需要强制进行认证,针对一些敏感内容,可以要求强制重新认证 gateway 参数刚好相反,只要session存在就不用重新认证了
最后就是 createTicketGrantingTicket
/**
* Create ticket granting ticket for the given credentials.
* Adds all warnings into the message context.
*
* @param context the context
* @param credential the credential
* @param messageContext the message context
* @return the resulting event.
* @since 4.1.0
*/
protected Event createTicketGrantingTicket(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
try {
//认证并创建票据
final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(credential);
WebUtils.putTicketGrantingTicketInScopes(context, tgt);
//写入缓存,也就是TGC的内容,重点
putWarnCookieIfRequestParameterPresent(context);
putPublicWorkstationToFlowIfRequestParameterPresent(context);
if (addWarningMessagesToMessageContextIfNeeded(tgt, messageContext)) {
return newEvent(SUCCESS_WITH_WARNINGS);
}
return newEvent(SUCCESS);
} catch (final AuthenticationException e) {
logger.debug(e.getMessage(), e);
return newEvent(AUTHENTICATION_FAILURE, e);
} catch (final Exception e) {
logger.debug(e.getMessage(), e);
return newEvent(ERROR, e);
}
}
生成ticket
public TicketGrantingTicket createTicketGrantingTicket(final Credential... credentials)
throws AuthenticationException, TicketException {
final Set<Credential> sanitizedCredentials = sanitizeCredentials(credentials);
if (!sanitizedCredentials.isEmpty()) {
//认证用户名密码
final Authentication authentication = this.authenticationManager.authenticate(credentials);
//生成ticket
final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
this.ticketGrantingTicketUniqueTicketIdGenerator
.getNewTicketId(TicketGrantingTicket.PREFIX),
authentication, this.ticketGrantingTicketExpirationPolicy);
this.ticketRegistry.addTicket(ticketGrantingTicket);
return ticketGrantingTicket;
}
final String msg = "No credentials were specified in the request for creating a new ticket-granting ticket";
logger.warn(msg);
throw new TicketCreationException(new IllegalArgumentException(msg));
}
大致的类访问路径: login-webflow.xml cas-servlet.xml
graph TD
emperor(( ))-->AuthenticationViaFormAction.submit
AuthenticationViaFormAction.submit-->checkLoginTicketIfExists
AuthenticationViaFormAction.submit-->createTicketGrantingTicket
AuthenticationViaFormAction.submit-->isRequestAskingForServiceTicket
createTicketGrantingTicket--保存TGT-->WebUtils.putTicketGrantingTicketInScopes
createTicketGrantingTicket--生成TGT-->CentralAuthenticationService.createTicketGrantingTicket
createTicketGrantingTicket--生成Cookie,TGC-->putWarnCookieIfRequestParameterPresent
CentralAuthenticationService.createTicketGrantingTicket-->PolicyBasedAuthenticationManager.authenticate
PolicyBasedAuthenticationManager.authenticate-->authenticateInternal
authenticateInternal-->AuthenticationHandler.authenticate
AuthenticationHandler.authenticate-->AbstractPreAndPostProcessingAuthenticationHandler.authenticate
AbstractPreAndPostProcessingAuthenticationHandler.authenticate-->doAuthentication
doAuthentication-->AbstractUsernamePasswordAuthenticationHandler.doAuthentication
AbstractUsernamePasswordAuthenticationHandler.doAuthentication-->authenticateUsernamePasswordInternal
authenticateUsernamePasswordInternal-->CustomAuthenticationHandler.authenticateUsernamePasswordInternal
注销过程
前面一样,通过logout-webflow.xml cas-servlet.xml找到入口方法
就是TerminateSessionAction.terminate
public Event terminate(final RequestContext context) {
String tgtId = WebUtils.getTicketGrantingTicketId(context);
// 获取TGT,TGC等信息
if (tgtId == null) {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
tgtId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
}
//如果存在TGT,则找到相关的请求信息
//destroyTicketGrantingTicket 会调用业务系统的回调地址,去销毁业务系统的session或者登录信息
if (tgtId != null) {
WebUtils.putLogoutRequests(context, this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId));
}
final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
this.ticketGrantingTicketCookieGenerator.removeCookie(response);
this.warnCookieGenerator.removeCookie(response);
return this.eventFactorySupport.success(this);
}
还有就是处理被单点的系统的退出 LogoutAction.doInternalExecute 例如有A,B,C系统登录了,这时候要分别调用A,B,C系统的注销工作,通过传递logRequest等参数,从而让客户端可以判断
protected Event doInternalExecute(final HttpServletRequest request, final HttpServletResponse response,
final RequestContext context) throws Exception {
boolean needFrontSlo = false;
putLogoutIndex(context, 0);
final List<LogoutRequest> logoutRequests = WebUtils.getLogoutRequests(context);
if (logoutRequests != null) {
for (final LogoutRequest logoutRequest : logoutRequests) {
// if some logout request must still be attempted
if (logoutRequest.getStatus() == LogoutRequestStatus.NOT_ATTEMPTED) {
needFrontSlo = true;
break;
}
}
}
final String service = request.getParameter("service");
if (this.followServiceRedirects && service != null) {
final Service webAppService = new SimpleWebApplicationServiceImpl(service);
final RegisteredService rService = this.servicesManager.findServiceBy(webAppService);
if (rService != null && rService.getAccessStrategy().isServiceAccessAllowed()) {
context.getFlowScope().put("logoutRedirectUrl", service);
}
}
// there are some front services to logout, perform front SLO
if (needFrontSlo) {
return new Event(this, FRONT_EVENT);
} else {
// otherwise, finish the logout process
return new Event(this, FINISH_EVENT);
}
}
JAVA模拟CAS登录
准备工作
CAS服务端需要开启Rest服务支持
pom里面增加cas-server-support-rest,让后重新编译就行。
<dependency>
<groupId>org.jasig.cas</groupId>
<artifactId>cas-server-support-rest</artifactId>
<version>4.1.10</version>
</dependency>
地址
/**
* cas根目录地址
*/
public static final String SERVER_URL="http://192.168.20.238:8080/cas/";
/**
* 回调函数地址
*/
public static final String TAGET_URL = "http://192.168.20.238:8089/testCas";
/**
* 登录页面地址,用于获取execution和lt
*/
private static final String GET_EXECUTION_URL = SERVER_URL + "login?service="+ConstValue.TAGET_URL;
/**
* 获取TGT票据地址
*/
private static final String GET_TOKEN_URL = SERVER_URL + "v1/tickets";
/**
* 登录接口地址
*/
private static final String GET_TOKEN_URL_TGC = SERVER_URL + "login";
/**
* 系统注销地址
*/
private static final String LOGOUT_URL = SERVER_URL + "logout";
根据CAS登录地址和回调地址获取属性
前面有讲到,登录过程中,需要用到lt和execution参数,所以,需要先模拟访问这个登录页面,然后获取到这两个参数
/**
* 获取 execution信息,用于后续的认证
* @return
* @throws IOException
*/
public String[] GetExecution() throws IOException {
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpGet httpGet = new HttpGet(GET_EXECUTION_URL);
HttpResponse response = client.execute(httpGet);
String strResult = EntityUtils.toString(response.getEntity());
Page page =new Page();
page.setRawText(strResult);
page.setRequest(new Request(GET_EXECUTION_URL));
String execution = page.getHtml().xpath("//input[@name='execution']/@value").get();
String lt = page.getHtml().xpath("//input[@name='lt']/@value").get();
String[] arr = new String[2];
arr[0] = execution;
arr[1] = lt;
return arr;
}
模拟登录过程,并设置TGC Cookie信息
public void putTGC(String username, String password, String execution,String lt, HttpServletResponse responses,HttpServletRequest request)
throws ClientProtocolException, IOException {
CloseableHttpClient httpClient = null;
try {
CookieStore cookieStore = new BasicCookieStore();
httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
HttpPost httpPost = new HttpPost(GET_TOKEN_URL_TGC);
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
nvps.add(new BasicNameValuePair("username", username));
nvps.add(new BasicNameValuePair("password", encode(password)));
//lt和execution两个必须参数
nvps.add(new BasicNameValuePair("execution", execution));
nvps.add(new BasicNameValuePair("lt", lt));
nvps.add(new BasicNameValuePair("_eventId", "submit"));
HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
String userAgent = request.getHeader("user-agent");
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
httpPost.setHeader("user-agent",userAgent);
httpPost.setEntity(reqEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
List<Cookie> cookies = cookieStore.getCookies();
if (null != cookies && cookies.size() > 0) {
cookies.forEach(p->{
//成功以后,可以获取到TGC信息,如果没有获取到,就是失败
//这块服务端要进行设置,服务端的异常都是内部处理掉了,不会返回,需要服务端增加异常处理机制
if(p.getName().toUpperCase(Locale.ROOT).equals("TGC")){
javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(p.getName(),
p.getValue());
cookie.setPath(p.getPath());
//cookie.setHttpOnly(true);
//cookie.setSecure(true);
//cookie.setMaxAge(1800);
responses.addCookie(cookie);
}
});
}
} finally {
httpClient.close();
}
}
获取TGT信息
这个是用于后续获取ST票据的重要过程
/**
* 获取TGT,服务端生成的票据信息
* @param username
* @param password
* @return
* @throws ClientProtocolException
* @throws IOException
*/
public String getTGT(String username, String password) throws ClientProtocolException, IOException {
String tgt = "";
CloseableHttpClient httpClient = null;
try {
CookieStore cookieStore = new BasicCookieStore();
httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
HttpPost httpPost = new HttpPost(GET_TOKEN_URL);
List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair("username", username));
nvps.add(new BasicNameValuePair("password", encode(password)));
HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
httpPost.setEntity(reqEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
String strResult = EntityUtils.toString(response.getEntity());
Page page =new Page();
page.setRawText(strResult);
page.setRequest(new Request(GET_TOKEN_URL));
String execution = page.getHtml().xpath("//form[@name='execution']/@value").get();
try {
Header[] tgtHead = response.getAllHeaders();
if (tgtHead != null) {
for (int i = 0; i < tgtHead.length; i++) {
if (StringUtils.equals(tgtHead[i].getName(), "Location")) {
tgt = tgtHead[i].getValue().substring(tgtHead[i].getValue().lastIndexOf("/") + 1);
}
}
}
HttpEntity respEntity = response.getEntity();
EntityUtils.consume(respEntity);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
response.close();
}
} finally {
httpClient.close();
}
return tgt;
}
根据TGT获取ST信息
public String getST(String tgt,String TAGET_URL) throws MalformedURLException {
String serviceTicket = "";
OutputStreamWriter out = null;
BufferedWriter wirter = null;
HttpURLConnection conn = null;
URL url = new URL(GET_TOKEN_URL + "/" + tgt);
try {
conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
String param = "service=" + URLEncoder.encode(TAGET_URL, "utf-8");
out = new OutputStreamWriter(conn.getOutputStream());
wirter = new BufferedWriter(out);
wirter.write(param);
wirter.flush();
wirter.close();
out.close();
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
while ((line = in.readLine()) != null) {
serviceTicket = line;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (conn != null) {
conn.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return serviceTicket;
}
最后地址回调
地址格式就是: 回调地址?ticket=前一步获取到的ST
if (StringUtils.isNotBlank(tgt)) {
String ticket = getST(tgt,ConstValue.TAGET_URL);
if(StringUtils.isNotBlank(ticket)){
response.sendRedirect(ConstValue.TAGET_URL +"?ticket=" + ticket);
}else{
response.sendRedirect("/login2");
}
} else {
response.sendRedirect("/login2");
}
完整代码
package com.example.caslogin.service;
import com.example.caslogin.conf.ConstValue;
import com.sun.deploy.net.URLEncoder;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import sun.misc.BASE64Encoder;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@Service
public class LoginService {
private static final String GET_EXECUTION_URL = ConstValue.SERVER_URL + "login?service="+ConstValue.TAGET_URL;
private static final String GET_TOKEN_URL = ConstValue.SERVER_URL + "v1/tickets";
private static final String GET_TOKEN_URL_TGC = ConstValue.SERVER_URL + "login";
private static final String LOGOUT_URL = ConstValue.SERVER_URL + "logout";
public void logout(HttpServletRequest servletRequest,HttpServletResponse servletResponse) throws IOException {
CookieStore cookieStore = new BasicCookieStore();
javax.servlet.http.Cookie[] cookies = servletRequest.getCookies();
for (javax.servlet.http.Cookie cookie :cookies){
BasicClientCookie cookieNew =new BasicClientCookie(cookie.getName(),cookie.getValue());
cookieNew.setDomain("192.168.20.238");
cookieNew.setPath(cookie.getPath());
cookieStore.addCookie(cookieNew);
}
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultCookieStore(cookieStore)
.build();
HttpGet httpGet = new HttpGet(LOGOUT_URL);
String userAgent = servletRequest.getHeader("user-agent");
httpGet.setHeader("user-agent",userAgent);
HttpResponse response = httpClient.execute(httpGet);
String strResult = EntityUtils.toString(response.getEntity());
System.out.println(strResult);
}
public void login(HttpServletRequest request,HttpServletResponse response) throws IOException {
String account = "admin";
String password = "admin";
String[] result = GetExecution();
String execution = result[0];
String lt = result[1];
putTGC(account, password, execution,lt, response,request);
String tgt = getTGT(account, password);
if (StringUtils.isNotBlank(tgt)) {
String ticket = getST(tgt,ConstValue.TAGET_URL);
if(StringUtils.isNotBlank(ticket)){
response.sendRedirect(ConstValue.TAGET_URL +"?ticket=" + ticket);
}else{
response.sendRedirect("/login2");
}
} else {
response.sendRedirect("/login2");
}
}
/**
* 获取 execution信息,用于后续的认证
* @return
* @throws IOException
*/
public String[] GetExecution() throws IOException {
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpGet httpGet = new HttpGet(GET_EXECUTION_URL);
HttpResponse response = client.execute(httpGet);
String strResult = EntityUtils.toString(response.getEntity());
Page page =new Page();
page.setRawText(strResult);
page.setRequest(new Request(GET_EXECUTION_URL));
String execution = page.getHtml().xpath("//input[@name='execution']/@value").get();
String lt = page.getHtml().xpath("//input[@name='lt']/@value").get();
String[] arr = new String[2];
arr[0] = execution;
arr[1] = lt;
return arr;
}
public static final String encode(String base){
return encode(base.getBytes());
}
public static final String encode(byte[] baseBuff){
return new BASE64Encoder().encode(baseBuff);
}
/**
* 获取TGT,服务端生成的票据信息
* @param username
* @param password
* @return
* @throws ClientProtocolException
* @throws IOException
*/
public String getTGT(String username, String password) throws ClientProtocolException, IOException {
String tgt = "";
CloseableHttpClient httpClient = null;
try {
CookieStore cookieStore = new BasicCookieStore();
httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
HttpPost httpPost = new HttpPost(GET_TOKEN_URL);
List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair("username", username));
nvps.add(new BasicNameValuePair("password", encode(password)));
HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
httpPost.setEntity(reqEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
String strResult = EntityUtils.toString(response.getEntity());
Page page =new Page();
page.setRawText(strResult);
page.setRequest(new Request(GET_TOKEN_URL));
String execution = page.getHtml().xpath("//form[@name='execution']/@value").get();
try {
Header[] tgtHead = response.getAllHeaders();
if (tgtHead != null) {
for (int i = 0; i < tgtHead.length; i++) {
if (StringUtils.equals(tgtHead[i].getName(), "Location")) {
tgt = tgtHead[i].getValue().substring(tgtHead[i].getValue().lastIndexOf("/") + 1);
}
}
}
HttpEntity respEntity = response.getEntity();
EntityUtils.consume(respEntity);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
response.close();
}
} finally {
httpClient.close();
}
return tgt;
}
public String getST(String tgt,String TAGET_URL) throws MalformedURLException {
String serviceTicket = "";
OutputStreamWriter out = null;
BufferedWriter wirter = null;
HttpURLConnection conn = null;
URL url = new URL(GET_TOKEN_URL + "/" + tgt);
try {
conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
String param = "service=" + URLEncoder.encode(TAGET_URL, "utf-8");
out = new OutputStreamWriter(conn.getOutputStream());
wirter = new BufferedWriter(out);
wirter.write(param);
wirter.flush();
wirter.close();
out.close();
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
while ((line = in.readLine()) != null) {
serviceTicket = line;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (conn != null) {
conn.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return serviceTicket;
}
public void putTGC(String username, String password, String execution,String lt, HttpServletResponse responses,HttpServletRequest request)
throws ClientProtocolException, IOException {
CloseableHttpClient httpClient = null;
try {
CookieStore cookieStore = new BasicCookieStore();
httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
HttpPost httpPost = new HttpPost(GET_TOKEN_URL_TGC);
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
nvps.add(new BasicNameValuePair("username", username));
nvps.add(new BasicNameValuePair("password", encode(password)));
//lt和execution两个必须参数
nvps.add(new BasicNameValuePair("execution", execution));
nvps.add(new BasicNameValuePair("lt", lt));
nvps.add(new BasicNameValuePair("_eventId", "submit"));
HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
String userAgent = request.getHeader("user-agent");
httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
httpPost.setHeader("user-agent",userAgent);
httpPost.setEntity(reqEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
List<Cookie> cookies = cookieStore.getCookies();
if (null != cookies && cookies.size() > 0) {
cookies.forEach(p->{
//成功以后,可以获取到TGC信息,如果没有获取到,就是失败
//这块服务端要进行设置,服务端的异常都是内部处理掉了,不会返回,需要服务端增加异常处理机制
if(p.getName().toUpperCase(Locale.ROOT).equals("TGC")){
javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(p.getName(),
p.getValue());
cookie.setPath(p.getPath());
//cookie.setHttpOnly(true);
//cookie.setSecure(true);
//cookie.setMaxAge(1800);
responses.addCookie(cookie);
}
});
}
} finally {
httpClient.close();
}
}
}
JAVA模拟CAS注销
public void logout(HttpServletRequest servletRequest,HttpServletResponse servletResponse) throws IOException {
CookieStore cookieStore = new BasicCookieStore();
javax.servlet.http.Cookie[] cookies = servletRequest.getCookies();
for (javax.servlet.http.Cookie cookie :cookies){
BasicClientCookie cookieNew =new BasicClientCookie(cookie.getName(),cookie.getValue());
cookieNew.setDomain("192.168.20.238");
cookieNew.setPath(cookie.getPath());
cookieStore.addCookie(cookieNew);
}
//获取Cookie,重要的是TGC这个,后续服务端要根据这个区获取TGT内容的
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultCookieStore(cookieStore)
.build();
HttpGet httpGet = new HttpGet(LOGOUT_URL);
//和验证一样,需要统一user-ahent
String userAgent = servletRequest.getHeader("user-agent");
httpGet.setHeader("user-agent",userAgent);
HttpResponse response = httpClient.execute(httpGet);
String strResult = EntityUtils.toString(response.getEntity());
System.out.println(strResult);
}