@
一、 SSO簡介
1.1 單點登錄定義
單點登錄(Single sign on),英文名稱縮寫SSO,SSO的意思就是在多系統的環境中,登錄單方系統,就可以在不用再次登錄的情況下訪問相關受信任的系統。也就是說只要登錄一次單體系統就可以。
1.2 單點登錄角色
單點登錄一般包括下面三種角色:
①用戶(多個);
②認證中心(一個);
③Web應用(多個)。
PS:這里所說的web應用可以理解為SSO Client,認證中心可以說是SSO Server。
1.3 單點登錄分類
因為http協議是無狀態的協議,所以要保持登錄狀態,必須要存儲登錄信息,按照存儲方式,單點登錄實現方式主要可以分為兩種。
- 一種是基於Cookie的,這種比較常見,比如下文介紹的CAS也是基於Cookie的;
- 另外一種是基於Session的,其實理解起來就是會話共享,只有實現不同子系統之間的會話共享就能實現單點登錄,詳情可以參考我之前的博客,就是實現會話共享實現單點登錄的,鏈接
二、 CAS簡介
2.1 CAS簡單定義
CAS(Center Authentication Service)是耶魯大學研究的一款開源的單點登錄項目,主要為web項目提供單點登錄實現,屬於Web SSO。
2.2 CAS體系結構
CAS體系結構分為CAS Server和CAS Client。
CAS Server就是Cas開源的,需要去github下載,然后進行修改;Cas Client
可以是App或者web端的或者PC端,CAS支持多種開發語言,java、php、C#等等
PS:圖來自官網,這里簡單介紹一下,從圖可以看出,CAS支持多種方式的認證,一種是LDAP的、比較常見的數據庫Database的JDBC,還有Active Directory等等;支持的協議有Custom Protocol 、 CAS 、 OAuth 、 OpenID 、 RESTful API 、 SAML1.1 、 SAML2.0 等
2.3 CAS原理
下面給出一張來自CAS官方的圖片
CAS登錄等系統分為CAS Server和CAS Client,下面,我根據我的理解稍微解釋一下:
1、用戶訪問CAS Client請求資源
2、客戶端程序做了重定向,重定向到CAS Server
3、CAS Server會對請求做認證,驗證是否有TGC(Ticket Granted Cookie,有TGC說明已經登錄過,不需要再登錄,沒有就返回登錄頁面
4、認證通過后會生成一個Service Ticket返回Cas Client,客戶端進行Ticket緩存,一般放在Cookie里,我們稱之為TGC(Ticket Granted Cookie)
5、然后Cas Client就帶着Ticket再次訪問Cas Server,CAS Server進行Ticket驗證
6、CAS Server對Ticket進行驗證,通過就返回用戶信息,用戶拿到信息后就可以登錄
看到這個過程,我們大概就能理解CAS是怎么實現的,看起來過程挺多的,不過這些過程都是CAS在后台做的。CAS Service和CAS Client通訊基於HttpUrlConnection
注意要點:
- TGT(Ticket Granded Ticket),就是存儲認證憑據的Cookie,有TGT說明已經通過認證
- ST(Service Ticket),是由CAS認證中心生成的一個唯一的不可偽裝的票據,用於認證的
- 沒登錄過的或者TGT失效的,訪問時候也跳轉到認證中心,發現沒有TGT,說明沒有通過認證,直接重定向登錄頁面,輸入賬號密碼后,再次重定向到認證中心,驗證通過后,生成ST,返回客戶端保存到TGC
- 登錄過的而且TGT沒有失效的,直接帶着去認證中心認證,認證中心發現有TGT,重定向到客戶端,並且帶上ST,客戶端再帶ST去認證中心驗證
三、CAS服務端搭建
3.1 CAS支持Http登錄配置
CAS默認是要https的鏈接才能登錄的,不過學習的話是可以先驅動https限制,本博客介紹的是基於Cas4.2.7的,之前改過4.0的,詳情見https://blog.csdn.net/u014427391/article/details/82083995
Cas4.2.7和4.0的修改是不一樣的,Cas4.2.7版本需要自己編譯,是基於Gradle的,不是基於Maven的,覺得麻煩可以下載4.0,因為4.0版本有提供war包,不需要自己編譯,下面介紹一下4.2.7版本,怎么支持http登錄
需要修改cas4.2.7的cas-server-webapp/WEB-INF/cas.properties,全都改為非安全的
tgc.secure=false
warn.cookie.secure=false
cas-server-webapp/resources/service/HTTPSandIMAPS-10000001.json原來的
"serviceId" : "^(https|imaps)://.*"
加上http
"serviceId" : "^(https|imaps|http)://.*"
注釋cas-server-webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp頁面中校驗是否是HTTPS協議的標簽塊
<c:if test="${not pageContext.request.secure}">
<div id="msg" class="errors">
<h2><spring:message code="screen.nonsecure.title" /></h2>
<p><spring:message code="screen.nonsecure.message" /></p>
</div>
</c:if>
然后登錄就沒非安全提示了
3.2 CAS服務端部署運行
然后將war包丟在Tomcat的webapp里,部署啟動,默認賬號密碼casuser/Mellon,cas4.2.7的賬號密碼是寫在cas.properties里的,這個和4.0的不一樣
accept.authn.users=casuser::Mellon
登錄成功,當然在項目中,肯定不能這樣做,這個需要我們配置jdbc或者加上權限校驗等等
單點登出,鏈接是http://127.0.0.1:8080/cas/logout
四、CAS客戶端接入
本博客介紹一下基於SpringBoot的Cas客戶端接入,數據庫采用mysql,權限控制采用Shiro
maven配置,加上Shiro和CAS的相關jar:
CAS和Shiro的相關版本
<properties>
<shiro.version>1.2.3</shiro.version>
<shiro.spring.version>1.2.4</shiro.spring.version>
<shiro.encache.version>1.2.4</shiro.encache.version>
<cas.version>3.2.0</cas.version>
<shiro.cas.version>1.2.4</shiro.cas.version>
</properties>
加上Shiro和cas相關jar
<!-- Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.spring.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.encache.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>${shiro.cas.version}</version>
</dependency>
<!-- cas -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>${cas.version}</version>
</dependency>
新建一個環境類,保存cas的一些配置鏈接:
package org.muses.jeeplatform.core;
/**
* <pre>
* CAS配置環境類
* </pre>
*
* @author nicky.ma
* <pre>
* 修改記錄
* 修改后版本: 修改人: 修改日期: 2019年05月25日 修改內容:
* </pre>
*/
public class CASConsts {
/* CAS單點登錄配置 */
//Cas server地址
public static final String CAS_SERVER_URL_PREFIX = "http://localhost:8080/cas";
//Cas單點登錄地址
public static final String CAS_LOGIN_URL = CAS_SERVER_URL_PREFIX +"/login";
//CAS單點登出地址
public static final String CAS_LOGOUT_URL = CAS_SERVER_URL_PREFIX + "/logout";
//對外提供的服務地址
public static final String SERVER_URL_PREFIX = "http://localhost:8081";
//Cas過濾器的urlPattern
public static final String CAS_FILTER_URL_PATTERN = "/jeeplatform";
//CAS客戶端單點登錄跳轉地址
public static final String CAS_CLIENT_LOGIN_URL = CAS_LOGIN_URL + "?service="+SERVER_URL_PREFIX+CAS_FILTER_URL_PATTERN;
//CAS客戶端單點登出
public static final String CAS_CLIENT_LOGOUT_URL = CAS_LOGOUT_URL + "?service="+SERVER_URL_PREFIX+CAS_FILTER_URL_PATTERN;
//登錄成功地址
public static final String LOGIN_SUCCESS_URL = "/index";
//無權訪問頁面403
public static final String LOGIN_UNAUTHORIZED_URL = "/403";
}
ShiroCas配置類:
package org.muses.jeeplatform.config;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import org.muses.jeeplatform.core.shiro.ShiroRealm;
import org.muses.jeeplatform.web.filter.SysAccessControllerFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.muses.jeeplatform.core.CASConsts.*;
/**
* @author nicky.ma
*/
@Configuration
public class ShiroConfig {
private static final Logger LOG = LoggerFactory.getLogger(ShiroConfig.class);
/**
* 單點登出監聽器
* @return
*/
@Bean
public ServletListenerRegistrationBean singleSignOutHttpSeessionListener(){
ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
bean.setListener(new SingleSignOutHttpSessionListener());
bean.setEnabled(true);
return bean;
}
/**
* 注冊單點登出的過濾器
* @return
*/
@Bean
public FilterRegistrationBean singleSignOutFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setName("singleSignOutFilter");
bean.setFilter(new SingleSignOutFilter());
bean.addUrlPatterns("/*");
bean.setEnabled(true);
return bean;
}
@Bean
public FilterRegistrationBean authenticationFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new AuthenticationFilter());
bean.addUrlPatterns("/*");
bean.setName("CAS AuthenticationFilter");
bean.addInitParameter("casServerLoginUrl",CAS_SERVER_URL_PREFIX);
bean.addInitParameter("serverName",SERVER_URL_PREFIX);
return bean;
}
/**
* 單點登錄校驗
* @return
*/
@Bean
public FilterRegistrationBean validationFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setName("CAS Validation Filter");
registrationBean.addInitParameter("casServerUrlPrefix", CAS_SERVER_URL_PREFIX );
registrationBean.addInitParameter("serverName", SERVER_URL_PREFIX );
return registrationBean;
}
/**
* CAS過濾器
* @return
*/
@Bean
public CasFilter getCasFilter(){
CasFilter casFilter = new CasFilter();
casFilter.setName("casFilter");
casFilter.setEnabled(true);
casFilter.setFailureUrl(CAS_CLIENT_LOGIN_URL);
casFilter.setSuccessUrl(LOGIN_SUCCESS_URL);
return casFilter;
}
/**
* 定義ShrioRealm
* @return
*/
@Bean
public ShiroRealm myShiroRealm(){
ShiroRealm myShiroRealm = new ShiroRealm();
return myShiroRealm;
}
/**
* Shiro Security Manager
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//securityManager.setRealm(myShiroRealm());
securityManager.setSubjectFactory(new CasSubjectFactory());
return securityManager;
}
/**
* ShiroFilterFactoryBean
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager,CasFilter casFilter) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//注冊Shrio Security Manager
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl(CAS_CLIENT_LOGIN_URL);
shiroFilterFactoryBean.setSuccessUrl(LOGIN_SUCCESS_URL);
shiroFilterFactoryBean.setUnauthorizedUrl(LOGIN_UNAUTHORIZED_URL);
//添加CasFilter到ShiroFilter
Map<String,Filter> filters = new HashMap<String,Filter>();
filters.put("casFilter",casFilter);
shiroFilterFactoryBean.setFilters(filters);
//攔截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
//Shiro集成CAS后需要添加該規則
filterChainDefinitionMap.put(CAS_FILTER_URL_PATTERN,"casFilter");
// 配置不會被攔截的鏈接 順序判斷
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/upload/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/code", "anon");
//filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/403", "anon");
//filterChainDefinitionMap.put("/logincheck", "anon");
filterChainDefinitionMap.put("/logout","anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
自定義的CasRealm:
package org.muses.jeeplatform.core.shiro;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.muses.jeeplatform.core.entity.admin.User;
import org.muses.jeeplatform.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import static org.muses.jeeplatform.core.CASConsts.CAS_FILTER_URL_PATTERN;
import static org.muses.jeeplatform.core.CASConsts.CAS_SERVER_URL_PREFIX;
/**
* @description 基於Shiro框架的權限安全認證和授權
* @author Nicky
* @date 2017年3月12日
*/
public class ShiroRealm extends CasRealm {
Logger LOG = LoggerFactory.getLogger(ShiroRealm.class);
/**注解引入業務類**/
@Resource
UserService userService;
@PostConstruct
public void initProperty(){
setCasServerUrlPrefix(CAS_SERVER_URL_PREFIX);
//客戶端回調地址
setCasService(CAS_SERVER_URL_PREFIX + CAS_FILTER_URL_PATTERN);
}
/**
* 登錄信息和用戶驗證信息驗證(non-Javadoc)
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if(LOG.isInfoEnabled()) {
LOG.info("=>執行Shiro權限認證");
}
String username = (String)token.getPrincipal(); //得到用戶名
String password = new String((char[])token.getCredentials()); //得到密碼
User user = userService.findByUsername(username);
/* 檢測是否有此用戶 */
if(user == null){
throw new UnknownAccountException();//沒有找到賬號異常
}
/* 檢驗賬號是否被鎖定 */
if(Boolean.TRUE.equals(user.getLocked())){
throw new LockedAccountException();//拋出賬號鎖定異常
}
/* AuthenticatingRealm使用CredentialsMatcher進行密碼匹配*/
if(null != username && null != password){
return new SimpleAuthenticationInfo(username, password, getName());
}else{
return null;
}
}
/**
* 授權查詢回調函數, 進行鑒權但緩存中無用戶的授權信息時調用,負責在應用程序中決定用戶的訪問控制的方法(non-Javadoc)
* @see AuthorizingRealm#doGetAuthorizationInfo(PrincipalCollection)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
if(LOG.isInfoEnabled()) {
LOG.info("=>執行Shiro授權");
}
String username = (String)pc.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.getRoles(username));
authorizationInfo.setStringPermissions(userService.getPermissions(username));
return authorizationInfo;
}
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
}
五、客戶端極速接入
上面例子是比較麻煩的,我們要接入客戶端可以常用第三方的starter lib來對接
<!-- CAS依賴包 -->
<dependency>
<groupId>net.unicon.cas</groupId>
<artifactId>cas-client-autoconfig-support</artifactId>
<version>1.5.0-GA</version>
</dependency>
yaml配置:
cas:
server-login-url: http://127.0.0.1:8080/cas/login
server-url-prefix: http://127.0.0.1:8080/cas
client-host-url: http://127.0.0.1:8081
validation-type: cas
# use-session: true
加個Springboot配置類:
package org.muses.jeeplatform.config;
import net.unicon.cas.client.configuration.CasClientConfigurerAdapter;
import net.unicon.cas.client.configuration.EnableCasClient;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableCasClient
public class CASConfig extends CasClientConfigurerAdapter {
private static final String CAS_SERVER_URL_LOGIN = "http://localhost:8080/cas/login";
private static final String SERVER_NAME = "http://localhost:8081/";
// @Override
// public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) {
// super.configureAuthenticationFilter(authenticationFilter);
// //authenticationFilter.getInitParameters().put("authenticationRedirectStrategyClass","com.test.CustomAuthRedirectStrategy");
// }
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new AuthenticationFilter());
registrationBean.addUrlPatterns("/*");
Map<String, String> initParameters = new HashMap<String,String>(16);
initParameters.put("casServerLoginUrl",CAS_SERVER_URL_LOGIN);
initParameters.put("serverName",SERVER_NAME);
initParameters.put("ignorePattern","/logoutSuccess/*");
registrationBean.setOrder(1);
return registrationBean;
}
}