这里我先要说明一下,我们的项目架构是Springboot+Shiro+Ehcache+ThymeLeaf+Mybaits,在这个基础上,我们再加入了CAS单点登录,虽然前面的框架看着很长,但是和单点登录相关的核心架构其实就是Springboot和Shiro而已,所以在看这篇文章之前,需要你掌握的知识有Springboot的基础框架搭建以及集成Shiro后的一些操作,因为之后的集成CAS其实也是在这个基础上进行的修改。
需要集成CAS那么肯定要引入CAS相关的组件包,在POM.xml中引入:
<!-- shiro spring. -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.6</version>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.6</version>
</dependency>
<!-- shiro cas -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.2.6</version>
</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的这两个类: 首先是设置类:
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;
@Configuration
public 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跳转功能,需在webapps\cas\WEB-INF\cas.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);
// <!-- 用户授权/认证信息Cache, 采用EhCache 缓存 -->
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<br/>
* 注意这里参数中的 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<String, Filter> filters = new HashMap<>();
filters.put("casFilter", casFilter);
shiroFilterFactoryBean.setFilters(filters);
/////////////////////// 下面这些规则配置最好配置到配置文件中 ///////////////////////
Map<String, String> 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<SPermission> 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空指针的异常,至于原因我也不是很清楚….待查
之后是登录验证和权限获取类:
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<SUser> 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> 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的服务端路径和本项目的对外路径,其实就两个参数:
//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,只要你登录成功,就会把服务端的用户名传递给客户端,客户端通过:
Subject currentUser = SecurityUtils.getSubject();
String username = currentUser.getPrincipal().toString();
这两行代码就可以获取到登录用户的用户名,然后再通过自己写的通过用户名获取用户信息的Service就可以获取到相关的用户信息了,这里应该不难理解。
至于获取用户的多属性,就要结合到之前的服务端的设置了,首先你要在服务端设置如下参数:
#多属性
cas.authn.attributeRepository.jdbc[0].singleRow=true
cas.authn.attributeRepository.jdbc[0].order=0
cas.authn.attributeRepository.jdbc[0].url=jdbc:mysql://172.18.18.25:3306/pa_db?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
cas.authn.attributeRepository.jdbc[0].username=username
cas.authn.attributeRepository.jdbc[0].user=root
cas.authn.attributeRepository.jdbc[0].password=1234
cas.authn.attributeRepository.jdbc[0].sql=select * from s_user where {0}
cas.authn.attributeRepository.jdbc[0].dialect=org.hibernate.dialect.MySQLDialect
cas.authn.attributeRepository.jdbc[0].ddlAuto=none
cas.authn.attributeRepository.jdbc[0].driverClass=com.mysql.jdbc.Driver
cas.authn.attributeRepository.jdbc[0].leakThreshold=10
cas.authn.attributeRepository.jdbc[0].propagationBehaviorName=PROPAGATION_REQUIRED
cas.authn.attributeRepository.jdbc[0].batchSize=1
cas.authn.attributeRepository.jdbc[0].healthQuery=SELECT 1
cas.authn.attributeRepository.jdbc[0].failFast=trueyeshi
以上代码就允许用户返回服务端的s_user 数据库表中的所有字段,当然你再客户端的写法也要跟着改变:
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
final Map attributes = principal.getAttributes();
CAS客户端的配置差不多就是这样了,注释写的都比较明白了,需要注意的坑有以下两点:
剩下的大家看着文章一步一步的走出来应该问题就不大了,下一篇我们讲两个小的内容:
source:jasoncool.github.io/2017/12/04/Springboot集成Shiro和Cas实现单点登录-客户端篇