夜雨
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    <style> .red { color: red; } </style> # SSO 技術實踐: CAS整合JWT(Client篇) ###### tags: `CAS` ## 前言: 這篇需要搭配[SSO 技術實踐: CAS整合JWT(Server篇)](/328QeSlORYeg8tsmPDfLIQ)一起看,這篇主要會介紹關於jwt如何在client這邊驗證,以及處理關於server發過來的SLO request怎麼處理。 另外,關於此篇不管是實作還是設定,都只是基於開發練習用,實際上關於認證及驗證的方式相信會有更好的方式,此篇僅為了練習而採用簡單的方式實作。 -------------------------------------------------- ## 使用版本及使用套件: * CAS Server相關設定傳送門: [SSO 技術實踐: CAS整合JWT(Server篇)](/328QeSlORYeg8tsmPDfLIQ) * CAS Server v6.0: https://github.com/apereo/cas-overlay-template/tree/6.0 * CAS Client: https://github.com/cas-projects/cas-sample-java-webapp * Tomcat Server: CAS Server * 1、CAS Client * 2 CAS Protocol v3.0 Server使用套件(請加入在build.gradle): * [JWT as Service Ticket](https://apereo.github.io/cas/6.6.x/installation/Configure-ServiceTicket-JWT.html) * [LDAP Authentication](https://apereo.github.io/cas/6.6.x/installation/LDAP-Authentication.html#ldap-authentication) Client使用套件(請加入在pom.xml): * [JOSE4j v0.7.0](https://mvnrepository.com/artifact/org.bitbucket.b_c/jose4j/0.7.0) :解析和驗證jwt字串 * [JSON](https://mvnrepository.com/artifact/org.json/json/20190722) * [OPENSAML J v2.6.4](https://mvnrepository.com/artifact/org.opensaml/opensaml/2.6.4) :接收Server發送的SLO request時需要解析saml字串 * [Tomcat util v8.5.23](https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-util/8.5.23) -------------------------------------------------- ## CAS Client web.xml設定 因為整體web.xml配置有點長,會拆局部一一說明。 可參考: [java cas 單點登入web.xml配置](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/617059/)[1] #### CAS Single Sign Out Filter: 由於filter的順序是會影響實際先流入哪個filter,因此順序非常重要,SSO Filter之所以需要擺第一個,是因為這是主要在控管sso狀態的filter。 下方的listener則是負責監聽slo request的部分,<span class="red">這個listener如果沒有的話,client會收不到訊息,而server則是不管發出的slo request是否有成功,都不會報異常!!</span> 所以想知道問題出在哪,就必須把中斷點下在filter內,當初也是因為這樣才發現slo request溝通過程ssl handshake失敗... ```xml= <filter> <filter-name>CAS Single Sign Out Filter</filter-name> <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>https://CAS Server的域名/cas</param-value> </init-param> </filter> <listener> <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class> </listener> <filter-mapping> <filter-name>CAS Single Sign Out Filter</filter-name> <url-pattern>/servlet/login</url-pattern> </filter-mapping> ``` #### CAS Authentication Filter: 此filter負責導向CAS Server做登入認證,如果是剛從git抓下來解壓後,他應該是會寫成`/*`的pattern,但因為我們要讓只有經過登入頁面的request才導去cas server,因此url pattern需要設定成client登入的路徑。 ```xml= <filter> <filter-name>CAS Authentication Filter</filter-name> <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> <init-param> <param-name>casServerLoginUrl</param-name> <param-value>https://CAS Server的域名/cas/login</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>https://當前Client的域名/cas-sample-java-webapp1/index.jsp</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS Authentication Filter</filter-name> <url-pattern>/servlet/login</url-pattern> </filter-mapping> ``` #### CAS Validation Filter: 此filter是用來驗證ticket的部分,<span class="red">但請注意!!</span> 原本如果只是單純的CAS流程,這邊的確是需要設定的,但因為今天我們採取的是<span class="red">搭配jwt的流程,變成驗證的動作是在client端執行</span>,所以這整段都需要註解掉,不需要設定,也就是說下面這段看看就好~ ```xml= <filter> <filter-name>CAS Validation Filter</filter-name> <filter-class>org.jasig.cas.client.validation.Saml11TicketValidationFilter</filter-class> <filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>https://CAS Server的路徑/cas</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>https://當前Client的域名/cas-sample-java-webapp1/index.jsp</param-value> </init-param> <init-param> <param-name>redirectAfterValidation</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>useSession</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS Validation Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ``` #### CAS HttpServletRequest Wrapper Filter 這個filter底層實現了能透過sso中已登錄的session取得principleUser的資料,如果是透過CAS Server驗證成功的話,會塞principle資料到session中,但因為我們沒透過CAS Server做驗證,所以基本上想取得資料,都是透過parse jwt string,如果直接使用session.getRemoteUser()會得到null。 ```xml= <filter> <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name> <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class> </filter> <filter-mapping> <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ``` #### loginFilter 主要負責request流進來後的走向,這屬於自己實作的部分,不用完全參照,只要登入流程可以work即可,這邊設定為不管任何入口都會攔住進行檢查是否登入。 ```xml= <filter> <display-name>loginFilter</display-name> <filter-name>loginFilter</filter-name> <filter-class>filter.loginFilter</filter-class> </filter> <filter-mapping> <filter-name>loginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ``` #### Servlet: login 檢查是否直接攜帶ticket,有的話直接導入到jwtValidate。 ```xml= <servlet> <display-name>login</display-name> <servlet-name>login</servlet-name> <servlet-class>servlet.login</servlet-class> </servlet> <servlet-mapping> <servlet-name>login</servlet-name> <url-pattern>/servlet/login</url-pattern> </servlet-mapping> ``` #### Servlet: logout 這邊有做接收及處理server發送過來的slo request,saml字串處理的部分有另一部分實作。 ```xml= <servlet> <display-name>logout</display-name> <servlet-name>logout</servlet-name> <servlet-class>servlet.logout</servlet-class> </servlet> <servlet-mapping> <servlet-name>logout</servlet-name> <url-pattern>/logout</url-pattern> </servlet-mapping> ``` ## loginFilter實作 這邊主要是當作一開始進來的入口,如果是直接走到login或logout的入口,直接往下流即可,因為下面會有`CAS Authentication Filter`接住後導向CAS Server進行登入。 如果是request其他路徑,就檢查有沒有帶jwt屬性而且驗證過沒有,都沒有的話就導去登入入口,有jwt就導去驗證~ ```java= public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; Enumeration<String> headerNames = httpRequest.getHeaderNames(); while (headerNames.hasMoreElements()) { System.out.println("header:" + httpRequest.getHeader(headerNames.nextElement())); } if(httpRequest.getServletPath().equals("/servlet/login") || httpRequest.getServletPath().equals("/logout")) { chain.doFilter(request, response); } else if(httpRequest.getSession().getAttribute("jwt") == null) { httpResponse.sendRedirect(httpRequest.getContextPath() + "/servlet/login"); } else { chain.doFilter(request, response); } } ``` ## login.java 這邊單純將login近來的入口導到index.jsp裡面。 ```java= protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if(jwtValidate.jwtvalid(request) && UserList.contains(request.getSession().getId())) { response.sendRedirect(request.getContextPath() + "/index.jsp"); } } ``` -------------------------------------------------- ## JWT驗證處理: jwtValidate.java 由於jwt字串驗證是在client端實作,這邊先提供cas社群的參考資料: [jwt-validation](https://apereo.github.io/cas/6.6.x/installation/Configure-ServiceTicket-JWT.html#jwt-validation---aes)[2] > 關於jwt、jws、jwe三者之間的關係,這篇文章非常有幫助: [JWT & JWE & JWS 大亂鬥!](https://blog.cssuen.tw/jwt-jwe-jws-%E5%A4%A7%E4%BA%82%E9%AC%A5-5ca68bebe426)[3] * 因為在CAS Server jwt有配置encryption key的關係,因此正確來說這邊不單純只是用到jws,而是應該稱<span class="red">jwe</span>才對。 * 由於signing key只是用來<span class="red">驗證簽名</span>的部分,如果想為內容增加安全性,可以使用encryption key把jwt當中的payload再加密一次,成為jwe之後丟給client處理,所以在client也需要配兩把key,但對於jwt的設定來說,encryption key為非必要,只需要設置signing key即可。 * 為了讓client端知道使用者的登入情形,所以我有寫了一個UserList去裝當登入成功後的使用者sessio、id、Service Ticket等等資料,這樣當CAS Server發slo request帶來的Service Ticket時,才能找到要廢除哪個session,但這邊只是為了實驗方便,其實應該是要搭配資料庫之類的才會比較符合實際使用情形。 * 要注意的是由於RFC規定的關係,傳輸的字串都會經過Base64 URLEncode的關係,一開始我完全不知道這件事,導致有時jwt拿去verifySignature()就會fail,找了很久後來才發現把%3D之類的換回=就成功了... 關於這部分的處理可參考: [Java 程式處理網址 URL 百分比編碼與解碼教學](https://blog.gtwang.org/programming/url-percent-encoding-and-decoding-using-java/)[4] ```java public static boolean jwtvalid(HttpServletRequest request) { final String signingKey = "將CAS Server中配置給client1的singingKey放在這"; final String encryptionKey = "將CAS Server中配置給client1的encryptionKey放在這"; final Key key = new AesKey(signingKey.getBytes(StandardCharsets.UTF_8)); final byte[] decodedByte; final String decodedPayload; final JsonWebEncryption jwe; final JsonWebKey jsonWebKey; final JsonWebSignature jws = new JsonWebSignature(); try { String jwt = ""; String queryString = URLDecoder.decode(request.getQueryString(), "UTF-8"); if(request.getSession().getAttribute("jwt") != null) { jwt = request.getSession().getAttribute("jwt").toString(); } else if(queryString.contains("ticket=")) { jwt = queryString.split("ticket=")[1]; } jws.setCompactSerialization(jwt); jws.setKey(key); if(!jws.verifySignature()) { throw new Exception("jwt signature verification failed"); } // jwe處理 decodedByte = Base64.decodeBase64(jws.getEncodedPayload().getBytes(StandardCharsets.UTF_8)); decodedPayload = new String(decodedByte, StandardCharsets.UTF_8); jwe = new JsonWebEncryption(); // kty參數設定請參考RFC 7518: Section 6.1 // oct: Octet sequence (used to represent symmetric keys) JSONObject jwejson = new JSONObject(); jwejson.put("kty", "oct"); jwejson.put("k", encryptionKey); jsonWebKey = JsonWebKey.Factory.newJwk(jwejson.toString()); jwe.setCompactSerialization(decodedPayload); jwe.setKey(new AesKey(jsonWebKey.getKey().getEncoded())); if(jwe.getPlaintextString().isEmpty() || jwe.getPlaintextString() == null) { System.out.println("decoded fail"); return false; } JSONObject jwspayload = new JSONObject(jwe.getPlaintextString()); for(Object claim: jwspayload.keySet()) { System.out.println("claim key: " + claim.toString()); System.out.println("claim value: " + jwspayload.get(claim.toString())); request.getSession().setAttribute(claim.toString(), jwspayload.get(claim.toString())); } long current = System.currentTimeMillis() / 1000L; System.out.println("now time: " + System.currentTimeMillis()/1000L); if(jwspayload.getLong("exp") < current) { request.getSession().removeAttribute("jwt"); System.out.println("token expired!"); return false; } else if(jwspayload.has("successfulAuthenticationHandlers")) { // 驗證成功,加入UserList裡面、session塞入jwt request.getSession().setMaxInactiveInterval(60 * 10); request.getSession().setAttribute("jwt", jwt); User user = new User(); user.setUsername(jwspayload.getString("sub")); user.setSessionid(request.getSession().getId()); user.setServiceticket(jwspayload.getString("jti")); user.setSession(request.getSession()); UserList.userlist.add(user); return true; } } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (JoseException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return false; } ``` -------------------------------------------------- ## UseerList.java 這一份其實參考價值不大,可自由實作或者透過資料庫拿資料都能達成,但為了閱讀完整性還是放上連結。 [UserList.java](/q1roFqbfQtG7eSp4Kg4G8Q) ## logout.java 由index.jsp連結到client logout的部分,是透過get的方式,同時也順便直接廢掉session,但是CAS Server打過來是透過<span class="red">POST的方式,帶了SAML格式的字串過來</span>,因此post的部分要另外處理。 ```java= protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // 透過本地logout.jsp登出 if(UserList.contains(request.getSession().getId())) { HttpSession session = UserList.getSessionById(request.getSession().getId()); UserList.removeUserById(request.getSession().getId()); session.invalidate(); } response.sendRedirect("https://sso.server.com:8443/cas/logout?service=https://sso.client1.com:18443/cas-sample-java-webapp1/web1/servlet/login"); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // 透過cas server發出logout request登出 String body = request.getReader().lines().collect(Collectors.joining()); System.out.println("body: " + URLDecoder.decode(body, "UTF-8")); String samlString = URLDecoder.decode(body, "UTF-8").split("logoutRequest=")[1]; HashMap<String, String> attributes = SamlUtils.getAttributes(samlString); HttpSession session = UserList.getSessionByST(attributes.get("SessionIndex")); if(session != null) { UserList.removeUserByST(attributes.get("SessionIndex")); session.invalidate(); } response.sendRedirect("https://sso.server.com:8443/cas/logout?service=https://sso.client1.com:18443/cas-sample-java-webapp1/web1/servlet/login"); } ``` -------------------------------------------------- ## SLO Request SAML字串處理: SamlUtils.java 這邊就是單純直接把saml字串拆出來之後用第三方套件處理,要注意的是他傳過來的tag有的是saml開頭,有些是samlp的部分,檔案連結在下方。 [SamlUtils.java](/wLkPFttuQn6F26YO3UydAQ) ## Reference [1] [java cas 單點登入web.xml配置](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/617059/) [2] [jwt-validation](https://apereo.github.io/cas/6.6.x/installation/Configure-ServiceTicket-JWT.html#jwt-validation---aes) [3] [JWT & JWE & JWS 大亂鬥!](https://blog.cssuen.tw/jwt-jwe-jws-%E5%A4%A7%E4%BA%82%E9%AC%A5-5ca68bebe426) [4] [Java 程式處理網址 URL 百分比編碼與解碼教學](https://blog.gtwang.org/programming/url-percent-encoding-and-decoding-using-java/)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully