1. 程式人生 > >spring boot整合Shiro實現單點登入

spring boot整合Shiro實現單點登入

前面的部落格中,我們說道了Shiro的兩個最大的特點,認證和授權,而單點登入也是屬於認證的一部分,預設情況下,Shiro已經為我們實現了和Cas的整合,我們加入整合的一些配置就ok了。

1、加入shiro-cas包

<!-- shiro整合cas單點 -->
		<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>1.2.4</version>
        </dependency>
2、加入單點登入的配置

這裡,我將所有的配置都貼出來,方便參考,配置裡面已經加了詳盡的說明。

package com.chhliu.springboot.shiro.config;

import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.Filter;

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.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
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.context.annotation.DependsOn;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.filter.DelegatingFilterProxy;

/**
 * Shiro 配置
 * 
 * Apache Shiro 核心通過 Filter 來實現,就好像SpringMvc 通過DispachServlet 來主控制一樣。 既然是使用
 * Filter 一般也就能猜到,是通過URL規則來進行過濾和許可權校驗,所以我們需要定義一系列關於URL的規則和訪問許可權。
 * 
 * @author chhliu
 */
@Configuration
public class ShiroConfiguration {
	
    // cas server地址
    public static final String casServerUrlPrefix = "http://127.0.0.1";
    // Cas登入頁面地址
    public static final String casLoginUrl = casServerUrlPrefix + "/login";
    // Cas登出頁面地址
    public static final String casLogoutUrl = casServerUrlPrefix + "/logout";
    // 當前工程對外提供的服務地址
    public static final String shiroServerUrlPrefix = "http://127.0.1.28:8080";
    // casFilter UrlPattern
    public static final String casFilterUrlPattern = "/index";
    // 登入地址
    public static final String loginUrl = casLoginUrl + "?service=" + shiroServerUrlPrefix + casFilterUrlPattern;
    // 登出地址(casserver啟用service跳轉功能,需在webapps\cas\WEB-INF\cas.properties檔案中啟用cas.logout.followServiceRedirects=true)
    public static final String logoutUrl = casLogoutUrl+"?service="+loginUrl;
    // 登入成功地址
//    public static final String loginSuccessUrl = "/index";
    // 許可權認證失敗跳轉地址
    public static final String unauthorizedUrl = "/error/403.html";
    
    /**
     * 例項化SecurityManager,該類是shiro的核心類
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
    	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroCasRealm());
//      <!-- 使用者授權/認證資訊Cache, 採用EhCache 快取 -->
        securityManager.setCacheManager(getEhCacheManager());
        // 指定 SubjectFactory,如果要實現cas的remember me的功能,需要用到下面這個CasSubjectFactory,並設定到securityManager的subjectFactory中
        securityManager.setSubjectFactory(new CasSubjectFactory());
        return securityManager;
    }

    /**
     * 配置快取
     * @return
     */
    @Bean
    public EhCacheManager getEhCacheManager() {
        EhCacheManager em = new EhCacheManager();
        em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
        return em;
    }

    /**
     * 配置Realm,由於我們使用的是CasRealm,所以已經集成了單點登入的功能
     * @param cacheManager
     * @return
     */
    @Bean
    public MyShiroRealm myShiroCasRealm() {
        MyShiroRealm realm = new MyShiroRealm();
        // cas登入伺服器地址字首
        realm.setCasServerUrlPrefix(ShiroConfiguration.casServerUrlPrefix);
        // 客戶端回撥地址,登入成功後的跳轉地址(自己的服務地址)
        realm.setCasService(ShiroConfiguration.shiroServerUrlPrefix + ShiroConfiguration.casFilterUrlPattern);
        // 登入成功後的預設角色,此處預設為user角色
        realm.setDefaultRoles("user");
        return realm;
    }

    /**
     * 註冊單點登出的listener
     * @return
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)// 優先順序需要高於Cas的Filter
    public ServletListenerRegistrationBean<?> singleSignOutHttpSessionListener(){
        ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
        bean.setListener(new SingleSignOutHttpSessionListener());
        bean.setEnabled(true);
        return bean;
    }

    /**
     * 註冊單點登出filter
     * @return
     */
    @Bean
    public FilterRegistrationBean singleSignOutFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setName("singleSignOutFilter");
        bean.setFilter(new SingleSignOutFilter());
        bean.addUrlPatterns("/*");
        bean.setEnabled(true);
        return bean;
    }

    /**
     * 註冊DelegatingFilterProxy(Shiro)
     */
    @Bean
    public FilterRegistrationBean delegatingFilterProxy() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        //  該值預設為false,表示生命週期由SpringApplicationContext管理,設定為true則表示由ServletContainer管理
        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
        filterRegistration.setEnabled(true);
        filterRegistration.addUrlPatterns("/*");
        return filterRegistration;
    }

    /**
     * 該類可以保證實現了org.apache.shiro.util.Initializable介面的shiro物件的init或者是destory方法被自動呼叫,
     * 而不用手動指定init-method或者是destory-method方法
     * 注意:如果使用了該類,則不需要手動指定初始化方法和銷燬方法,否則會出錯
     * @return
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 下面兩個配置主要用來開啟shiro aop註解支援. 使用代理方式;所以需要開啟程式碼支援;
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
        daap.setProxyTargetClass(true);
        return daap;
    }
    
    /**
	 * @param securityManager
	 * @return
	 */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 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);// 我們選擇認證失敗後再開啟登入頁面
        casFilter.setLoginUrl(loginUrl);
        return casFilter;
    }

    /**
     * 使用工廠模式,建立並初始化ShiroFilter
     * @param securityManager
     * @param casFilter
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager, CasFilter casFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必須設定 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        /*
         *  登入成功後要跳轉的連線,不設定的時候,會預設跳轉到前一步的url
         *  比如先在瀏覽器中輸入了http://localhost:8080/userlist,但是現在使用者卻沒有登入,於是會跳轉到登入頁面,等登入認證通過後,
         *  頁面會再次自動跳轉到http://localhost:8080/userlist頁面而不是登入成功後的index頁面
         *  建議不要設定這個欄位
         */
//        shiroFilterFactoryBean.setSuccessUrl(loginSuccessUrl);
        
        // 設定無許可權訪問頁面
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        /*
         *  新增casFilter到shiroFilter中,注意,casFilter需要放到shiroFilter的前面,
         *  從而保證程式在進入shiro的login登入之前就會進入單點認證
         */
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("casFilter", casFilter);
        
        // logout已經被單點登入的logout取代
       // filters.put("logout",logoutFilter());
        shiroFilterFactoryBean.setFilters(filters);

        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }

    /**
     * 載入shiroFilter許可權控制規則(從資料庫讀取然後配置),角色/許可權資訊由MyShiroCasRealm物件提供doGetAuthorizationInfo實現獲取來的
     * 生產中會將這部分規則放到資料庫中
     * @param shiroFilterFactoryBean
     */
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean){
        /////////////////////// 下面這些規則配置最好配置到配置檔案中,注意,此處加入的filter需要保證有序,所以用的LinkedHashMap ///////////////////////
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        filterChainDefinitionMap.put(casFilterUrlPattern, "casFilter");

        //2.不攔截的請求
        filterChainDefinitionMap.put("/css/**","anon");
        filterChainDefinitionMap.put("/js/**","anon");
        filterChainDefinitionMap.put("/login", "anon");
        // 此處將logout頁面設定為anon,而不是logout,因為logout被單點處理,而不需要再被shiro的logoutFilter進行攔截
        filterChainDefinitionMap.put("/logout","anon");
        filterChainDefinitionMap.put("/error","anon");
        //3.攔截的請求(從本地資料庫獲取或者從casserver獲取(webservice,http等遠端方式),看你的角色許可權配置在哪裡)
        filterChainDefinitionMap.put("/user", "authc"); //需要登入

        //4.登入過的不攔截
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }
}

部分配置參考:http://shiro.apache.org/spring.html

3、編寫Realm

由於需要整合單點登入的功能,所以需要整合CasRealm類,該類已經為我們實現了單點認證的功能,我們要做的就是實現授權部分的功能,示例程式碼如下:

package com.chhliu.springboot.shiro.config;

import javax.annotation.Resource;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
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 com.chhliu.springboot.shiro.mode.SysPermission;
import com.chhliu.springboot.shiro.mode.SysRole;
import com.chhliu.springboot.shiro.mode.UserInfo;
import com.chhliu.springboot.shiro.service.UserInfoService;

/**
 * 許可權校驗核心類; 由於使用了單點登入,所以無需再進行身份認證 只需要授權即可
 * 
 * @author chhliu
 */
public class MyShiroRealm extends CasRealm {

	@Resource
	private UserInfoService userInfoService;

	/**
	 * 1、CAS認證 ,驗證使用者身份 
	 * 2、將使用者基本資訊設定到會話中,方便獲取
	 * 3、該方法可以直接使用CasRealm中的認證方法,此處僅用作測試
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {

		// 呼叫父類中的認證方法,CasRealm已經為我們實現了單點認證。
		AuthenticationInfo authc = super.doGetAuthenticationInfo(token);

		// 獲取登入的賬號,cas認證成功後,會將賬號存起來
		String account = (String) authc.getPrincipals().getPrimaryPrincipal();

		// 將使用者資訊存入session中,方便程式獲取,此處可以將根據登入賬號查詢出的使用者資訊放到session中
		SecurityUtils.getSubject().getSession().setAttribute("no", account);

		return authc;
	}

	/**
	 * 此方法呼叫 hasRole,hasPermission的時候才會進行回撥.
	 * 
	 * 許可權資訊.(授權): 1、如果使用者正常退出,快取自動清空; 2、如果使用者非正常退出,快取自動清空;
	 * 3、如果我們修改了使用者的許可權,而使用者不退出系統,修改的許可權無法立即生效。 (需要手動程式設計進行實現;放在service進行呼叫)
	 * 在許可權修改後呼叫realm中的方法,realm已經由spring管理,所以從spring中獲取realm例項, 呼叫clearCached方法;
	 * :Authorization 是授權訪問控制,用於對使用者進行的操作授權,證明該使用者是否允許進行當前操作,如訪問某個連結,某個資原始檔等。
	 * 
	 * @param principals
	 * @return
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("許可權配置-->MyShiroRealm.doGetAuthorizationInfo()");

		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
		// 獲取單點登陸後的使用者名稱,也可以從session中獲取,因為在認證成功後,已經將使用者名稱放到session中去了
		String userName = (String) super.getAvailablePrincipal(principals);
//				principals.getPrimaryPrincipal(); 這種方式也可以獲取使用者名稱

		// 根據使用者名稱獲取該使用者的角色和許可權資訊
		UserInfo userInfo = userInfoService.findByUsername(userName);

		// 將使用者對應的角色和許可權資訊打包放到AuthorizationInfo中
		for (SysRole role : userInfo.getRoleList()) {
			authorizationInfo.addRole(role.getRole());
			for (SysPermission p : role.getPermissions()) {
				authorizationInfo.addStringPermission(p.getPermission());
			}
		}

		return authorizationInfo;
	}
}

下面,我們就可以進行驗證測試了!

在瀏覽器輸入http:127.0.1.28:8080/userInfo/userList 我們會發現,會自動跳轉到單點的登入頁面


然後我們輸入使用者名稱和密碼,就會自動跳轉到http:127.0.1.28:8080/userInfo/userList頁面了。