pac4j探索(二): buji-pac4j+Cas+Shiro+SpringMvc實現單點登入
在pac4j探索的上一篇文章大致講述了一下buji-pac4j+CAS的認證流程。這裡記錄一下本人實現的最簡單的單點登入,僅作為筆記、學習交流之用,戳這裡獲取本文原始碼。
一、專案框架
1、 buji-pac4j(v.3.0.0)
2、shiro (v.1.4.0)
3、springmvc (v.4.3.2)
4、CAS (v.4.2.6)
5、pac4j-cas(v.2.2.1)
在maven專案的pom.xml裡配置以上相關依賴,具體依賴配置可以檢視我的專案,這裡不再贅述。
二、目錄結構
客戶端專案(pac4jtest1)目錄結構如下:
1、java檔案目錄中,Redirect2CasLoginFilter是測試用的,可以不管,MyCasClient類是繼承自CasClient的自定義客戶端,ShiroCasLogoutHandler類是單點登出時對shiro的一些操作,Controller類是請求控制器,util包裡的是單點登出相關的類;
2、配置檔案目錄中,log4j.properties是日誌管理檔案,url.properties配置了專案中用到的各種url,spring-comm.xml配置了shiro整合pac4j的配置,spring-mvc.xml是springmvc的相關配置;
3、另外還有個index.jsp,就是受保護的頁面,請求訪問前需要先認證。
二、springMvc配置
這裡springmvc作最簡單的配置:
<!-- 自動掃描的包名 -->
<context:component-scan base-package="com.pac4j.rest"/>
<!-- 預設的註解對映的支援,自動註冊DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter -->
<mvc:annotation-driven />
<!-- 檢視解釋類 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
</bean>
<!-- 對靜態資原始檔的訪問-->
<mvc:resources mapping="/images/**" location="/WEB-INF/images/"/>
<mvc:resources mapping="/js/**" location="/WEB-INF/js/" />
<mvc:resources mapping="/css/**" location="/WEB-INF/css/"/>
三、pac4j配置
spring-comm.xml是shiro整合pac4j的配置,具體配置如下:
<!-- 地址配置 -->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>/WEB-INF/classes/url.properties</value>
</list>
</property>
</bean>
<!-- pac4j configurations -->
<!-- 請求cas服務端配置 -->
<bean id="casConfig" class="org.pac4j.cas.config.CasConfiguration">
<!-- CAS server登入連結 -->
<property name="loginUrl" value="${sso.cas.server.loginUrl}"></property>
<!-- CAS server服務字首 -->
<property name="prefixUrl" value="${sso.cas.server.prefixUrl}"></property>
<!-- 登出處理器,單點登出時所需要的操作在這裡實現-->
<property name="logoutHandler" ref="casLogoutHandler"></property>
</bean>
<!-- cas客戶端配置 -->
<bean id="casClient" class="com.pac4j.client.MyCasClient">
<constructor-arg ref="casConfig" />
<property name="includeClientNameInCallbackUrl" value="false"></property>
<!-- 客戶端回撥地址 -->
<property name="callbackUrl" value="${sso.cas.client.callbackUrl}"></property>
</bean>
<!-- shiro登出處理器,銷燬session及登入狀態等-->
<bean id="casLogoutHandler" class="com.pac4j.handler.ShiroCasLogoutHandler">
<property name="destroySession" value="true"></property>
</bean>
<bean id="sessionStore" class="com.pac4j.util.MyShiroSessionStore"></bean>
<!-- pac4j配置 -->
<bean id="authcConfig" class="org.pac4j.core.config.Config">
<constructor-arg ref="casClient"></constructor-arg>
<property name="sessionStore" ref="sessionStore"></property>
</bean>
<!-- shiro configurations -->
<bean id="sessionIdGenerator"
class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" />
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid" />
<property name="httpOnly" value="false" />
<property name="maxAge" value="180000" />
<property name="path" value="/" />
</bean>
<bean id="sessionDAO"
class="org.apache.shiro.session.mgt.eis.MemorySessionDAO">
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean>
<bean id="sessionValidationScheduler"
class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
<property name="sessionValidationInterval" value="1800000" />
<property name="sessionManager" ref="sessionManager" />
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="1800000" />
<property name="deleteInvalidSessions" value="true" />
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionValidationScheduler" ref="sessionValidationScheduler" />
<property name="sessionDAO" ref="sessionDAO" />
<property name="sessionIdCookieEnabled" value="true" />
<property name="sessionIdCookie" ref="sessionIdCookie" />
</bean>
<!-- 基於pac4j的Subject工廠 -->
<bean id="pac4jSubjectFactory" class="io.buji.pac4j.subject.Pac4jSubjectFactory"></bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm">
<bean class="io.buji.pac4j.realm.Pac4jRealm">
<property name="cachingEnabled" value="false" />
<property name="authenticationCachingEnabled" value="false" />
<property name="authenticationCacheName" value="authenticationCache" />
<property name="authorizationCachingEnabled" value="false" />
<property name="authorizationCacheName" value="authorizationCache" />
</bean>
</property>
<property name="subjectFactory" ref="pac4jSubjectFactory"></property>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"></property>
<property name="filters">
<util:map>
<!-- 配置SecurityFilter,用於攔截受保護的url -->
<entry key="casSecurityFilter">
<bean class="io.buji.pac4j.filter.SecurityFilter">
<property name="config" ref="authcConfig"></property>
<property name="clients" value="MyCasClient"></property>
</bean>
</entry>
<!-- 回撥過濾器,完成ticket認證 -->
<entry key="callback">
<bean class="io.buji.pac4j.filter.CallbackFilter">
<property name="config" ref="authcConfig"></property>
<property name="defaultUrl" value="${sso.cas.client.successUrl}"></property>
</bean>
</entry>
<!-- 登出過濾器 -->
<entry key="logout">
<bean id="logout" class="io.buji.pac4j.filter.LogoutFilter">
<property name="defaultUrl" value="${sso.cas.client.callbackUrl}"></property>
<property name="config" ref="authcConfig"></property>
<property name="centralLogout" value="true"></property>
<property name="localLogout" value="false"></property>
</bean>
</entry>
<!-- 攔截/login,並作一些操作,測試用,可以忽略 -->
<entry key="login">
<bean class="com.pac4j.filter.Redirect2CasLoginFilter"></bean>
</entry>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/index = casSecurityFilter
/logout = logout
/callback = callback
/login** = login
/login/** = login
</value>
</property>
</bean>
<!-- 生命週期處理 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<bean id="annotationProxy"
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
關於shiro的相關知識,在這裡不再贅述,因為不屬於本文的討論範圍。需要留意的是,以上配置在原來一般的shiro配置基礎上,除了增加pac4j的配置,還修改了一下原來shiro的配置,具體變更如下:
- 原來的CasFilter被替換成CallbackFilter
- CasRealm被替換成Pac4jRealm
- 當一個url需要被保護時(需要經過認證、鑑權),不僅需要使用shiro預設的過濾器,而且必須使用SecurityFilter。
四、url.properties
在spring-comm.xml會用到的一些路徑:
##cas服務字首
sso.cas.server.prefixUrl=http://localhost:8080/hgretail.authc/
##cas服務登入url
sso.cas.server.loginUrl=http://localhost:8080/hgretail.authc/login
##cas客戶端回撥地址
sso.cas.client.callbackUrl=http://localhost:8080/pac4jtest1/callback?client_name=MyCasClient
##cas服務端成功跳轉地址
sso.cas.client.successUrl=http://localhost:8080/pac4jtest1/index
五、MyCasClient
這個類是繼承自CasClient的,因為其超類IndirectClient中的getRedirectAction方法用起來有問題,報401異常,所以寫了這個類,覆蓋getRedirectAction方法,遮蔽掉異常程式碼。以後思考一下是否有更好的解決辦法。
public class MyCasClient extends CasClient {
public MyCasClient(final CasConfiguration configuration) {
super(configuration);
}
/*
* (non-Javadoc)
* @see org.pac4j.core.client.IndirectClient#getRedirectAction(org.pac4j.core.context.WebContext)
*/
@Override
public RedirectAction getRedirectAction(WebContext context) throws HttpAction {
init(context);
// it's an AJAX request -> unauthorized (with redirection url in header)
if (getAjaxRequestResolver().isAjax(context)) {
logger.info("AJAX request detected -> returning 401");
RedirectAction action = getRedirectActionBuilder().redirect(context);
cleanRequestedUrl(context);
throw HttpAction.unauthorized("AJAX request -> 401", context, null, action.getLocation());
}
// authentication has already been tried -> unauthorized
//FIXME 以下這段程式碼在org.pac4j.cas.client.CasClient中會出現401錯誤,所以在這裡遮蔽掉。以後尋求更好的解決辦法。
// final String attemptedAuth = (String) context.getSessionAttribute(getName() + ATTEMPTED_AUTHENTICATION_SUFFIX);
// if (CommonHelper.isNotBlank(attemptedAuth)) {
// cleanAttemptedAuthentication(context);
// cleanRequestedUrl(context);
// throw HttpAction.unauthorized("authentication already tried -> forbidden", context, null, null);
// }
return getRedirectActionBuilder().redirect(context);
}
private void cleanRequestedUrl(final WebContext context) {
context.setSessionAttribute(Pac4jConstants.REQUESTED_URL, "");
}
}
六、ShiroCasLogoutHandler
ShiroCasLogoutHandler繼承於DefaultCasLogoutHandler。用於單點登出的時候shiro的一系列登出操作:
public class ShiroCasLogoutHandler<C extends WebContext> extends DefaultCasLogoutHandler<C> {
public ShiroCasLogoutHandler() {
}
public ShiroCasLogoutHandler(final Store<String, Object> store) {
super(store);
}
protected void destroy(final C context, final SessionStore sessionStore, final String channel) {
// remove profiles
final ShiroProfileManager manager = new ShiroProfileManager(context);
manager.logout();//shiro登出操作
logger.debug("destroy the user profiles");
// and optionally the web session
if (isDestroySession()) {
logger.debug("destroy the whole session");
final boolean invalidated = sessionStore.destroySession(context);
if (!invalidated) {
logger.error("The session has not been invalidated for {} channel logout", channel);
}
}
}
}
七、MyShiroSessionStore類和MyShiroProvidedSessionStore類
1、SessionStore是用於暫時快取session以供後續的操作,MyShiroSessionStore類重寫了ShiroSessionStore,因為pac4j本身的ShiroSessionStore不能滿足單點登出,會有問題,所以改寫了該類:
public class MyShiroSessionStore implements SessionStore<J2EContext> {
private final static Logger logger = LoggerFactory.getLogger(MyShiroSessionStore.class);
//獲取shiro session
protected Session getSession(final boolean createSession) {
return SecurityUtils.getSubject().getSession(createSession);
}
//獲取shiro的sessionid
@Override
public String getOrCreateSessionId(final J2EContext context) {
final Session session = getSession(false);
if (session != null) {
return session.getId().toString();
}
return null;
}
/**
* 獲取shiro session中的屬性
*/
@Override
public Object get(final J2EContext context, final String key) {
final Session session = getSession(false);
if (session != null) {
return session.getAttribute(key);
}
return null;
}
/**
* 設定session屬性
*/
@Override
public void set(final J2EContext context, final String key, final Object value) {
final Session session = getSession(true);
if (session != null) {
try {
session.setAttribute(key, value);
} catch (final UnavailableSecurityManagerException e) {
logger.warn("Should happen just once at startup in some specific case of Shiro Spring configuration", e);
}
}
}
/**
* 銷燬session
*/
@Override
public boolean destroySession(final J2EContext context) {
getSession(true).stop();
return true;
}
/**
* 獲取shiro session並快取用於單點登出
*/
@Override
public Object getTrackableSession(final J2EContext context) {
return getSession(true);
}
/**
* 從getTrackableSession中獲取的session來構建SessionStore
*/
@Override
public SessionStore<J2EContext> buildFromTrackableSession(final J2EContext context, final Object trackableSession) {
if(trackableSession != null) {
return new MyShiroProvidedSessionStore((Session) trackableSession);
}
return null;
}
/**
* 重新整理session屬性,這裡暫返回false,實際應用中需實現
*/
@Override
public boolean renewSession(final J2EContext context) {
return false;
}
}
2、MyShiroProvidedSessionStore是由MyShiroSessionStore構建來臨時儲存TrackableSession的:
public class MyShiroProvidedSessionStore extends MyShiroSessionStore{
/**儲存的TrackableSession,往後要操作時用這個session操作*/
private Session session;
public MyShiroProvidedSessionStore(Session session) {
this.session = session;
}
protected Session getSession(final boolean createSession) {
return session;
}
}
八、Controller
這是個控制器,負責請求處理
public class Controller {
@RequestMapping(value="/index",method=RequestMethod.GET)
public String index(ModelMap map) {
//獲取使用者身份
Pac4jPrincipal p = SecurityUtils.getSubject().getPrincipals().oneByType(Pac4jPrincipal.class);
if(p != null) {
CommonProfile profile = p.getProfile();
map.put("profile", profile);
}
return "index";
}
}
九、index.jsp
這裡是個簡單的jsp頁面,上面將會打印出使用者的資訊
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Hello pac4j1!</title>
</head>
<body>
hello~~~ <b>I'm pac4jtest1</b>
profile:${profile}
</body>
</html>
十、web.xml
主要配置了springmvc和shiroFilter
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-comm.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springDispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
十一、執行
CAS服務的專案在這裡就不貼出來了,因為改變不大,就是注意要在CAS服務裡註冊Cas Client,也就是上面的專案,否則CAS服務會報錯。
為了認證單點登入,要再弄一個Cas Client,所以把上面的專案pac4jtest1複製一份命名為pac4jtest2,記得把裡面對應的配置要改過來。
部署CAS Server、pac4jtest1、pac4jtest2三個專案並啟動tomcat。根據以下步驟進行測試:
1、在位址列輸入:
2、會看到去到了登入頁,位址列也發生了改變:
3、輸入使用者名稱密碼登入,去到了pac4jtest1的index:
4、此時在位址列輸入 http://localhost:8080/pac4jtest2/index,按道理如果是還沒認證,會跳轉到第一步中的登入頁面;但是我們已經在第三步已經登入認證通過了,所以無須再進行登入,直接進入pac4jtest2的index
至此,一個簡單的整合pac4j+CAS+shiro+springmvc的單點登入完成
十二、後記
這是pac4j最迷你版的單點登入,實際應用中比這個複雜很多,其實主要的部分弄清楚了,擴充套件也就很容易了。本人才疏學淺,專案中存在一些不足,歡迎大家批評指出!
原始碼位置:pac4jtest1
相關推薦
pac4j探索(二): buji-pac4j+Cas+Shiro+SpringMvc實現單點登入
在pac4j探索的上一篇文章大致講述了一下buji-pac4j+CAS的認證流程。這裡記錄一下本人實現的最簡單的單點登入,僅作為筆記、學習交流之用,戳這裡獲取本文原始碼。 一、專案框架 1、 buji-pac4j(v.3.0.0) 2、
cas shiro spring實現單點登入
這裡貼出傳送門,來自幕課網大神。講了cas配置和cas的基本原理。 CAS deployerConfigContext.xml配置檔案 預設通過配置檔案管理授權登入賬戶 <bean id="primaryAuthentication
JAVA三種實現單例模式方法(二):使用靜態內部類實現單例設計模式
靜態程式碼塊和靜態內部類的載入順序:當呼叫外部類的建構函式是,外部類的靜態程式碼塊同時被載入,但是其內部類不會同時被載入;當且僅當內部類的靜態域或其構造方法或其靜態方法被呼叫時,內部內才被載入。 因此,通過內部內實現單例,就能實現延遲載入。 這個解決方案被稱為Lazy i
(二)如何使用spring-security來實現使用者的登入許可權功能?(配合使用資料庫的方式)
如何使用spring-security來實現使用者的登入功能之配合使用資料庫的方式 這個圖大家先熟悉一下簡單的過一遍,等把步驟都寫完之後,後面會總結 (一)使用spring-security之前需要做的準備(基於springMVC和dubbo的專案)
SpringBootSecurity學習(26)前後端分離版之github單點登入
單點登入(SSO) 關於oauth2.0,最後我們再來學習一下單點登入。前面介紹過單點登入的定義,單點登入(Single Sign On),簡稱為 SSO,是目前比較流行的企業業務整合的解決方案之一。SSO的定義是在多個應用系統中,使用者只需要登入一次就可以訪問所有相互信任的應用系統。 關於單點登入,spri
Spring Security原始碼分析十二:Spring Security OAuth2基於JWT實現單點登入
單點登入(英語:Single sign-on,縮寫為 SSO),又譯為單一簽入,一種對於許多相互關連,但是又是各自獨立的軟體系統,提供訪問控制的屬性。當擁有這項屬性時,當用戶登入時,就可以獲取所有
pac4j探索(一)之buji-pac4j
一、初步認識buji-pac4j 公司單點登入cas客戶端用的是shiro的shiro-cas模組,但從原始碼看來,shiro不建議再使用shiro-cas,也就是說shiro-cas模組的相關都被s
AI探索(二)Tensorflow環境準備
Python + Tensorflow環境安裝 Tensorflow支援Windows/Mac/Linux等三種作業系統, 其中windows下python需要安裝3.5以上的版本 Mac/Linux自帶的python 2.7可以支援安裝Tensorflow #1. Python安裝
App自動化測試探索(二)MAC環境搭建iOS+Python+Appium測試環境
code -s image ios 使用 usr developer contents gis 環境搭建要求,MAC 機器一臺,要求 Xcode 8.0以上 1. 安裝 Homebrew /usr/bin/ruby -e "$(curl -fsSL https://raw
C++ 非同步程式設計探索(二) thread safe
前言 在非同步程式設計實踐中,曾經遇到的最大的問題就是thread safe 問題。 我所在的團隊是Database團隊,主要是為APP提供和redis互動的API。場景就是application thread呼叫我們的API。我們有個worker thread
vue v-for迴圈巢狀的探索(二)
使用v-for迴圈的目的就是為了處理大量型別重複的資料,歸根結底是一種有規律的資料,但是有些規律卻不是那麼容易的,很多時候,我們會使用到迴圈,甚至多重迴圈的巢狀,不同的迴圈巢狀對應著不同的json資料的結構,本篇主要講述的是使用v-for迴圈解決部分同,部分不同的情況,主要是
iOS自動化探索(二)WDA(WebDriverAgent) API的使用
前面我們已經安裝好了WebdriverAgent, 現在可以用Facebook官方提供的API來進行一些操作 WDA API官方頁面: https://github.com/facebook/WebDriverAgent/wiki/Queries 如果在終端執行的話可以使用curl來呼叫API,需要搭配
Android外掛化探索(二)資源載入
前情提要 PathClassLoader和DexClassLoader的區別 DexClassLoader的原始碼如下: public class DexClassLoader extends BaseDexClassLoader {
React Native探索(二):佈局篇
可以看到iphone 6的寬度為 375pt,對應了上邊的375,由此可見react的單位為pt。 那如何獲取實際的畫素尺寸呢? 這對圖片的高清化很重要,如果我的圖片大小為100*100 px. 設定寬度為100 * 100. 那在iphone上的尺寸就是模糊的。 這個時候需要的影象大小應該是 100 *
scrapy研究探索(二)——爬w3school.com.cn
下午被一個問題困擾了好一陣,最終使用另一種方式解決。 在開始之前假設你已經成功安裝一切所需,整懷著一腔熱血想要抓取某網站。一起來have a try。 1. 前期基礎準備。 Oh,不能在準備了,直接來。 (1) 建立專案。 輸入: scapy startproject
CAS實現單點登入(二):自定義的使用者驗證登入
上一篇演示單點登入服務端認證機制採用的是cas server預設的使用者名稱和密碼(admin/admin)。今天介紹正常專案中如何通過驗證DB中的使用者資料,來驗證使用者的密碼的合法性 自定義驗證登入有兩種方式: 採用cas-server預設的資料庫查詢
Socket程式設計(二)回射客戶/伺服器的實現
TCP客戶/伺服器模型,即C/S模型 可以將這個模型類比為打電話。 TCP伺服器通過socket()建立一個套接字,相當於安裝一臺話機;然後為安裝好的話機繫結bind()一個電話號碼;讓話機處於監聽的狀態listen();等待客戶端的連線accept(),也就是等待對方電話撥打過來,如果一直
(二)mycat搭建主從複製,實現讀寫分離
環境準備 準備三臺linux伺服器,mycat、mycatmaster、mycatslave,分別安裝好mycat服務、mysql服務、mysql服務 mycatmaster和mycatslave基於binlog實現主從複製,見主從複製搭建 mycat配置 mycat權威指南中配
Java併發程式設計(二)多執行緒四種實現方式
Java實現多執行緒的方式 Java實現多執行緒的方式有4種: 繼承Thread方法、實現Runnable介面、實現Callable介面並通過FutureTask建立執行緒、使用ExecutorService。 其中,前兩種執行緒執行結果沒有返回值,後兩種是有返回值的。 1、繼承Th
IO多路複用(二) -- select、poll、epoll實現TCP反射程式
接著上文IO多路複用(一)-- Select、Poll、Epoll,接下來將演示一個TCP回射程式,原始碼來自於該博文https://www.cnblogs.com/Anker...,在這裡將其進行了整合,突出select、poll和epoll不同方法之間的比較,但