Springboot 集成 Shiro 和 CAS 实现单点登录(客户端)
前言
这里我先要说明一下,我们的项目架构是Springboot+Shiro+Ehcache+ThymeLeaf+Mybaits,在这个基础上,我们再加入了CAS单点登录,虽然前面的框架看着很长,但是和单点登录相关的核心架构其实就是Springboot和Shiro而已,所以在看这篇文章之前,需要你掌握的知识有Springboot的基础框架搭建以及集成Shiro后的一些操作,因为之后的集成CAS其实也是在这个基础上进行的修改。
引入Shiro-cas包
需要集成CAS那么肯定要引入CAS相关的组件包,在POM.xml中引入:
1
2<dependency>
3 <groupId>org.apache.shirogroupId>
4 <artifactId>shiro-springartifactId>
5 <version>1.2.6version>
6dependency>
7
8
9<dependency>
10 <groupId>org.apache.shirogroupId>
11 <artifactId>shiro-ehcacheartifactId>
12 <version>1.2.6version>
13dependency>
14
15<dependency>
16 <groupId>org.apache.shirogroupId>
17 <artifactId>shiro-casartifactId>
18 <version>1.2.6version>
19dependency>
前两个一个是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//
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
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 filters = new HashMap<>();
150 filters.put("casFilter", casFilter);
151 shiroFilterFactoryBean.setFilters(filters);
152 /////////////////////// 下面这些规则配置最好配置到配置文件中 ///////////////////////
153 Map 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 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 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 = 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路径,它的界面如下:
之后在这个界面登录正确的用户名和密码后,系统会自动跳转到项目的主页中去。
获取用户信息
在你不在服务端做任何设置的默认情况下,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客户端的配置差不多就是这样了,注释写的都比较明白了,需要注意的坑有以下两点:
设置类中的Service引入方式
POM.xml中更改组件版本号一定要剪切黏贴,不要直接修改版本号
剩下的大家看着文章一步一步的走出来应该问题就不大了,下一篇我们讲两个小的内容:
修改CAS服务端的默认登录页
如何登出CAS客户端
1source:jasoncool.github.io/2017/12/04/Springboot集成Shiro和Cas实现单点登录-客户端篇
喜欢,在看