1. 程式人生 > >CAS單點登入-客戶端整合(shiro、springboot、jwt、pac4j)(十)

CAS單點登入-客戶端整合(shiro、springboot、jwt、pac4j)(十)

CAS單點登入-客戶端整合(shiro springboot jwt pac4j)(十)

由於我們通常在業務上會有以下的使用場景:

  • 移動端通過業務系統鑑權
  • 移動端免登入(登入一次以後)

解決方案:

  • JWT(token認證方案)
  • OAuth(第三方認證)

PS:若想繼續往下讀,必須具備JWT的基本概念以及Pac4j的認證原理及應用場景

疑問

當然我們這章是講JWT,那麼會有以下的疑問:

  1. 若服務端已經接入了SSO,那麼在移動端使用者登入資訊提交給SSO還是服務端?(毫無疑問是服務端,SSO對於移動端必須是透明的)
  2. 若採用無會話方式,如何獲取token,服務端如何鑑權?(1.提交使用者名稱密碼到服務端,服務端把資料給到sso,sso最終返回使用者資料 2. 根據使用者資料建立token返回給移動端 3. 移動端登入後請求都帶token給到服務端鑑權)
  3. 在使用token鑑權的情況下,退出如何解決?(客戶端丟棄token即可)

鑑權流程

我們再講一下整一個鑑權流程

Created with Raphaël 2.1.0MobileMobileServerServerSSOSSO提交使用者名稱密碼提交使用者名稱密碼鑑權(pac4j)成功返回UserProfile根據profile建立token返回jwt token攜帶token請求資源Shiro Subject.login()鑑權通過返回報文

配置要素

重點: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);
}

由於以上程式碼僅做為學習參考
需要執行該測試,需要下載單點登入程式碼,並且執行,執行教程需要參考

下載程式碼嘗試:GitHub GitHub

執行步驟:
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。

郵箱:[email protected]

QQ: 756884434 (請註明:SSO-CSDN)