cas的工作原理(图是从百度拿的)
cas认证主要是靠TGT和ST
TGT(Ticket Grangting Ticket):TGT是CAS为用户签发的登录票据,拥有了TGT,用户就可以证明自己在CAS成功登录过。TGT封装了Cookie值以及此Cookie值对应的用户信息。用户在CAS认证成功后,CAS生成cookie(叫TGC),写入浏览器,同时生成一个TGT对象,放入自己的缓存,TGT对象的ID就是cookie的值。当HTTP再次请求到来时,如果传过来的有CAS生成的cookie,则CAS以此cookie值为key查询缓存中有无TGT ,如果有的话,则说明用户之前登录过,如果没有,则用户需要重新登录;
ST(Service Ticket):ST是CAS为用户签发的访问某一service的票据。用户访问service时,service发现用户没有ST,则要求用户去CAS获取ST。用户向CAS发出获取ST的请求,如果用户的请求中包含cookie,则CAS会以此cookie值为key查询缓存中有无TGT,如果存在TGT,则用此TGT签发一个ST,返回给用户。用户凭借ST去访问service,service拿ST去CAS验证,验证通过后,允许用户访问资源。
cas流程:
1、用户访问cas-client,被拦截跳转到cas-server进行登录,输入正确的用户信息
2、登录成功后,cas-server签发一个TGC票据,写入浏览器同时生成一个TGT对象,放入自己的缓存,TGT对象的ID就是cookie的值,并再次跳转到cas-client,同时携带着ST票据
cas-client发现有ST票据则拿着ST票据去cas-server验证,如果验证通过,则返回用户名信息
3、cas-client登录成功,用户访问另一个cas-client2时,也会被拦截再次跳转到cas-server发现TGC票据生成的TGT对象的ID值存在则直接验证通过,签发一个ST票据给cas-client2。
项目结构:
cas服务端:localhost:8080/cas
客户端a:localhost:8010
客户端b:localhost:8020
一、下载cas的服务端cas-overlay-template,这里我用的是5.2版本,JDK用的1.8,tomcat用的8
注:登录用户若要查询数据库(用配置文件里写的登录做测试可跳过此步骤),先在pom.xml里添加
oracle:
com.oracle
ojdbc6
1.0.0
org.apereo.cas
cas-server-support-jdbc
${cas.version}
ojdbc6因远程库的无法使用,需要自己安装在本地的maven中,安装方法(需要自己做相应的修改):
mvn install:install-file -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=1.0.0 -Dpackaging=jar -Dfile=D:/ojdbc6.jar
mysql:
org.apereo.cas
cas-server-support-jdbc
${cas.version}
org.apereo.cas
cas-server-support-jdbc-drivers
${cas.version}
mysql
mysql-connector-java
5.1.36
二、进入到解压后的cas-overlay-template-master文件夹,使用maven命令打包,打包比较慢,建议maven仓库换成阿里云的提升速度
mvn clean package
三、打包完后会出现target文件夹,里面有cas.war文件
四、把war包放到tomcat下运行后,修改war包后缀,进入cas文件夹找到WEB-INF\classes\services\HTTPSandIMAPS-10000001.json修改添加http(本来不支持http)
修改前:
"serviceId" : "^(https|imaps)://.*",
修改后:
"serviceId" : "^(https|http|imaps)://.*",
五、找到WEB-INF\classes\application.properties位置修改如下内容。
1、添加兼容http协议
cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true
2、修改端口号,原先https默认端口号是8443
修改前:
server.port=8443
修改后:
server.port=8080
3、修改用户名密码(若通过数据库查询则注释这一行)
修改前:
cas.authn.accept.users=casuser::Mellon
修改后:
cas.authn.accept.users=admin::123456
4、若通过数据库查询,添加数据库相关配置
cas.authn.jdbc.query[0].driverClass=oracle.jdbc.driver.OracleDriver
cas.authn.jdbc.query[0].url=jdbc:oracle:thin:@192.168.10.110:1521:orcl
cas.authn.jdbc.query[0].user=jeefh_business_performance01
cas.authn.jdbc.query[0].password=jeefh_business_performance01
cas.authn.jdbc.query[0].sql=select * from t_sys_user where loginname = ?
cas.authn.jdbc.query[0].fieldPassword=password
#使用md5加密可添加
#cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
#cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
#cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5
5、添加单点退出配置
#配置单点登出
#配置允许登出后跳转到指定页面
cas.logout.followServiceRedirects=true
#跳转到指定页面需要的参数名为 service
cas.logout.redirectParameter=service
#登出后需要跳转到的地址,如果配置该参数,service将无效。
#cas.logout.redirectUrl=https://www.taobao.com
#在退出时是否需要 确认退出提示 true弹出确认提示框 false直接退出
cas.logout.confirmLogout=false
#是否移除子系统的票据
cas.logout.removeDescendantTickets=true
#禁用单点登出,默认是false不禁止
#cas.slo.disabled=true
#默认异步通知客户端,清除session
cas.slo.asynchronous=true
六、启动tomcat,访问localhost:8080/cas,登录成功则cas服务端搭建完成
七、cas客户端搭建,新建一个maven项目,添加maven如下jar包,主要添加springbooo、cas、shiro
org.springframework.boot
spring-boot-starter-parent
1.4.0.RELEASE
org.springframework.boot
spring-boot-starter-web
org.apache.tomcat.embed
tomcat-embed-jasper
provided
javax.servlet
jstl
org.apache.shiro
shiro-spring
1.2.2
org.apache.shiro
shiro-core
1.4.0
org.apache.shiro
shiro-cas
1.4.0
org.jasig.cas.client
cas-client-core
3.3.3
junit
junit
4.11
test
八、新建MyShiroCasRealm类,继承CasRealm
package springboot.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class MyShiroCasRealm extends CasRealm{
/**
* 权限认证,为当前登录的Subject授予角色和权限
* 本例中该方法的调用时机为需授权资源被访问时
* 并且每次访问需授权资源时都会执行该方法中的逻辑,这表明本例中默认并未启用AuthorizationCache
* 如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),超过这个时间间隔再刷新页面,该方法会被执行
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 获取单点登陆后的用户名,也可以从session中获取,因为在认证成功后,已经将用户名放到session中去了
String userName = (String) super.getAvailablePrincipal(principals);
// principals.getPrimaryPrincipal(); 这种方式也可以获取用户名
// 根据用户名获取该用户的角色和权限信息
// UserInfo userInfo = userInfoService.findByUsername(userName);
//
// // 将用户对应的角色和权限信息打包放到AuthorizationInfo中
// for (SysRole role : userInfo.getRoleList()) {
// authorizationInfo.addRole(role.getRole());
// for (SysPermission p : role.getPermissions()) {
// authorizationInfo.addStringPermission(p.getPermission());
// }
// }
return authorizationInfo;
}
/**
* 1、CAS认证 ,验证用户身份
* 2、将用户基本信息设置到会话中(不用了,随时可以获取的)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
AuthenticationInfo authenticationInfo = super.doGetAuthenticationInfo(token);
String name = (String) authenticationInfo.getPrincipals().getPrimaryPrincipal();
SecurityUtils.getSubject().getSession().setAttribute("name", name);
return authenticationInfo;
}
}
九、新建MyCasFilter类,继承CasFilter
注:输入地址例如:localhost:8010/111,登录cas认证后返回的还是localhost:8010/111,没有跳转到首页("/index"),打断点发现shiro里封装的逻辑是如果SavedRequest里的url("/111")不为空就返回"/111",为空才返回设置的"/index"页面,所以使用反射修改SavedRequest的requestURL属性为"/index"
package springboot.shiro;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.util.SavedRequest;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
public class MyCasFilter extends CasFilter {
private static final String TICKET_PARAMETER = "ticket";
public MyCasFilter() {
}
@Override
public AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
// 获取请求的ticket
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ticket = getRequestTicket(httpRequest);
if (StringUtils.isEmpty(ticket)) {
return null;
}
return new CasToken(ticket);
}
/**
* 拒绝除了option以外的所有请求
**/
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
@Override
public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 获取ticket,如果不存在,直接返回false
String ticket = getRequestTicket((HttpServletRequest) request);
if (StringUtils.isEmpty(ticket)) {
return false;
}
return this.executeLogin(request, response);
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//获取AuthenticationToken实体
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
//执行分子系统的Shrio认证与授权
subject.login(token);
SavedRequest shiroSavedRequest = (SavedRequest) SecurityUtils.getSubject().getSession(false).getAttribute("shiroSavedRequest");
if (shiroSavedRequest != null) {
//修改url地址为登录首页,否则会跳转到之前手输的地址容易404,由于没有set方法,所以使用反射
Class extends Object> clazz = shiroSavedRequest.getClass();
Field requestURI = clazz.getDeclaredField("requestURI");
requestURI.setAccessible(true);
requestURI.set(shiroSavedRequest,this.getSuccessUrl());
}
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
/**
* 获取请求的ticket
*/
private String getRequestTicket(HttpServletRequest httpRequest) {
// 从参数中获取ticket
String ticket = httpRequest.getParameter(TICKET_PARAMETER);
if (StringUtils.isEmpty(ticket)) {
// 如果为空的话,则从header中获取参数
ticket = httpRequest.getHeader(TICKET_PARAMETER);
}
return ticket;
}
}
十、配置ShiroCasConfiguration类
package springboot.shiro;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroCasConfiguration {
// cas 的server地址
// public static final String casServerUrlPrefix = "https://www.royal.com:8443/cas";
public static final String casServerUrlPrefix = "http://localhost:8080/cas";
// cas 登录页面的地址
public static final String casLoginUrl = casServerUrlPrefix + "/login";
// cas 登出页面地址
public static final String casLogoutUrl = casServerUrlPrefix + "/logout";
// 对外提供的服务地址
public static final String shiroServerUrlPrefix = "http://localhost:8020";
// casFilter cas 拦截的地址
public static final String casFilterUrlPattern = "/shiro_cas";
// 登录成功的地址
public static final String loginSuccessUrl = "/index";
// 登录的地址
public static final String loginUrl = casLoginUrl + "?service=" + shiroServerUrlPrefix + casFilterUrlPattern;
// 退出的地址
public static final String logoutUrl = casLogoutUrl + "?service=" + casLogoutUrl;
// 失败的地址
public static final String unauthorizedUrl = "/403";
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 指定shiro
defaultWebSecurityManager.setRealm(myShiroCasRealm());
// 指定subjectFactory,如果实现的cas的remember me(免登录) 的功能,
defaultWebSecurityManager.setSubjectFactory(new CasSubjectFactory());
return defaultWebSecurityManager;
}
@Bean
public MyShiroCasRealm myShiroCasRealm() {
MyShiroCasRealm myShiroCasRealm = new MyShiroCasRealm();
// 设置cas登录服务器地址的前缀
myShiroCasRealm.setCasServerUrlPrefix(casServerUrlPrefix);
// 客户端回调地址,登录成功后的跳转的地址(自己的服务器)
myShiroCasRealm.setCasService(shiroServerUrlPrefix + casFilterUrlPattern);
return myShiroCasRealm;
}
// 注册单点登出的listener
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
bean.setListener(new SingleSignOutHttpSessionListener());
bean.setEnabled(true);
return bean;
}
// 注册单点登出filter
@Bean
public FilterRegistrationBean registrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setName("registrationBean");
registrationBean.setFilter(new SingleSignOutFilter());
registrationBean.addUrlPatterns("/*");//拦截所有的请求
registrationBean.setEnabled(true);
registrationBean.setOrder(10);//设置优先级
return registrationBean;
}
// 注册DelegatingFilterProxy(Shiro)
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new DelegatingFilterProxy("shiroFilter")); //设置的shiro的拦截器 ShiroFilterFactoryBean
bean.addInitParameter("targetFilterLifecycle", "true");
bean.setEnabled(true);
bean.addUrlPatterns("/*");
return bean;
}
// 该类可以保证实现了org.apache.shiro.util.Initializable接口的shiro对象的init或者是destory方法被自动调用,
// 而不用手动指定init-method或者是destory-method方法
// 注意:如果使用了该类,则不需要手动指定初始化方法和销毁方法,否则会出错
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
// 下面两个配置主要用来开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
// 设置代理方式,true是cglib的代理方式,false是普通的jdk代理方式
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
// 开启注解
@Bean
public AuthorizationAttributeSourceAdvisor attributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// 使用工厂模式,创建并初始化ShiroFilter
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager, CasFilter casFilter) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 如果不设置,会自动寻找目录下的/login.jsp页面
factoryBean.setLoginUrl(loginUrl);
factoryBean.setSuccessUrl("/index");
// 设置无权限访问页面
factoryBean.setUnauthorizedUrl(unauthorizedUrl);
// 添加casFilter中,注意,casFilter需要放到shiroFilter的前面
Map linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("casFilter", casFilter);
factoryBean.setFilters(linkedHashMap);
loadShiroFilterChain(factoryBean);
return factoryBean;
}
// CAS过滤器
@Bean(name = "casFilter")
public CasFilter getCasFilter() {
MyCasFilter casFilter = new MyCasFilter();
casFilter.setName("casFilter");
casFilter.setEnabled(true);
casFilter.setFailureUrl(loginUrl);
// casFilter.setLoginUrl(loginUrl);
// casFilter.setLoginUrl("/index");
casFilter.setSuccessUrl("/index");
return casFilter;
}
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
Map filterChainDefinitionMap = new LinkedHashMap();
filterChainDefinitionMap.put(casFilterUrlPattern, "casFilter");
//2.不拦截的请求
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
// filterChainDefinitionMap.put("/login", "anon");
// filterChainDefinitionMap.put("/index", "anon");
// filterChainDefinitionMap.put("/verify", "anon");
// 此处将logout页面设置为anon,而不是logout,因为logout被单点处理,而不需要再被shiro的logoutFilter进行拦截
filterChainDefinitionMap.put("/logout", "anon");
filterChainDefinitionMap.put("/error", "anon");
//3.拦截的请求(从本地数据库获取或者从casserver获取(webservice,http等远程方式),看你的角色权限配置在哪里)
filterChainDefinitionMap.put("/user", "authc"); //需要登录
//4.登录过的不拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
}
十一、配置controller类
package springboot.controller;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Controller
public class JspController {
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping(value="logout",method =RequestMethod.GET)
public String logout(){
//退出
//return "redirect:http://localhost:8080/cas/logout";
// 退出登录后,跳转到退出成功的页面,不走默认页面
return "redirect:http://localhost:8080/logout?service=http://localhost:8010/";
}
@RequestMapping(value="403")
public String unAuth(){
return "403";
}
@RequestMapping("/login")
public String login() {
return "login";
}
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
十二、整体项目结构如下,其中login.jsp其实没用到,登录页面用的是cas server的
十三、复制boot-cas-shiro项目,修改名称为boot-cas-shiro-b,修改springboot的端口号
name value
server.port 8020
十四、修改pom.xml里
十五、修改ShiroCasConfiguration,地址端口改成自己的
十六、修改JspController登出重定向的端口号
十七、测试
访问a系统localhost:8010会跳转localhost:8080/cas/login?service=http://localhost:8010/shiro_cas
登录后跳转
点击跳转b系统
点击退出登录
注:application.properties配置文件里的server.session.timeout属性,默认session失效是300,单位是分钟