CAS單點登入-客戶端整合(shiro、springboot、jwt、pac4j)(十)
CAS單點登入-客戶端整合(shiro springboot jwt pac4j)(十)
由於我們通常在業務上會有以下的使用場景:
- 移動端通過業務系統鑑權
- 移動端免登入(登入一次以後)
解決方案:
PS:若想繼續往下讀,必須具備JWT的基本概念以及Pac4j的認證原理及應用場景
疑問
當然我們這章是講JWT,那麼會有以下的疑問:
- 若服務端已經接入了SSO,那麼在移動端使用者登入資訊提交給SSO還是服務端?(毫無疑問是服務端,SSO對於移動端必須是透明的)
- 若採用無會話方式,如何獲取token,服務端如何鑑權?(1.提交使用者名稱密碼到服務端,服務端把資料給到sso,sso最終返回使用者資料 2. 根據使用者資料建立token返回給移動端 3. 移動端登入後請求都帶token給到服務端鑑權)
- 在使用token鑑權的情況下,退出如何解決?(客戶端丟棄token即可)
鑑權流程
我們再講一下整一個鑑權流程
配置要素
重點:sso必須支援rest認證方式
maven依賴
<dependency>
<groupId >io.buji</groupId>
<artifactId>buji-pac4j</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-cas</artifactId>
<version>2.1.0</version>
</dependency >
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-jwt</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-http</artifactId>
<version>2.1.0</version>
</dependency>
鑑權配置
若馬上看下面的程式碼,估計一時半會看不懂,所以必須再講一下整個交易過程
服務端鑑權過程有兩個個角色分別為,Shiro、Pac4j,那他們的職責是什麼?
Shiro:
判斷當前Subject是否有許可權執行該資源,所以他的核心是Realm、Filter,只有被過濾到的資源才會走到Realm
Pac4j:
1. JWTAuthenticator對token進行鑑別
2. CasRestFormClient 支援通過rest介面傳入使用者名稱密碼進行對sso進行認證獲取UserProfile
3. Pac4jRealm鑑權的realm
4. SubjectFactory需要調整成Pac4jSubjectFactory
ShiroConfiguration.java
/*
* 版權所有.(c)2008-2017. 卡爾科技工作室
*/
package com.carl.wolf.permission.config;
import io.buji.pac4j.filter.CallbackFilter;
import io.buji.pac4j.filter.LogoutFilter;
import io.buji.pac4j.filter.SecurityFilter;
import io.buji.pac4j.realm.Pac4jRealm;
import io.buji.pac4j.subject.Pac4jSubjectFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.AbstractShiroWebFilterConfiguration;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.pac4j.cas.client.CasClient;
import org.pac4j.cas.client.rest.CasRestFormClient;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.cas.config.CasProtocol;
import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
import org.pac4j.http.client.direct.ParameterClient;
import org.pac4j.jwt.config.encryption.SecretEncryptionConfiguration;
import org.pac4j.jwt.config.signature.SecretSignatureConfiguration;
import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator;
import org.pac4j.jwt.profile.JwtGenerator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
/**
* 對shiro的安全配置,是對cas的登入策略進行配置
*
* @author Carl
* @date 2017/9/16
* @since 1.0.0
*/
@Configuration
public class ShiroConfiguration extends AbstractShiroWebFilterConfiguration {
@Value("#{ @environment['cas.prefixUrl'] ?: null }")
private String prefixUrl;
@Value("#{ @environment['cas.loginUrl'] ?: null }")
private String casLoginUrl;
@Value("#{ @environment['cas.callbackUrl'] ?: null }")
private String callbackUrl;
//jwt祕鑰
@Value("${jwt.salt}")
private String salt;
@Bean
public Realm pac4jRealm() {
return new Pac4jRealm();
}
/**
* cas核心過濾器,把支援的client寫上,filter過濾時才會處理,clients必須在casConfig.clients已經註冊
*
* @return
*/
@Bean
public Filter casSecurityFilter() {
SecurityFilter filter = new SecurityFilter();
filter.setClients("CasClient,rest,jwt");
filter.setConfig(casConfig());
return filter;
}
/**
* JWT Token 生成器,對CommonProfile生成然後每次攜帶token訪問
* @return
*/
@Bean
protected JwtGenerator jwtGenerator() {
return new JwtGenerator(new SecretSignatureConfiguration(salt), new SecretEncryptionConfiguration(salt));
}
/**
* 通過rest介面可以獲取tgt,獲取service ticket,甚至可以獲取CasProfile
* @return
*/
@Bean
protected CasRestFormClient casRestFormClient() {
CasRestFormClient casRestFormClient = new CasRestFormClient();
casRestFormClient.setConfiguration(casConfiguration());
casRestFormClient.setName("rest");
return casRestFormClient;
}
@Bean
protected Clients clients() {
//可以設定預設client
Clients clients = new Clients();
//token校驗器,可以用HeaderClient更安全
ParameterClient parameterClient = new ParameterClient("token", jwtAuthenticator());
parameterClient.setSupportGetRequest(true);
parameterClient.setName("jwt");
//支援的client全部設定進去
clients.setClients(casClient(), casRestFormClient(), parameterClient);
return clients;
}
/**
* JWT校驗器,也就是目前設定的ParameterClient進行的校驗器,是rest/或者前後端分離的核心校驗器
* @return
*/
@Bean
protected JwtAuthenticator jwtAuthenticator() {
JwtAuthenticator jwtAuthenticator = new JwtAuthenticator();
jwtAuthenticator.addSignatureConfiguration(new SecretSignatureConfiguration(salt));
jwtAuthenticator.addEncryptionConfiguration(new SecretEncryptionConfiguration(salt));
return jwtAuthenticator;
}
@Bean
protected Config casConfig() {
Config config = new Config();
config.setClients(clients());
return config;
}
/**
* cas的基本設定,包括或url等等,rest呼叫協議等
* @return
*/
@Bean
public CasConfiguration casConfiguration() {
CasConfiguration casConfiguration = new CasConfiguration(casLoginUrl);
casConfiguration.setProtocol(CasProtocol.CAS30);
casConfiguration.setPrefixUrl(prefixUrl);
return casConfiguration;
}
@Bean
public CasClient casClient() {
CasClient casClient = new CasClient();
casClient.setConfiguration(casConfiguration());
casClient.setCallbackUrl(callbackUrl);
return casClient;
}
/**
* 路徑過濾設定
*
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
definition.addPathDefinition("/callback", "callbackFilter");
definition.addPathDefinition("/logout", "logoutFilter");
definition.addPathDefinition("/**", "casSecurityFilter");
return definition;
}
/**
* 由於cas代理了使用者,所以必須通過cas進行建立物件
*
* @return
*/
@Bean
protected SubjectFactory subjectFactory() {
return new Pac4jSubjectFactory();
}
/**
* 對過濾器進行調整
*
* @param securityManager
* @return
*/
@Bean
protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
//把subject物件設為subjectFactory
((DefaultSecurityManager) securityManager).setSubjectFactory(subjectFactory());
ShiroFilterFactoryBean filterFactoryBean = super.shiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setFilters(filters());
return filterFactoryBean;
}
/**
* 對shiro的過濾策略進行明確
* @return
*/
@Bean
protected Map<String, Filter> filters() {
//過濾器設定
Map<String, Filter> filters = new HashMap<>();
filters.put("casSecurityFilter", casSecurityFilter());
CallbackFilter callbackFilter = new CallbackFilter();
callbackFilter.setConfig(casConfig());
filters.put("callbackFilter", callbackFilter);
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setConfig(casConfig());
filters.put("logoutFilter", logoutFilter);
return filters;
}
}
token生成
@RequestMapping("/user/login")
public Object login(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> model = new HashMap<>();
J2EContext context = new J2EContext(request, response);
final ProfileManager<CasRestProfile> manager = new ProfileManager(context);
final Optional<CasRestProfile> profile = manager.get(true);
//獲取ticket
TokenCredentials tokenCredentials = casRestFormClient.requestServiceTicket(serviceUrl, profile.get(), context);
//根據ticket獲取使用者資訊
final CasProfile casProfile = casRestFormClient.validateServiceTicket(serviceUrl, tokenCredentials, context);
//生成jwt token
String token = generator.generate(casProfile);
model.put("token", token);
return new HttpEntity<>(model);
}
由於以上程式碼僅做為學習參考
需要執行該測試,需要下載單點登入程式碼,並且執行,執行教程需要參考
執行步驟:
1. 啟動sso
2. 啟動wolf-permission-management
測試
通過使用者名稱密碼非同步請求獲取token
http://localhost:8082/user/login?client_name=rest&username=admin&password=123
返回的json
{
"token": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..zDvUm-Q9YhwdvcR1.fz5-ar7FEWEnisDjyjZZ5lgb8xWaS5sffOafYUeZ_-sJJLSx2utoDOK2d1gS8G2oYbg7kbSR_Pcb_m3x3a_awLsoki79LOtk-yI1INWcYJTuF_zY27JPIqAOXF8GFwIc0QzuPke6mLoVUzuwc7ILjvqXg9Z2VA7hWrYJZ1WNsO3UDyPAltq9TXgzl3aJc3XBAUXPNzw8BLUDclTUGs1MnNzjlZYI94qVZgsybZwokkXh1WZ8JEnc7XGtFNJVQOHiIbhSCYJgkjb5xjEtZRRbI7LGSn-kPm99tau68hrR3qbcZofR3lxZ8p-Cta9EHiZ-SQGFg6yBNFSslQnNSoYPuyOo4kFtUgUvG9QJJgwMj8E_sXnixJ4rBkcLuok7mBvkelYr8CbBUyMdZJvwhPCEt9yaBDxJSGL-osWhvseoFFVc9Rp-Egie-NCuUzrygzi2juwMyLLsHybQL6m77rn3Hi_flgAtClUeyuwLbCSQ_Qn2fSz31NCydkxsC7NIsMf-VDiQwQMe9eO3WLcAkr1EAUCyMse61eww9n944350oXhMAQLpShr45AEXNHQ7wls5ZS1Wh6llm4kpQyMh_fwfNEAS7bPBlncKc9rjMloYaQPIkshxh5qysVxPlmWSctGxDdQMzQoJWGaeA9ZXSivF0p_zDAAqM0PAwUPYlBlRkCbO-2I7fapCs-5r0zpZBvYCwYxr4-h1n98x.v8ucroIDOmJbMLLwUkLUow"
}
通過token訪問資源
http://localhost:8082/user/detail?client_name=jwt&token=eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIn0..zDvUm-Q9YhwdvcR1.fz5-ar7FEWEnisDjyjZZ5lgb8xWaS5sffOafYUeZ_-sJJLSx2utoDOK2d1gS8G2oYbg7kbSR_Pcb_m3x3a_awLsoki79LOtk-yI1INWcYJTuF_zY27JPIqAOXF8GFwIc0QzuPke6mLoVUzuwc7ILjvqXg9Z2VA7hWrYJZ1WNsO3UDyPAltq9TXgzl3aJc3XBAUXPNzw8BLUDclTUGs1MnNzjlZYI94qVZgsybZwokkXh1WZ8JEnc7XGtFNJVQOHiIbhSCYJgkjb5xjEtZRRbI7LGSn-kPm99tau68hrR3qbcZofR3lxZ8p-Cta9EHiZ-SQGFg6yBNFSslQnNSoYPuyOo4kFtUgUvG9QJJgwMj8E_sXnixJ4rBkcLuok7mBvkelYr8CbBUyMdZJvwhPCEt9yaBDxJSGL-osWhvseoFFVc9Rp-Egie-NCuUzrygzi2juwMyLLsHybQL6m77rn3Hi_flgAtClUeyuwLbCSQ_Qn2fSz31NCydkxsC7NIsMf-VDiQwQMe9eO3WLcAkr1EAUCyMse61eww9n944350oXhMAQLpShr45AEXNHQ7wls5ZS1Wh6llm4kpQyMh_fwfNEAS7bPBlncKc9rjMloYaQPIkshxh5qysVxPlmWSctGxDdQMzQoJWGaeA9ZXSivF0p_zDAAqM0PAwUPYlBlRkCbO-2I7fapCs-5r0zpZBvYCwYxr4-h1n98x.v8ucroIDOmJbMLLwUkLUow
返回
users:admin
下章介紹
最近很多朋友問我,QQ登入、微信登入如何整合,怎麼做?這些都是第三方的登入整合,目前通常的做法都是採用OAuth2協議來解決,cas統稱為代理登入
其實這個問題也延伸出好幾個問題:
- 業務系統整合OAuth
- 第三方登入後,使用者繫結(如csdn通過github登入,要求繫結csdn賬號)
我覺得最有意思的是第三點,那麼下一章給大家講解一下,也簡單寫個demo,當然由於國內的網路安全法限制的多,將不採取QQ登入或者微信登入,直接採用GitHub的登入即可,因為都是OAuth2登入,但qq的登入是返回xml,這個是需要另解決的
作者聯絡方式
如果技術的交流或者疑問可以聯絡或者提出issue。
QQ: 756884434 (請註明:SSO-CSDN)