使用場景:
- 移動端通過業務系統鑒權
- 移動端免登錄(登錄一次以后)
解決方案:
疑問
當然我們這章是講JWT,那么會有以下的疑問:
若服務端已經接入了SSO,那么在移動端用戶登錄信息提交給SSO還是服務端?(毫無疑問是服務端,SSO對於移動端必須是透明的)
若采用無會話方式,如何獲取token,服務端如何鑒權?
1.提交用戶名密碼到服務端,服務端把數據給到sso,sso最終返回用戶數據
2. 根據用戶數據創建token返回給移動端
3. 移動端登錄后請求都帶token給到服務端鑒權
在使用token鑒權的情況下,退出如何解決?(客戶端丟棄token即可)
鑒權流程
我們再講一下整一個鑒權流程

配置要素
重點:sso必須支持rest認證方式
<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);
}
