shiro作为一个安全框架,在它的 1.2 版本开始就已经有了对 cas 的支持,所以用shiro来做客户端的安全框架对于我们的cas来说是非常方便的。
准备工作做好,首先新建一个Springboot 工程添加以下依赖项。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<!--<version>1.5.8.RELEASE</version>-->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xc</groupId>
<artifactId>cas-shiro-client1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cas-shiro-client1</name>
<description>Demo project for Spring Boot</description>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<shiro.version>1.2.3</shiro.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
<!-- Shiro begin -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- Shiro end -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
依赖项配置完成之后,就可以开始添加对Shiro以及单点登出过滤器的配置;首先先将要准备的配置属性都添加到配置文件中 application.properties 中 , 另外 我在hosts文件中添加了 127.0.0.1 shiro1.cas.com
server.servlet.context-path=/shirocas
server.port=8081
cas.server-url-prefix=http://server.cas.com:8080/cas
cas.server-login-url=http://server.cas.com:8080/cas/login
cas.client-host-url=http://shiro1.cas.com:8081
cas.logout.url=http://server.cas.com:8080/cas/logout?service=http://shiro1.cas.com:8081/shirocas
cas.validation-type=CAS
cas.shiro.login-url=http://server.cas.com:8080/cas/login?service=http://shiro1.cas.com:8081/shirocas/cas
csa.shiro.logout-url=http://server.cas.com:8080/cas/logout?service=http://shiro1.cas.com:8081/shirocas
cas.shiro.cas-service=http://shiro1.cas.com:8081/shirocas/cas
cas.shiro.role-attr=roles
cas.shiro.permission-attr=permissions
添加一个配置类用于访问该配置文件
package com.xc.cas.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.validation.constraints.NotNull;
@ConfigurationProperties(prefix = "cas")
public class CasProperties {
/**
* CAS server URL E.g. https://example.com/cas or https://cas.example. Required.
* CAS 服务端 url 不能为空
*/
@NotNull
private String serverUrlPrefix;
/**
* CAS server login URL E.g. https://example.com/cas/login or https://cas.example/login. Required.
* CAS 服务端登录地址 上面的连接 加上/login 该参数不能为空
*/
@NotNull
private String serverLoginUrl;
/**
* CAS-protected client application host URL E.g. https://myclient.example.com Required.
* 当前客户端的地址
*/
@NotNull
private String clientHostUrl;
/**
* 忽略规则,访问那些地址 不需要登录
*/
private String ignorePattern;
/**
* 自定义UrlPatternMatcherStrategy验证
*/
private String ignoreUrlPatternType;
private Shiro shiro;
public String getServerUrlPrefix() {
return serverUrlPrefix;
}
public void setServerUrlPrefix(String serverUrlPrefix) {
this.serverUrlPrefix = serverUrlPrefix;
}
public String getServerLoginUrl() {
return serverLoginUrl;
}
public void setServerLoginUrl(String serverLoginUrl) {
this.serverLoginUrl = serverLoginUrl;
}
public String getClientHostUrl() {
return clientHostUrl;
}
public void setClientHostUrl(String clientHostUrl) {
this.clientHostUrl = clientHostUrl;
}
public String getIgnorePattern() {
return ignorePattern;
}
public void setIgnorePattern(String ignorePattern) {
this.ignorePattern = ignorePattern;
}
public String getIgnoreUrlPatternType() {
return ignoreUrlPatternType;
}
public void setIgnoreUrlPatternType(String ignoreUrlPatternType) {
this.ignoreUrlPatternType = ignoreUrlPatternType;
}
public Shiro getShiro() {
return shiro;
}
public void setShiro(Shiro shiro) {
this.shiro = shiro;
}
public static class Shiro{
@NotNull
private String loginUrl;
@NotNull
private String logoutUrl;
@NotNull
private String casService;
private String roleAttr;
private String permissionAttr;
public String getLoginUrl() {
return loginUrl;
}
public void setLoginUrl(String loginUrl) {
this.loginUrl = loginUrl;
}
public String getLogoutUrl() {
return logoutUrl;
}
public void setLogoutUrl(String logoutUrl) {
this.logoutUrl = logoutUrl;
}
public String getRoleAttr() {
return roleAttr;
}
public void setRoleAttr(String roleAttr) {
this.roleAttr = roleAttr;
}
public String getPermissionAttr() {
return permissionAttr;
}
public void setPermissionAttr(String permissionAttr) {
this.permissionAttr = permissionAttr;
}
public String getCasService() {
return casService;
}
public void setCasService(String casService) {
this.casService = casService;
}
}
}
配置属性添加好之后,现在开始着手对Shiro的配置,都是一些常规的配置。这里配置了一个CasRealm,用于验证对于CAS客户端传过来的ticket进行验证,其中我添加了roleAttributeName和permissionAttributeName这两个属性,他们的作用是将CAS 服务端传过来与之对应的属性,解析为这个用户的角色和权限,多个角色和权限默认是以逗号分隔。
package com.xc.cas.config;
import com.xc.cas.app.CustomCasRealm;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
@EnableConfigurationProperties(CasProperties.class)
public class ShiroConfig {
@Autowired
private CasProperties casProperties;
/**
* security manager
* @return
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(@Qualifier("shiroCacheManager")EhCacheManager shiroCacheManager,@Qualifier("casRealm")Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setCacheManager(shiroCacheManager);
securityManager.setSubjectFactory(casSubjectFactory());
return securityManager;
}
/**
* cas subject factory
* @return
*/
@Bean
public CasSubjectFactory casSubjectFactory(){
return new CasSubjectFactory();
}
/**
* shiro ehcache manager
* @return
*/
@Bean("shiroCacheManager")
public EhCacheManager shiroCacheManager(@Qualifier("ehCacheManagerFactoryBean") EhCacheManagerFactoryBean ehCacheManagerFactoryBean){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManager(ehCacheManagerFactoryBean.getObject());
return cacheManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl(casProperties.getShiro().getLoginUrl());
shiroFilter.setSuccessUrl("/");
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("cas",casFilter());
Map<String, String> filterChainDefinitions = new LinkedHashMap<>();
filterChainDefinitions.put("/login/failure","anon");
filterChainDefinitions.put("/cas","cas");
filterChainDefinitions.put("/**","user");
shiroFilter.setFilters(filters);
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitions);
return shiroFilter;
}
@Bean
public CasFilter casFilter(){
CasFilter filter = new CasFilter();
filter.setFailureUrl("/login/failure");
return filter;
}
@Bean("casRealm")
public CasRealm casRealm(){
CasRealm realm = new CasRealm();
realm.setName("casRealm");
realm.setPermissionAttributeNames(casProperties.getShiro().getPermissionAttr());
realm.setRoleAttributeNames(casProperties.getShiro().getRoleAttr());
realm.setCasServerUrlPrefix(casProperties.getServerUrlPrefix());
realm.setCasService(casProperties.getShiro().getCasService());
return realm;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
还有三个bean的配置我放到了另外一个配置文件中去了,第一个bean用于装载ehcache,后两个用于开始shiro的权限注解
package com.xc.cas.config;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
@Configuration
public class Config {
/**
* ehcache
* @return
*/
@Bean("ehCacheManagerFactoryBean")
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean(){
EhCacheManagerFactoryBean cacheManager = new EhCacheManagerFactoryBean();
cacheManager.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:ehcache.xml"));
return cacheManager;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
接下来配置单点登出的过滤器
package com.xc.cas.config;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableConfigurationProperties(CasProperties.class)
public class CasLogoutConfig implements WebMvcConfigurer {
@Autowired
private CasProperties configProps;
/**
* 配置登出过滤器
* @return
*/
@Bean
public FilterRegistrationBean filterSingleRegistration() {
final FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new SingleSignOutFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
Map<String,String> initParameters = new HashMap<String, String>();
initParameters.put("casServerUrlPrefix", configProps.getServerUrlPrefix());
registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(1);
return registration;
}
/**
* request wraper过滤器
* @return
*/
@Bean
public FilterRegistrationBean filterWrapperRegistration() {
final FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new HttpServletRequestWrapperFilter());
// 设定匹配的路径
registration.addUrlPatterns("/*");
// 设定加载的顺序
registration.setOrder(4);
return registration;
}
/**
* 添加监听器
* @return
*/
@Bean
public ServletListenerRegistrationBean<EventListener> singleSignOutListenerRegistration(){
ServletListenerRegistrationBean<EventListener> registrationBean = new ServletListenerRegistrationBean<EventListener>();
registrationBean.setListener(new SingleSignOutHttpSessionListener());
registrationBean.setOrder(1);
return registrationBean;
}
}
之后再建立一个Controller进行测试即可
@GetMapping("/")
@ResponseBody
public String index(){
return "shiro cas client inde1";
}
@RequiresPermissions("admin:view")
@GetMapping("/index")
@ResponseBody
public String index2(){
return "shiro cas client index2";
}
@GetMapping("/login/failure")
@ResponseBody
public String loginFailure(){
return "login fail....";
}
@GetMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:http://server.cas.com:8080/cas/logout?service=http://shiro1.cas.com:8081/shirocas";
}
项目启动之后,可能打印以下 Bean 'beanName' of type [package.className] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)信息,我也是弄了老半天都不知道为什么,后来发现其实对项目没有什么影响,所以就不管了。
项目成功启动之后,跳转到CAS服务端进行登录,登录生成ticket之后会进入 CasRealm的 doGetAuthenticationInfo 方法,认证通过则跳转到你 service 参数的接口 。之前,我在CasRealm中设置了roleAtributeName和permissionAttributeName属性,此时CAS 服务端并没有将这些信息返回(因为服务端还没改),而且一般各个客户端都有本身应用下的角色和权限,所以还需要自身去进行授权。自定义授权也很简单,就是新建一个Realm继承CasRealm,然后重写doGetAuthorizationInfo方法,将其添加到securityManager中即可。
问题:不进入doGetAuthorizationInfo方法?
在执行完doGetAuthenticationInfo 方法之后并没有进入doGetAuthorizationInfo方法,就直接跳转到service的接口去了。对此研究了很久,我一直以为我是lifecycleBeanPostProcessor 和 defaultAdvisorAutoProxyCreator这两个bean没加导致的,后来发现并不是。shiro是在你的客户端程序中需要用到角色或者权限时,才会进入doGetAuthorizationInfo方法进行授权,比如遇到 subject.hasRole 等方法或者 @RequireRoles 等注解的时候,才会进行授权。
问题:登出为什么不直接采用logout过滤器?
我之前也是直接使用的logout过滤器,但是会出一些莫名其妙的问题,比如无法成功登录的问题。所以我才直接将登出写成接口。