知方号

知方号

Springboot 集成 Shiro 和 CAS 实现单点登录(客户端)<多系统集成 单点登录怎么操作>

前言

这里我先要说明一下,我们的项目架构是Springboot+Shiro+Ehcache+ThymeLeaf+Mybaits,在这个基础上,我们再加入了CAS单点登录,虽然前面的框架看着很长,但是和单点登录相关的核心架构其实就是Springboot和Shiro而已,所以在看这篇文章之前,需要你掌握的知识有Springboot的基础框架搭建以及集成Shiro后的一些操作,因为之后的集成CAS其实也是在这个基础上进行的修改。

引入Shiro-cas包

需要集成CAS那么肯定要引入CAS相关的组件包,在POM.xml中引入:

代码语言:javascript复制 org.apache.shiro shiro-spring 1.2.6 org.apache.shiro shiro-ehcache 1.2.6 org.apache.shiro shiro-cas 1.2.6

前两个一个是Spring和Shiro结合的shiro-spring包和与ehcache结合的shiro-ehcache包,这两个包应该是之前就有的,之所以也把他们写进来是因为如果要引入CAS的组件包,需要保证这三个包的版本号一致,笔者之前引入的前两个包的版本号是1.2.4,结果单独引入1.2.6的shiro-cas包后,一些cas关键的类是找不到的,所以这里尽量保持这三个引入包的版本号一致。

小插曲我在升级1.2.4的shiro-spring和shiro-ehcache这连个组件包的时候,是直接修改的1.2.4为1.2.6,但是引入一直报错,尝试了各种办法都不行,后来发现,你需要剪切该引入包的dependency再黏贴到pom中去,不能直接修改版本号,否则会出现引入不成功的问题,这个问题卡了我一下午,坑啊!

加入单点登录的配置

如果你在你的Springboot项目中集成过shiro框架,应该对两个自定义的类不陌生,一个是myShiroConfig另一个是myShiroRealm,这两个类其实就是用户自定义的Shiro的设置类和登录验证获取权限的管理类,在这里我将不再赘述该类如何使用,直接上集成了CAS的这两个类:首先是设置类:

代码语言:javascript复制import com.dhcc.pa.domain.SPermission;import com.dhcc.pa.other.shiro.MyShiroCasRealm;import com.dhcc.pa.service.SystemService;import com.dhcc.pa.util.PublicMsg;import org.apache.shiro.cache.ehcache.EhCacheManager;import org.apache.shiro.cas.CasFilter;import org.apache.shiro.cas.CasSubjectFactory;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.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.util.StringUtils;import org.springframework.web.filter.DelegatingFilterProxy;import javax.servlet.Filter;import java.util.HashMap;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;@Configurationpublic class ShiroConfig { private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class); // Cas登录页面地址 public static final String casLoginUrl = PublicMsg.CASServerUrlPrefix + "/login"; // Cas登出页面地址 public static final String casLogoutUrl = PublicMsg.CASServerUrlPrefix + "/logout"; // casFilter UrlPattern public static final String casFilterUrlPattern = "/"; // 登录地址 public static final String loginUrl = casLoginUrl + "?service=" + PublicMsg.SHIROServerUrlPrefix + casFilterUrlPattern; // 登出地址(casserver启用service跳转功能,需在webappscasWEB-INFcas.properties文件中启用cas.logout.followServiceRedirects=true) public static final String logoutUrl = casLogoutUrl+"?service="+loginUrl; @Bean public EhCacheManager getEhCacheManager() { EhCacheManager em = new EhCacheManager(); em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml"); return em; } @Bean(name = "myShiroCasRealm") public MyShiroCasRealm myShiroCasRealm(EhCacheManager cacheManager) { MyShiroCasRealm realm = new MyShiroCasRealm(); realm.setCacheManager(cacheManager); return realm; } /** * 注册DelegatingFilterProxy(Shiro) * * @param * @return */ @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistration = new FilterRegistrationBean(); filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter")); // 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 filterRegistration.addInitParameter("targetFilterLifecycle", "true"); filterRegistration.setEnabled(true); filterRegistration.addUrlPatterns("/*"); return filterRegistration; } @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator(); daap.setProxyTargetClass(true); return daap; } @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(MyShiroCasRealm myShiroCasRealm) { DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager(); dwsm.setRealm(myShiroCasRealm);// dwsm.setCacheManager(getEhCacheManager()); // 指定 SubjectFactory dwsm.setSubjectFactory(new CasSubjectFactory()); return dwsm; } @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor(); aasa.setSecurityManager(securityManager); return aasa; } /** * CAS过滤器 * * @return */ @Bean(name = "casFilter") public CasFilter getCasFilter() { CasFilter casFilter = new CasFilter(); casFilter.setName("casFilter"); casFilter.setEnabled(true); // 登录失败后跳转的URL,也就是 Shiro 执行 CasRealm 的 doGetAuthenticationInfo 方法向CasServer验证tiket casFilter.setFailureUrl(loginUrl);// 我们选择认证失败后再打开登录页面 return casFilter; } /** * ShiroFilter * 注意这里参数中的 StudentService 和 IScoreDao 只是一个例子,因为我们在这里可以用这样的方式获取到相关访问数据库的对象, * 然后读取数据库相关配置,配置到 shiroFilterFactoryBean 的访问规则中。实际项目中,请使用自己的Service来处理业务逻辑。 * * @param * @param * @param * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager, CasFilter casFilter,SystemService sysPermissionInitService) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl(loginUrl); // 登录成功后要跳转的连接 shiroFilterFactoryBean.setSuccessUrl("/templete"); shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // 添加casFilter到shiroFilter中 Map filters = new HashMap(); filters.put("casFilter", casFilter); shiroFilterFactoryBean.setFilters(filters); /////////////////////// 下面这些规则配置最好配置到配置文件中 /////////////////////// Map filterChainDefinitionMap = new LinkedHashMap(); filterChainDefinitionMap.put(casFilterUrlPattern, "casFilter");// shiro集成cas后,首先添加该规则 // authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter // anon:它对应的过滤器里面是空的,什么都没做 logger.info("##################从数据库读取权限规则,加载到shiroFilter中##################"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/bootstrapDatePicker/**", "anon"); //阻止登录成功后下载favicon filterChainDefinitionMap.put("/favicon.ico", "anon"); //从数据库获取 List list = sysPermissionInitService.menuGetAll(); for (SPermission sysPermissionInit : list) { if(!StringUtils.isEmpty(sysPermissionInit.getUrl())){ filterChainDefinitionMap.put(sysPermissionInit.getUrl(), "perms["+sysPermissionInit.getPermission()+"]"); } } //配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put(logoutUrl, "logout"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }}

注释写的都比较清楚了, 我这里将不再赘述,这里只有一个知识点需要强调一下:

在这个设置类中如果需要从数据库获取用户的权限列表,一定要将对应的Service写在shiroFilter这个方法里当作一个参数来使用,而不能直接用@AutoWired将该类引入,否则使用时会报该Service空指针的异常,至于原因我也不是很清楚….待查

之后是登录验证和权限获取类:

代码语言:javascript复制import com.dhcc.pa.domain.Role;import com.dhcc.pa.domain.SUser;import com.dhcc.pa.other.config.ShiroConfig;import com.dhcc.pa.service.UserService;import com.dhcc.pa.util.PublicMsg;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.cas.CasRealm;import org.apache.shiro.subject.PrincipalCollection;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import javax.annotation.PostConstruct;import java.util.List;public class MyShiroCasRealm extends CasRealm { private static final Logger logger = LoggerFactory.getLogger(MyShiroCasRealm.class); @Autowired private UserService userService; @PostConstruct public void initProperty(){// setDefaultRoles("ROLE_USER"); setCasServerUrlPrefix(PublicMsg.CASServerUrlPrefix); // 客户端回调地址 setCasService(PublicMsg.SHIROServerUrlPrefix + ShiroConfig.casFilterUrlPattern); } /** * 权限认证,为当前登录的Subject授予角色和权限 * @see :本例中该方法的调用时机为需授权资源被访问时 * @see :并且每次访问需授权资源时都会执行该方法中的逻辑,这表明本例中默认并未启用AuthorizationCache * @see :如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),超过这个时间间隔再刷新页面,该方法会被执行 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { logger.info("##################执行Shiro权限认证##################"); //获取用户的输入的账号. //获取当前登录输入的用户名,等价于(String) principalCollection.fromRealm(getName()).iterator().next(); String username = (String)super.getAvailablePrincipal(principalCollection); //到数据库查是否有此对象 List userList = userService.findByUsername(username); System.out.println("----->>userInfo=" + userList.size()); if (userList.size()==0) { return null; } //账号判断; //凌海天2017 -11-14 修改 SUser user= userList.get(0); if(user!=null){ //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) SimpleAuthorizationInfo info=new SimpleAuthorizationInfo(); int id = user.getId().intValue(); //凌海天2017 -11-14 修改 List role = userService.findByUserid(id); for (Role r :role){ //用户的角色集合 if(!StringUtils.isEmpty(r.getRole())){ info.addRole(r.getRole()); } //用户的角色对应的所有权限,如果只使用角色定义访问权限 if(!StringUtils.isEmpty(r.getPermission())){ info.addStringPermission(r.getPermission()); } } // 或者按下面这样添加 //添加一个角色,不是配置意义上的添加,而是证明该用户拥有admin角色// simpleAuthorInfo.addRole("admin"); //添加权限// simpleAuthorInfo.addStringPermission("admin:manage");// logger.info("已为用户[mike]赋予了[admin]角色和[admin:manage]权限"); return info; } // 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到unauthorizedUrl指定的地址 return null; }}

这两个类中都用到了PublicMsg类,这个类里主要设置的是CAS的服务端路径和本项目的对外路径,其实就两个参数:

代码语言:javascript复制//CAS服务器地址public static final String CASServerUrlPrefix = "http://xxx.xx.xx.xxx:9092/cas";// 当前工程对外提供的服务地址public static final String SHIROServerUrlPrefix = "http://127.0.0.1:9091";

读者可以直接放置到设置类中,我这里单独提出来是因为我的项目专门有一个类管理这些参数而已。

查看效果

在启动CAS服务端的情况下,启动本项目,然后再浏览器中输入:http://localhost:9091浏览器的url路径会自动转化为:http://172.18.18.25:9092/cas/login?service=http://127.0.0.1:9091/这是一个CAS特有的URL路径,它的界面如下:

之后在这个界面登录正确的用户名和密码后,系统会自动跳转到项目的主页中去。

获取用户信息

在你不在服务端做任何设置的默认情况下,CAS服务端只会给客户端返回一个用户名,比如你的服务端的用户名是admin,只要你登录成功,就会把服务端的用户名传递给客户端,客户端通过:

代码语言:javascript复制Subject currentUser = SecurityUtils.getSubject();String username = currentUser.getPrincipal().toString();

这两行代码就可以获取到登录用户的用户名,然后再通过自己写的通过用户名获取用户信息的Service就可以获取到相关的用户信息了,这里应该不难理解。

至于获取用户的多属性,就要结合到之前的服务端的设置了,首先你要在服务端设置如下参数:

代码语言:javascript复制#多属性cas.authn.attributeRepository.jdbc[0].singleRow=truecas.authn.attributeRepository.jdbc[0].order=0cas.authn.attributeRepository.jdbc[0].url=jdbc:mysql://172.18.18.25:3306/pa_db?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.attributeRepository.jdbc[0].username=usernamecas.authn.attributeRepository.jdbc[0].user=rootcas.authn.attributeRepository.jdbc[0].password=1234cas.authn.attributeRepository.jdbc[0].sql=select * from s_user where {0}cas.authn.attributeRepository.jdbc[0].dialect=org.hibernate.dialect.MySQLDialectcas.authn.attributeRepository.jdbc[0].ddlAuto=nonecas.authn.attributeRepository.jdbc[0].driverClass=com.mysql.jdbc.Drivercas.authn.attributeRepository.jdbc[0].leakThreshold=10cas.authn.attributeRepository.jdbc[0].propagationBehaviorName=PROPAGATION_REQUIREDcas.authn.attributeRepository.jdbc[0].batchSize=1cas.authn.attributeRepository.jdbc[0].healthQuery=SELECT 1cas.authn.attributeRepository.jdbc[0].failFast=trueyeshi

以上代码就允许用户返回服务端的s_user 数据库表中的所有字段,当然你再客户端的写法也要跟着改变:

代码语言:javascript复制AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();final Map attributes = principal.getAttributes();后记

CAS客户端的配置差不多就是这样了,注释写的都比较明白了,需要注意的坑有以下两点:

设置类中的Service引入方式POM.xml中更改组件版本号一定要剪切黏贴,不要直接修改版本号

剩下的大家看着文章一步一步的走出来应该问题就不大了,下一篇我们讲两个小的内容:

修改CAS服务端的默认登录页如何登出CAS客户端代码语言:javascript复制source:jasoncool.github.io/2017/12/04/Springboot集成Shiro和Cas实现单点登录-客户端篇

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至lizi9903@foxmail.com举报,一经查实,本站将立刻删除。