springboot+shiro+cas5.2实现SSO单点登录(超详细)

cas的工作原理(图是从百度拿的)

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第1张图片

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文件

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第2张图片

四、把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服务端搭建完成

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第3张图片

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第4张图片

七、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 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的

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第5张图片

十三、复制boot-cas-shiro项目,修改名称为boot-cas-shiro-b,修改springboot的端口号

name        value
server.port 8020

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第6张图片

十四、修改pom.xml里

名称改为

十五、修改ShiroCasConfiguration,地址端口改成自己的

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第7张图片

十六、修改JspController登出重定向的端口号

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第8张图片

十七、测试

访问a系统localhost:8010会跳转localhost:8080/cas/login?service=http://localhost:8010/shiro_cas

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第9张图片

登录后跳转

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第10张图片

点击跳转b系统

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第11张图片

点击退出登录

springboot+shiro+cas5.2实现SSO单点登录(超详细)_第12张图片

注:application.properties配置文件里的server.session.timeout属性,默认session失效是300,单位是分钟

你可能感兴趣的:(springboot+shiro+cas5.2实现SSO单点登录(超详细))