1756 views
# MCL Project Presentation - SSO 日誌系統 ###### tags: `MCL` `108-2-project` 107 李家妤 [Project Proposal](https://hackmd.mcl.math.ncu.edu.tw/obzeQnWrRsCi1H9_A3qGpw) ## 報告綱要 * ~~教大家如何新建資料夾~~ * 簡介 Laravel 框架與 MVC 架構 * SSO 簡介 * OAuth2 介紹與使用 Laravel 實作 * 網頁 Demo ## Laravel 框架 ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_6d2edfffb112d7bed9acf477bdbce05a.png) > 真是太可惜了 > Source from [here](https://dometi.com.tw/blog/laravel-beginner-01/) ### 網頁框架 (Framework) 以蓋房子來比喻,框架就是別人設計好了房子的骨架、元件,連地基都幫你打好了。而你就可以使用這套骨架去建構你想要的房子,而不需自己從零開始。 而在對寫程式來說,就是去使用別人設計好的函式庫 (Library) 或類別庫 (Class Library),同時遵守它所定義的架構。網頁現在大多數的框架都參考 MVC 架構為概念來設計。 使用框架的好處 : * 可以加快開發速度 * 讓程式碼更加優美,易讀 * 遇到問題的時候會很好找到答案 > **完全新手適合從 Laravel 開始學嗎** : 我個人是覺得有點越級打怪的感覺,但熟了之後就會順便連基礎 (PHP, CSS, JS....) 一起學起來ㄌ > > 不過一開始看的時候真的會懷疑人生^^ > > ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_3e58550055d155fad1b46abdf2607556.png) > > P.S. : 這我 Google "人生重來槍" 找到的 (雖然我比較想要結束就是了XD) ### MVC (Model-View-Controller) 架構 MVC 模式是一種軟體架構模式,將軟體系統分成三大部分 : * Model : 負責所有的商業邏輯以及資料處理,擁有對資料的直接存取權。 * Controller : 負責接收請求,轉發給 Model 處理。 * View : 負責介面呈現。 換成示意圖大概長這樣 <img src='https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_4faf67eed02f565462ab70bd06877934.png' width="400px"> > Source from wikipedia 但 Laravel 的 MVC 與傳統架構不太一樣,稍加整理後整個流程大概會長這樣 <img src="https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_7b6b5af635bab947050f4f35069039d6.png" width="400px"> > Source from [here](https://docs.mcl.math.ncu.edu.tw/books/%E7%B6%B2%E9%A0%81%E8%A8%AD%E8%A8%88/page/ep-2-laravel-%E8%88%87-mvc) and [here](https://dometi.com.tw/blog/laravel-beginner-03/) ### Laravel 目錄架構 當你建立好一個 Laravel 專案的時候,資料夾結構會長這樣 ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_66c06c1ea5cad8de2b1cfbf5b1b52ec3.png) 下面這邊是每個資料夾跟重要檔案的簡單介紹,我這邊只會講常用 (有加粗體) 的,其他如果有興趣再自己看。 * **app** : app 目錄底下包含了整個網站大多數的核心程式,之後我們所寫的程式大多都會在這個資料夾中,其中包含了 Controller 以及 Model 的部分。 * bootstrap : 進行 Laravel 的初始化,載入相關程式。另外也包含了cache的資料夾,用來提升網站運作的效能。 > 不是前端那個 Bootstrap 喔不要搞混XD * config : 顧名思義,裡面放著各式各樣的設定檔,包含前面提到的 app, database,還有像是 session, cache, mail 等等的設定。 * **database** : 裡面主要包含3個部分 : * Migration – 用於建立資料表,並記錄每一次資料表的更新,讓資料庫也可以進行版本控制。 * Seed – 產生假資料,可以呼叫 Factory 來大量產生。 * Factory – 負責如何產生假資料的邏輯,然後給 Seed 使用。 * **public** : 專門放公開的檔案,包含網站的 index 文件,還有 CSS, JavaScript, Image 等等都會放在這裡。 * **resources** : 最主要就是包含了 View,也就是網頁的前端。其他還有網頁語言檔 (你可以輕易的讓網站顯示不一樣的語言),以及還沒有被編譯的資源 (e.g LESS, SASS…)。 * **routes** : 就像網路的路由器一樣,裡面有好多路由器,去處理使用者存取網站的要求。最常用到的就是 web.php。 * storage : 這個目錄會包含編譯過後的 Blade templates (後面會提到), 檔案式的 session, cache 以及 log 紀錄等各式各樣 Laravel 自己產生的檔案。 * tests : tests 目錄用來放自動化測試的程式,基本上不會用到。 * vendor : 從 composer 安裝的套件,都會在 vendor 裡面。同時這個資料夾在版本控制時會被排除,不會一起被同步 (因為檔案太多)。 * **.env** : 環境設定檔,會放一些 Laravel 專案本身需要用到的環境變數 (如連接資料庫時需要的變數等等)。 * **composer.json** : 這個專案本身會用到的所有套件以及套件版本。 知道這些之後,就可以開始寫屬於你自己的第一個網頁了喔<3 ## SSO (Single Sign-On 單一登入) > 接下來我都會拿 MCL 內部服務當例子 > 補充小知識 -- 認證 (authentication) 和授權 (authorization) : > > 認證是實體 (使用者、應用程式或元件) 用於確定其他實體是否是其宣告的實體的方法。 > 授權則是決定該使用者可以執行哪些作業。 > > 我們以日誌網頁當作例子 : > 要登入日誌網頁的時候,你會需要輸入帳號密碼來證明這是你本人,這就是認證。 > 而授權就是當你登入後會確定你是否為管理員,如果是的話會給你權限去修改一些非管理員不能修改的東西 (像是公告或是使用者管理等等)。 > > Source from [here](https://docs.oracle.com/cd/E19900-01/820-0848/6nciel12k/index.html) and [here](https://www.itread01.com/content/1544361318.html) ### 從傳統登入到單一登入 ![](https://codimd.mcl.math.ncu.edu.tw/uploads/upload_6f88ecf4b6e2c4c10c1cf9c93ae94942.png) #### 傳統登入 例子 : 就是在日誌網頁那邊手動輸入帳號密碼登入。 在傳統登入的架構下,我們瀏覽一個需要驗證的網頁的時候的流程大致上會是這樣 : 1. 確認使用者是否已經被認證,有的話就讓使用者存取。 2. 沒有的話,就需要輸入帳號密碼確認是否用戶資料有在 user datebase 裡。 3. 登入後,每當使用者瀏覽新頁面時,網頁會傳認證訊息到 server 確認使用者是否已經被認證。 下面是示意圖 <img src="https://codimd.mcl.math.ncu.edu.tw/uploads/upload_0f5fb32c0db191badb38a4a99edb74ff.png" width="300px"> > Source from [here](https://www.onelogin.com/learn/how-single-sign-on-works) 當我們在建立一個網站的時候,因為每個網站都會有屬於自己的一套登入系統,用戶就會需要記住自己在每個網站的設定的帳號密碼,而且每個系統都需要自己維護使用者的帳號以及密碼,被攻擊的危險自然就提高了。因此許多大廠開始提供登入 (還有身分驗證) 的服務,所謂的 SSO 就這樣誕生了。 #### 單一登入 例子 : 日誌網頁用 Google 或是 MCL 登入 在單一登入的架構下,會有一個網站 ([MCL 提供的 SSO 系統](https://sso.mcl.math.ncu.edu.tw/auth/)) 扮演所有應用系統之間唯一的身分驗證中心,它會負責維護管理所有使用者唯一的一組帳號密碼還有身分識別的功能,而終端使用者在這個認證中心進行登入,其他的所有應用系統都可以透過認證中心獲取該使用者的登入狀態。 > 簡單來說,假如我在 Hackmd 登入了,那我在日誌系統如果使用 MCL 登入的話就不需要再打一次帳號密碼,認證中心會幫你自動登入 > 在 SSO 概念裡,身分認證中心會稱之為 Identity Provider(簡稱 IDP),應用系統會稱為 Service Provider(簡稱 SP~~4~~) 單一登入的概念大概會長這樣 : 1. 先確認你在認證中心是否為登入狀態,沒有的話會要求你登入 2. 一樣也是輸入帳號密碼,不同的是這邊要輸入的是你在單一登入服務的帳號密碼 3. 在認證中心認證成功後,會回傳使用者資料給原本的應用系統 4. 應用系統在接收到資料後,會檢查此使用者是否有被授權使用系統功能 5. 登入後,每當使用者瀏覽新頁面時,網頁會到認證中心取得認證訊息確認使用者是否已經被認證。 示意圖 : <img src="https://codimd.mcl.math.ncu.edu.tw/uploads/upload_cd090e3dcacaac7c9604985372d59b38.png" width="300px"> <img src="https://codimd.mcl.math.ncu.edu.tw/uploads/upload_0dc2693cd3379064a7d069bad5195931.png" width="300px"> > Source from [here](https://www.onelogin.com/learn/how-single-sign-on-works) 既然有這個概念了,那我們就會需要一些方法來實現它;目前實現的方法主要有兩種 : OIDC 1.0 與 SAML 2.0。 下面是協定演進圖 ![](https://codimd.mcl.math.ncu.edu.tw/uploads/upload_723250513b892265a25e31ad4b1fe13a.png) > Source from [here](https://blog.aspiresys.pl/technology/oauth-vs-oidc-vs-saml-security-battle-royale/) ### OAuth 2.0 為什麼前面講到 OIDC 跟 SAML 這兩種協定但突然冒出來一個 OAuth 呢 ? ~~因為我爽~~ 其實 OpenID Connect (OIDC) 是以 OAuth 2.0 為基礎的認證通訊協定,差別是 OIDC 主要是以認證為主,而 OAuth 是以授權為主,而我們這次的專案主要 OAuth 協定,所以這篇主要重點會放在介紹 OAuth 2.0。 > OIDC 跟 OpenID 主要的差異就是前面有提到 ODIC 是架構在 OAuth 2.0 之上,而OpenID 1.0/1.1/2.0 則是獨立的協定,但目的都一樣是規範線上的認證機制。 #### 角色定義 * Resource Owner : 可以授權別人存取 Protected Resource。(MCL 使用日誌網頁的使用者) * Resource Server : 存放 Protected Resource 的 Server,可以根據 Access Token 來處理 Protected Resource 的請求。(MCL 的認證中心) * Client : 代表 Resource Owner 存取 Protected Resource 的應用程式。(日誌網頁) > 這邊的 Client 不限定任何實作方式 * Authorization Server : 在認證過 Resource Owner 並且 Resource Owner 許可之後,核發 Access Token 的伺服器。(MCL 的認證中心的 Server) #### 簡介 在 OAuth 裡,Client 會先索取存取權來存取 Resource Owner 的資源 (資源會放在 Resource Server 上),而 Client 會取得一個 Access Token 來存取 Protected Resources ,而非使用 Resource Owner 的帳號密碼。 Access Token 是一個字串,記載了特定的存取範圍 (scope) 、時效等等的資訊。且 Access Token 是從 Authorization Server 拿到的,取得之前會得到 Resource Owner 的許可。Client 用這個 Access Token 來存取 Resource Server 上面的 Protected Resources 。 舉日誌網頁來當實際例子 : 當我們 (Resource Owner) 在登入日誌網頁 (Client) 的時候,我們會授權日誌網頁去 MCL 的認證中心 (Resource Server) 來存取我們放在那邊的使用者資料 (Protected Resources) (因為我們要把使用者資料存進日誌網頁,它才會知道你是誰)。而使用者就會透過認證中心信任的伺服器 (Authorization Server) 核發一個屬於日誌網頁認證碼 (Access Token)。 #### 抽象化流程 以下是抽象化的流程概觀,以比較宏觀的角度來描述,**不是實際程式運作的流程** ``` +--------+ +---------------+ | |--(1)- Authorization Request ->| Resource | | | | Owner | | |<-(2)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(3)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(4)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(5)----- Access Token ------>| Resource | | | | Server | | |<-(6)--- Protected Resource ---| | +--------+ +---------------+ ``` (1) : Client 向 Resource Owner 請求授權。這個授權請求可以直接向 Resource Owner 發送 (如圖),或是間接由 Authorization Server 來請求。 (2) : Client 得到來自 Resource Owner 的 Authorization Grant (授權許可)。這個 Grant 是用來代表 Resource Owner 的授權。 (3): Client 向 Authorization Server 請求 Access Token ,Client 要認證自己,並出示 Authorization Grant。 (4): Authorization Server 認證 Client 並驗證 Authorization Grant。如果都合法,就核發 Access Token 。 (5): Client 向 Resource Server 請求 Protected Resource ,Client 要出示 Access Token。 (6): Resource Server 驗證 Access Token ,如果合法,就處理該請求。 * Authorization Grant : 代表了 Resource Owner 授權 Client 可以去取得 Access Token 來存取 Protected Resource 。 > Client 從 Resource Owner 取得 Authorization Grant 的方式 (圖中的 (A) 和 (B) 流程) 會比較偏好透過 Authorization Server 當作中介。 * Access Token : Access Token 用來存取 Protected Resource ,是一個具體的字串 (string),其代表特定的 scope (存取範圍)、時效。概念上是由 Resoruce Owner 授予,Resource Server 和 Authorization Server 遵循之。 #### Client 註冊 剛剛的宏觀流程圖有提到 : Client 需要認證自己並且驗證授權許可後才能取得 Access Token,想當然的 Client 也必須要像 Authorization Server 證明自己的身分,所以就會需要像 Authorization Server 註冊。 註冊的時候我們主要會需要 Client ID 與 Client Secret (類似使用者的帳號密碼),除此之外也需要指定一個 Redirection URL 讓授權後得到的資料可以從這個網址接收。 知道這些之後就可實作 SSO 功能了喔 (≧ω≦)/ ### 利用 Laravel 實作 SSO 功能 (Socialite) #### Google 因為 Laravel 的 Socialite 套件裡有內建 Google 方法,所以主要重點是要去 Google 那邊申請一個你自己的專案拿到 Client ID 跟 Client Secret。 [網址在這邊](https://console.developers.google.com/apis/credentials/oauthclient) <img src="https://codimd.mcl.math.ncu.edu.tw/uploads/upload_5f0e7ff436debd7fc1d513aa56195956.png" width="800px"> 然後在 Laravel 編輯環境設定檔 ```php= # .env GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID # 你在 Google 開啟專案的用戶端編號 GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET # 用戶端密碼 GOOGLE_REDIRECT_URL=YOUR_REDIRECT_URL # 你接收回傳資料的網址 ``` ![](https://codimd.mcl.math.ncu.edu.tw/uploads/upload_5631f22680f90a881a3aa7a7d01c79d4.png) 然後要把你這邊的設定套用到 Socialite 的 Google 方法上,不然它會不知道應該傳什麼資料過去,也不知道要回傳到哪個網址 ```php= # config\services.php return[ 'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT_URL'), ] ] ``` 之後在 `LoginController` 增加這項登入功能,然後再把 Controller 連接到 Route 上就可以了 > 喔對,還要記得在登入畫面那邊加上一個按鈕給人家按,不然別人會不知道你有這個功能喔 (ฅ´ωˋฅ) :::spoiler Controller & Route ```php= # app\Controllers\Auth\LoginController.php public function redirectToGoogleProvider() { return Socialite::driver('google')->redirect(); } public function handleGoogleProviderCallback(GoogleAccountServices $service) { $user = $service->CheckOrCreateUser(Socialite::driver('google')->stateless()->user()); auth()->login($user); return redirect('/index'); $user->token; } ``` ```php= # routes/web.php Route::get('login/google', 'Auth\LoginController@redirectToGoogleProvider'); Route::get('login/google/callback', 'Auth\LoginController@handleGoogleProviderCallback'); ``` ::: 不過在這邊我們要注意一件事,再使用者授權給網頁拿到使用者的資料之後,我們必須將使用者的資料存到這個網頁的 User 的 Database 裡,不然網頁再檢查使用者的時候一樣會認為沒有這個使用者。然後就會永遠沒辦法使用登入後才能用的功能喔 為了實現這件事,我們就需要一個方法來存取使用者的資料 > 這邊存取使用者資料的方式跟資料型式因人而異,我這裡就是提供一個方法而已 :::spoiler Service ```php= # \app\Services\GoogleAccountService.php public function CheckOrCreateUser(ProviderUser $providerUser) { $account = ModelGoogleAccount::whereProviderUserId($providerUser->getId()) ->first(); if ($account) { return $account->user; } else { $account = new ModelGoogleAccount([ 'provider_user_id' => $providerUser->getId(), ]); $user = User::whereEmail($providerUser->getEmail())->first(); if (!$user) { // dd($providerUser->getAvatar()); $user = User::create([ 'avatar' => $providerUser->getAvatar(), 'provider' => 'Google', 'email' => $providerUser->getEmail(), 'name' => $providerUser->getName(), 'password' => md5(rand(1, 10000)), 'admin' => false, ]); } $account->user()->associate($user); $account->save(); return $user; } } ``` ::: #### MCL 實作 MCL 的方法跟 Google 的方法其實大同小異,唯一不同的是因為我們 SSO 的服務不是官方的,所以 Socialite 套件並不會自動幫你取得 Access Token 與授權,方法我們要自己寫,以下就是我們實際取得 Access Token 與授權的方法 : :::spoiler MCLServiceProvider ```php= # app\Provider\MCLProvider.php /** * {@inheritdoc} */ protected function getAuthUrl($state) { return $this->buildAuthUrlFromBase(self::MCL_BASE_URL . '/auth', $state); } /** * {@inheritdoc} */ protected function getTokenUrl() { return self::MCL_BASE_URL . '/token'; } /** * {@inheritdoc} */ protected function getUserByToken($token) { $userUrl = self::MCL_BASE_URL . '/userinfo'; $response = $this->getHttpClient()->get($userUrl, [ 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer '.$token, ], ]); return (array) json_decode($response->getBody(), true); } /** * Get the access token response for the given code. * * @param string $code * @return array */ public function getAccessTokenResponse($code) { $postKey = (version_compare(ClientInterface::VERSION, '6') === 1) ? 'form_params' : 'body'; $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 'form-params' => array( 'auth' => [$this->clientId, $this->clientSecret], 'headers' => ['Accept' => 'application/json'] ), $postKey => $this->getTokenFields($code),]); return json_decode($response->getBody(), true); } /** * {@inheritdoc} */ protected function mapUserToObject(array $user) { return (new User)->setRaw($user)->map([ 'id' => $user['preferred_username'], 'name' => Arr::get($user, 'name'), 'email' => Arr::get($user, 'email'), 'picture' => Arr::get($user, 'picture'), 'verified_email' => Arr::get($user, 'email_verified'), ]); } /** * {@inheritdoc} */ protected function getTokenFields($code) { return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; } ``` ::: 因為這個 Provider 是你自己寫的,所以你要把它連接到 Socialite 的功能上,不然它會找不到這個方法,最好的方法就是把它加到 `AppServiceProvider` 裡然後用 `boot` 函數呼叫它 (比較安全) :::spoiler AppServiceProvider ```php= # \app\Providers\AppServiceProvider.php public function boot() { // $this->bootMCLSocialite(); } private function bootMCLSocialite() { $socialite = $this->app->make('Laravel\Socialite\Contracts\Factory'); $socialite->extend( 'MCL', function ($app) use ($socialite) { $config = $app['config']['services.mcl']; return $socialite->buildProvider(MCLProvider::class, $config); } ); } ``` ::: 之後一樣也在 LoginController 跟 Route 增加這個功能就可以使用了~ :::spoiler Controller & Route ```php= # app\Http\Controllers\Auth\LoginController.php public function redirectToMCLProvider() { return Socialite::with('MCL')->redirect(); } public function handleMCLProviderCallback(MCLAccountServices $service) { $user = $service->CheckOrCreateUser(Socialite::with('MCL')->stateless()->user()); auth()->login($user); return redirect('/index'); $user->token; } ``` ```php= # routes\web.php Route::get('login/mcl', 'Auth\LoginController@redirectToMCLProvider'); Route::get('login/mcl/callback', 'Auth\LoginController@handleMCLProviderCallback'); ``` ::: 這樣就完成ㄌ,這邊是在進行 SSO 的時候日誌網頁會經過的流程 : ```flow diary=>start: 日誌網頁 auth=>operation: 取得授權 token=>operation: 認證授權與日誌網頁取得 Access Token getdata=>operation: 使用 Access Token 取得資料 return=>operation: 從取得資料中選取需要的項目並回傳 check=>operation: 確認資料是否有在 User database 中 (沒有的話就建一個) login=>end: 成功登入 diary->auth->token->getdata->return->check->login ``` <!-- > 善用 composer dump-autoload --> ## 網頁 Demo 有些功能 (Ex : 安排班表這個有嚴重的短時間實現困難XD) 尚未完成,但基本功能 (日誌、代辦、注意事項跟公告) 已經可以使用而且可以用 Google 與 MCL 登入了。 > 正式上線之後會把 Google 登入功能拿掉,現在就是都開給大家玩玩看醬 網址 : [點這裡](http://140.115.26.46:7777) > 這個日誌網頁應該下學期就會上線,有什麼想要加減的功能或排版問題都可以跟我反映~ ## Reference 這邊是報告的 Reference * [[ Laravel ] 初心者之路#01 – Laravel 介紹](https://dometi.com.tw/blog/laravel-beginner-01/) * [OAuth 2.0 筆記 (1) 世界觀](https://blog.yorkxin.org/2013/09/30/oauth2-1-introduction.html) 這裡是實做用到的 Reference * [Laravel 與 MVC](https://hackmd.mcl.math.ncu.edu.tw/8gACMzjpRfKPAy4tui_Utg) * [OAuth 2.0 and OpenID Connect Implementation in Laravel (Authlete)](https://medium.com/@darutk/oauth-2-0-and-openid-connect-implementation-in-laravel-authlete-4d32802ab335) * [2019 Spring MCL 文件](https://docs.mcl.math.ncu.edu.tw/books/weekly-meeting/chapter/2019-spring) * [Laravel 官方文件](https://laravel.com/docs/6.x/) * [Laravel 學習筆記 - 登入驗證 (Authentication)](http://blog.tonycube.com/2015/01/laravel-19-authentication.html) * [Create Laravel Search Box With Live Results Using AJAX jQuery](https://www.cloudways.com/blog/live-search-laravel-ajax/) * [laravel - ajax](https://www.yiibai.com/laravel/laravel_ajax.html) * [daterangepicker 控制](https://codertw.com/%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC/228725/) * [Live search in javascript](https://github.com/GurudayalKhalsa/Live-Search) * [Google+ OAuth 第三方登入](https://blog.scottchayaa.com/post/2018/11/14/google-oauth-tutorial/) * [OAuth 2.0 筆記 (1) 世界觀](https://blog.yorkxin.org/2013/09/30/oauth2-1-introduction.html) * [30天快速上手 Laravel](https://ithelp.ithome.com.tw/users/20112515/ironman/2041?page=2) * [Laravel 5.8 Google Socialite Authentication](https://medium.com/@confidenceiyke/laravel-5-8-google-socialite-authentication-a8b57aa59241) * [Laravel Socialite Custom Providers](https://medium.com/laravel-news/adding-auth-providers-to-laravel-socialite-ca0335929e42)