f1c07afdb7345d8d4726c3eb63b65ee7.png

前言

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

引入Shiro-cas包

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

1<!-- shiro spring. -->
 2<dependency>
 3    <groupId>org.apache.shiro</groupId>
 4    <artifactId>shiro-spring</artifactId>
 5    <version>1.2.6</version>
 6</dependency>
 7
 8<!-- shiro ehcache -->
 9<dependency>
10    <groupId>org.apache.shiro</groupId>
11    <artifactId>shiro-ehcache</artifactId>
12    <version>1.2.6</version>
13</dependency>
14<!-- shiro cas -->
15<dependency>
16    <groupId>org.apache.shiro</groupId>
17    <artifactId>shiro-cas</artifactId>
18    <version>1.2.6</version>
19</dependency>

前两个一个是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的这两个类:
首先是设置类:

1import com.dhcc.pa.domain.SPermission;
  2import com.dhcc.pa.other.shiro.MyShiroCasRealm;
  3import com.dhcc.pa.service.SystemService;
  4import com.dhcc.pa.util.PublicMsg;
  5import org.apache.shiro.cache.ehcache.EhCacheManager;
  6import org.apache.shiro.cas.CasFilter;
  7import org.apache.shiro.cas.CasSubjectFactory;
  8import org.apache.shiro.spring.LifecycleBeanPostProcessor;
  9import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
 10import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
 11import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
 12import org.slf4j.Logger;
 13import org.slf4j.LoggerFactory;
 14import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
 15import org.springframework.boot.web.servlet.FilterRegistrationBean;
 16import org.springframework.context.annotation.Bean;
 17import org.springframework.context.annotation.Configuration;
 18import org.springframework.util.StringUtils;
 19import org.springframework.web.filter.DelegatingFilterProxy;
 20
 21import javax.servlet.Filter;
 22import java.util.HashMap;
 23import java.util.LinkedHashMap;
 24import java.util.List;
 25import java.util.Map;
 26
 27@Configuration
 28public class ShiroConfig {
 29
 30    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
 31
 32    // Cas登录页面地址
 33    public static final String casLoginUrl = PublicMsg.CASServerUrlPrefix + "/login";
 34    // Cas登出页面地址
 35    public static final String casLogoutUrl = PublicMsg.CASServerUrlPrefix  + "/logout";
 36
 37    // casFilter UrlPattern
 38    public static final String casFilterUrlPattern = "/";
 39    // 登录地址
 40    public static final String loginUrl = casLoginUrl + "?service=" + PublicMsg.SHIROServerUrlPrefix + casFilterUrlPattern;
 41    // 登出地址(casserver启用service跳转功能,需在webapps\cas\WEB-INF\cas.properties文件中启用cas.logout.followServiceRedirects=true)
 42    public static final String logoutUrl = casLogoutUrl+"?service="+loginUrl;
 43
 44    @Bean
 45    public EhCacheManager getEhCacheManager() {
 46        EhCacheManager em = new EhCacheManager();
 47        em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
 48        return em;
 49    }
 50
 51    @Bean(name = "myShiroCasRealm")
 52    public MyShiroCasRealm myShiroCasRealm(EhCacheManager cacheManager) {
 53        MyShiroCasRealm realm = new MyShiroCasRealm();
 54        realm.setCacheManager(cacheManager);
 55        return realm;
 56    }
 57
 58    /**
 59     * 注册DelegatingFilterProxy(Shiro)
 60     *
 61     * @param
 62     * @return
 65     */
 66    @Bean
 67    public FilterRegistrationBean filterRegistrationBean() {
 68        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
 69        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
 70        //  该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
 71        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
 72        filterRegistration.setEnabled(true);
 73        filterRegistration.addUrlPatterns("/*");
 74        return filterRegistration;
 75    }
 76
 77    @Bean(name = "lifecycleBeanPostProcessor")
 78    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
 79        return new LifecycleBeanPostProcessor();
 80    }
 81
 82    @Bean
 83    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
 84        DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
 85        daap.setProxyTargetClass(true);
 86        return daap;
 87    }
 88
 89    @Bean(name = "securityManager")
 90    public DefaultWebSecurityManager getDefaultWebSecurityManager(MyShiroCasRealm myShiroCasRealm) {
 91        DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
 92        dwsm.setRealm(myShiroCasRealm);
 93//      <!-- 用户授权/认证信息Cache, 采用EhCache 缓存 -->
 94        dwsm.setCacheManager(getEhCacheManager());
 95        // 指定 SubjectFactory
 96        dwsm.setSubjectFactory(new CasSubjectFactory());
 97        return dwsm;
 98    }
 99
100    @Bean
101    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
102        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
103        aasa.setSecurityManager(securityManager);
104        return aasa;
105    }
106
107
108    /**
109     * CAS过滤器
110     *
111     * @return
114     */
115    @Bean(name = "casFilter")
116    public CasFilter getCasFilter() {
117        CasFilter casFilter = new CasFilter();
118        casFilter.setName("casFilter");
119        casFilter.setEnabled(true);
120        // 登录失败后跳转的URL,也就是 Shiro 执行 CasRealm 的 doGetAuthenticationInfo 方法向CasServer验证tiket
121        casFilter.setFailureUrl(loginUrl);// 我们选择认证失败后再打开登录页面
122        return casFilter;
123    }
124
125    /**
126     * ShiroFilter<br/>
127     * 注意这里参数中的 StudentService 和 IScoreDao 只是一个例子,因为我们在这里可以用这样的方式获取到相关访问数据库的对象,
128     * 然后读取数据库相关配置,配置到 shiroFilterFactoryBean 的访问规则中。实际项目中,请使用自己的Service来处理业务逻辑。
129     *
130     * @param
131     * @param
132     * @param
133     * @return
136     */
137    @Bean
138    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager, CasFilter casFilter,SystemService sysPermissionInitService) {
139        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
140        // 必须设置 SecurityManager
141        shiroFilterFactoryBean.setSecurityManager(securityManager);
142        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
143        shiroFilterFactoryBean.setLoginUrl(loginUrl);
144        // 登录成功后要跳转的连接
145        shiroFilterFactoryBean.setSuccessUrl("/templete");
146        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
147
148        // 添加casFilter到shiroFilter中
149        Map<String, Filter> filters = new HashMap<>();
150        filters.put("casFilter", casFilter);
151        shiroFilterFactoryBean.setFilters(filters);
152        /// 下面这些规则配置最好配置到配置文件中 ///
153        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
154
155        filterChainDefinitionMap.put(casFilterUrlPattern, "casFilter");// shiro集成cas后,首先添加该规则
156
157        // authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
158        // anon:它对应的过滤器里面是空的,什么都没做
159        logger.info("##################从数据库读取权限规则,加载到shiroFilter中##################");
160        filterChainDefinitionMap.put("/js/**", "anon");
161        filterChainDefinitionMap.put("/css/**", "anon");
162        filterChainDefinitionMap.put("/bootstrapDatePicker/**", "anon");
163        //阻止登录成功后下载favicon
164        filterChainDefinitionMap.put("/favicon.ico", "anon");
165
166        //从数据库获取
167        List<SPermission> list = sysPermissionInitService.menuGetAll();
168
169        for (SPermission sysPermissionInit : list) {
170            if(!StringUtils.isEmpty(sysPermissionInit.getUrl())){
171                filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
172                        "perms["+sysPermissionInit.getPermission()+"]");
173            }
174        }
175        //配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
176        filterChainDefinitionMap.put(logoutUrl, "logout");
177        filterChainDefinitionMap.put("/**", "authc");
178        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
179        return shiroFilterFactoryBean;
180    }
181
182}

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

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

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

1import com.dhcc.pa.domain.Role;
 2import com.dhcc.pa.domain.SUser;
 3import com.dhcc.pa.other.config.ShiroConfig;
 4import com.dhcc.pa.service.UserService;
 5import com.dhcc.pa.util.PublicMsg;
 6import org.apache.shiro.authz.AuthorizationInfo;
 7import org.apache.shiro.authz.SimpleAuthorizationInfo;
 8import org.apache.shiro.cas.CasRealm;
 9import org.apache.shiro.subject.PrincipalCollection;
10import org.slf4j.Logger;
11import org.slf4j.LoggerFactory;
12import org.springframework.beans.factory.annotation.Autowired;
13import org.springframework.util.StringUtils;
14
15import javax.annotation.PostConstruct;
16import java.util.List;
17
18
24public class MyShiroCasRealm extends CasRealm {
25
26    private static final Logger logger = LoggerFactory.getLogger(MyShiroCasRealm.class);
27
28    @Autowired
29    private UserService userService;
30
31    @PostConstruct
32    public void initProperty(){
33//      setDefaultRoles("ROLE_USER");
34        setCasServerUrlPrefix(PublicMsg.CASServerUrlPrefix);
35        // 客户端回调地址
36        setCasService(PublicMsg.SHIROServerUrlPrefix + ShiroConfig.casFilterUrlPattern);
37    }
38
39    /**
40     * 权限认证,为当前登录的Subject授予角色和权限
41     * @see :本例中该方法的调用时机为需授权资源被访问时
42     * @see :并且每次访问需授权资源时都会执行该方法中的逻辑,这表明本例中默认并未启用AuthorizationCache
43     * @see :如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),超过这个时间间隔再刷新页面,该方法会被执行
44     */
45    @Override
46    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
47        logger.info("##################执行Shiro权限认证##################");
48        //获取用户的输入的账号.
49        //获取当前登录输入的用户名,等价于(String) principalCollection.fromRealm(getName()).iterator().next();
50        String username = (String)super.getAvailablePrincipal(principalCollection);
51        //到数据库查是否有此对象
52        List<SUser> userList = userService.findByUsername(username);
53        System.out.println("----->>userInfo=" + userList.size());
54        if (userList.size()==0) {
55            return null;
56        }
57
58        //账号判断;
59        //凌海天2017 -11-14 修改
60        SUser user= userList.get(0);
61        if(user!=null){
62            //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
63            SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
64            int id  = user.getId().intValue();
65            //凌海天2017 -11-14 修改
66            List<Role> role = userService.findByUserid(id);
67            for (Role r :role){
68                //用户的角色集合
69                if(!StringUtils.isEmpty(r.getRole())){
70                    info.addRole(r.getRole());
71                }
72                //用户的角色对应的所有权限,如果只使用角色定义访问权限
73                if(!StringUtils.isEmpty(r.getPermission())){
74                    info.addStringPermission(r.getPermission());
75                }
76            }
77            // 或者按下面这样添加
78            //添加一个角色,不是配置意义上的添加,而是证明该用户拥有admin角色
79//            simpleAuthorInfo.addRole("admin");
80            //添加权限
81//            simpleAuthorInfo.addStringPermission("admin:manage");
82//            logger.info("已为用户[mike]赋予了[admin]角色和[admin:manage]权限");
83            return info;
84        }
85        // 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到unauthorizedUrl指定的地址
86        return null;
87    }
88}

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

1//CAS服务器地址
2public static final String CASServerUrlPrefix = "http://xxx.xx.xx.xxx:9092/cas";
3// 当前工程对外提供的服务地址
4public 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路径,它的界面如下:

ed0e2baf04de6e911bfb6aac4f7865e9.png

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

获取用户信息

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

1Subject currentUser = SecurityUtils.getSubject();
2String username = currentUser.getPrincipal().toString();

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

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

1#多属性
 2cas.authn.attributeRepository.jdbc[0].singleRow=true
 3cas.authn.attributeRepository.jdbc[0].order=0
 4cas.authn.attributeRepository.jdbc[0].url=jdbc:mysql://172.18.18.25:3306/pa_db?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
 5cas.authn.attributeRepository.jdbc[0].username=username
 6cas.authn.attributeRepository.jdbc[0].user=root
 7cas.authn.attributeRepository.jdbc[0].password=1234
 8cas.authn.attributeRepository.jdbc[0].sql=select * from s_user where {0}
 9cas.authn.attributeRepository.jdbc[0].dialect=org.hibernate.dialect.MySQLDialect
10cas.authn.attributeRepository.jdbc[0].ddlAuto=none
11cas.authn.attributeRepository.jdbc[0].driverClass=com.mysql.jdbc.Driver
12cas.authn.attributeRepository.jdbc[0].leakThreshold=10
13cas.authn.attributeRepository.jdbc[0].propagationBehaviorName=PROPAGATION_REQUIRED
14cas.authn.attributeRepository.jdbc[0].batchSize=1
15cas.authn.attributeRepository.jdbc[0].healthQuery=SELECT 1
16cas.authn.attributeRepository.jdbc[0].failFast=trueyeshi

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

1AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
2final Map attributes = principal.getAttributes();

后记

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

  1. 设置类中的Service引入方式

  2. POM.xml中更改组件版本号一定要剪切黏贴,不要直接修改版本号

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

  1. 修改CAS服务端的默认登录页

  2. 如何登出CAS客户端

1source:jasoncool.github.io/2017/12/04/Springboot集成Shiro和Cas实现单点登录-客户端篇

8f0d784d0681bddf8a01e3028dd176fe.png

喜欢,在看

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐