fyunli / ps-authservice

Use JWT & OAuth2 Create AuthorizationServer (spring-security)

Home Page:http://samchu.logdown.com/posts/1433379

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

使用 JWT 跟 OAuth2 來實作授權系統(spring-security)

什麼是 JWT ?

JWT 介紹網路有很多

什麼是 OAuth2 ?

這邊有很詳細的 Oauth 說明 OAuth 2.0 筆記 (1) 世界觀 但目前我們不會全部都用到 目前只用以下兩個 可以參考看看

OAuth 中的角色定義

  • Resource Owner - 可以授權別人去存取 Protected Resource 。如果這個角色是人類的話,則就是指使用者 (end-user)。
  • Resource Server - 存放 Protected Resource 的伺服器,可以根據 Access Token 來接受 Protected Resource 的請求。
  • Client - 代表 Resource Owner 去存取 Protected Resource 的應用程式。 “Client” 一詞並不指任何特定的實作方式(可以在 Server 上面跑、在一般電腦上跑、或是在其他的設備)。
  • Authorization Server - 在認證過 Resource Own

Implicit Grant Flow

是你常見的像 FB 那樣,當別人的問券或是網站要用的你資料,則會回到 FB 取得授權後才能繼續玩 Implicit Grant Flow 關於 Implicit Grant Flow 注意幾點

  • Authorization Server 直接向 Client 核發 Access Token (一步)。
  • 適合非常特定的 Public Clients ,例如跑在 Browser 裡面的應用程式。
  • Authorization Server 不必(也無法)驗證 Client 的身份。
  • 禁止核發 Refresh Token。

Resource Owner Credentials Grant Flow

是比較會偏內部可信任的應用在取得授權,因為會經手用戶的帳號密碼 Resource Owner Credentials Grant Flow 關於 Resource Owner Credentials Grant Flow 注意幾點

  • Resource Owner 的帳號密碼直接拿來當做 Grant。
  • 適用於 Resource Owner 高度信賴的 Client (像是 OS 內建的)或是官方應用程式。
  • 其他流程不適用時才能用。
  • 可以核發 Refresh Token。
  • 沒有 User-Agent Redirection。

實做一個用戶管理

資料庫表格

  • 請參考 initalize\schema.sql
  • 初始化數據 initalize\import.sql

會建立一個用戶 admin 密碼為 123456

新增加的資料表可以控制 scop 跟 client schema

OAuth 流程

其實 Spring Security 有個預設的流程 org.springframework.security.oauth2.provider.token.DefaultTokenService 可以去看看 我們現在要客製化自己的流程,讓 AuthService 可以依照用戶實際關聯的權限給予 scop 實作請參考 com.ps.security.CustomTokenServices

實作 TokenStore

Spring Security 預設的 org.springframework.security.oauth2.provider.token.store.JdbcTokenStore 管理方式是 Single sign-on 也就是會踢掉前一次登入的 Token ,但是這並不符合我們要的 當你是登入的時候,會依照上面 DefaultTokenServices 的流程跑這幾個方法

getAccessToken >> storeAccessToken >> storeRefreshToken

當你是 Refresh Token 的時候會依序執行以下方法

eadRefreshToken >> readAuthenticationForRefreshToken >> removeAccessTokenUsingRefreshToken >> storeAccessToken

所以我們實作以上幾個動作就可以了 請參考 ps-authservice\src\main\java\com\ps\security\CustomTokenStore.java

實作 UserDetailsService

介面 UserDetailsService.java

這是介面提供 security 來讀取用戶資料 請參考 ps-authservice\src\main\java\com\ps\security\CustomUserDetailsService.java

繼承 AbstractUserDetailsAuthenticationProvider.java

繼承 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider 這支是在驗證用戶帳密,我們使用 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 來做密碼的儲存 相關範例請參考 Spring BCryptPasswordEncoder BCryptPasswordEncoder 是 spring security 3 推薦的

安全性更多閱讀 在我的印象中,hash+salt已经足够好了。为什么我还要使用BCrypt?

實際程式碼部分在這 ps-authservice\src\main\java\com\ps\security\CustomUserDetailsAuthenticationProvider.java

如果要客製化 AccessTokenConverter

最後 AccessTokenConverter 不一定需要實作 這個是把原本亂數產生 Token 的方式轉成 JWT 格式

而我們這支 ps-authservice\src\main\java\com\ps\security\CustomAccessTokenConverter.java 是跟原本 org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter.java 的一模一樣 只是方便我們想去加些什麼在 JWT 內

如果 JWT 內的 exp 時間直接解開來看起來很怪是沒有問題的喔,因為在轉換過程中有處理過,你用其他套件他也會換算回來的

if (token.getExpiration() != null) {
    response.put(EXP, token.getExpiration().getTime() / 1000);
}

在 WebSecurityConfiguration 註冊元件

請參考 com.ps.security.WebSecurityConfiguration.java 實作

配置 AuthorizationServer 並把我們服務組件組裝起來

請參考 com.ps.security.AuthorizationServerConfiguration.java 實作

怎麼設計 Scope 也許可以參考 https://developers.google.com/identity/protocols/googlescopes Client 其實也可以配置到資料庫中,不過我們還沒對外開放,所以還不需要。 我們配置了兩個客戶端 clientapp 是走 password 可信任的內部服務 web 則是 implicit 外部一次性授權 網頁方式授權 忘記了就回上面看吧

啟動主程式 AuthApplication.java

測試

password Auth

Request

curl --request POST \
  --url http://localhost:8080/oauth/token \
  --header 'authorization: Basic Y2xpZW50YXBwOjEyMzQ1Ng==' \
  --header 'cache-control: no-cache' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'username=papidakos&password=papidakos123&grant_type=password&scope=account%20role'

response

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0ODcyMjIxNDMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiIzMWUzYzdiNi0zY2U4LTQ1YWMtOGU1Mi1lNzU0M2JhZTljMzUiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.tUCo7NUhMCZDz_CMyr9fsVSqwFoHEvkSOfZHAeMEmn8",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiIzMWUzYzdiNi0zY2U4LTQ1YWMtOGU1Mi1lNzU0M2JhZTljMzUiLCJleHAiOjE0ODcyMjIxNDMsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiIxNDVhNjFkNi0wYzczLTQ4YzUtOWE0ZS1kNzNiNzI0MTY4YmYiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.zXdUTCdiXT5pOpjRanRkrGpiIG3p_C4AsiysjIWHtS8",
  "expires_in": 499,
  "scope": "read write",
  "jti": "31e3c7b6-3ce8-45ac-8e52-e7543bae9c35"
}

password refresh

Request

curl --request POST \
  --url http://localhost:8080/oauth/token \
  --header 'authorization: Basic Y2xpZW50YXBwOjEyMzQ1Ng==' \
  --header 'cache-control: no-cache' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --header 'postman-token: f754a47d-f7b7-7ad7-c517-02969addfcbb' \
  --data 'grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI0ZTY5ZmJmZS00ODAzLTQ0YTYtOTBkOC1hOTcwMDY2YjhlZTEiLCJleHAiOjE0ODcyMTQxNTUsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiJkMTM2OTExNS04NTIwLTRlMDctYTUzNS0yNTA3NDM0OTAxZWIiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.WaHrDJa2mgZxjUDZ2WRsB7_bQluF2HkVk0ILct7KZRA'

response

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0ODcyMTQxNjksImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiJjMzNjZGViMi00NjgyLTRkZTEtOWYwYy1kMWUyMGIxNzIyMDYiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.p7n8tOpAr6EKpdV47bo-re-qway2Zz59j0nj-4Fl-48",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiIl0sInVzZXJfbmFtZSI6InBhcGlkYWtvcyIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJjMzNjZGViMi00NjgyLTRkZTEtOWYwYy1kMWUyMGIxNzIyMDYiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiZDEzNjkxMTUtODUyMC00ZTA3LWE1MzUtMjUwNzQzNDkwMWViIiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIn0.HaMmBQY7BRlcvjEHt4CVn4j3G74luN_7ZaqssC1XPlY",
  "expires_in": 499,
  "scope": "read write",
  "jti": "c33cdeb2-4682-4de1-9f0c-d1e20b172206"
}

implicit

使用瀏覽器開啟 http://localhost:8080/oauth/authorize?response_type=token&client_id=web

有點醜沒關係,這是可以客製的

再看一下原始碼這頁面是有擋 跨站請求偽造(Cross-site request forgery)

<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
<h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
<table>
    <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
    <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
    <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
    <input name="_csrf" type="hidden" value="2c8806fa-ee70-44dc-b289-5dbc0df07ed9" />
</table>
</form></body></html>

輸入正確帳密之後後有個授權清單頁面

同意之後就會產生 Token 透過瀏覽器 轉回客戶端設定的 http://www.google.com.tw 網址如下

https://www.google.com.tw/?gws_rd=ssl#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiY29tbW9uIiwiZnJpZW5kIiwidXNlciJdLCJ1c2VyX25hbWUiOiJwYXBpZGFrb3MiLCJzY29wZSI6WyJjb21tb24iLCJ1c2VyLnJlYWRvbmx5IiwiZnJpZW5kIl0sImV4cCI6MTQ4NzIyNzI1MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjcxNjU3ZmNlLTdmNTktNDMwYi1hMjUzLTc5MmNiYzZjZmMyYSIsImNsaWVudF9pZCI6IndlYiJ9.xKktY90aizvAFaR7W1eJzn4NIQLuIaaG88lfTQzSNlQ&token_type=bearer&expires_in=3599&scope=common%20user.readonly%20friend&jti=71657fce-7f59-430b-a253-792cbc6cfc2a

AuthServer 這邊就已經可以用了 想簡單用可以走 implicit 想控制權高一點又可以 refresh 就用 password

Resource Server 則不一定需要套 Spring Security 你也可以簡單使用 Filter 、 LocalThread 、 JWT 套件 就可以達成 那些 x-xss-protection 再自己加上也蠻快的

參考資料

程式碼

https://github.com/samzhu/ps-authservice

About

Use JWT & OAuth2 Create AuthorizationServer (spring-security)

http://samchu.logdown.com/posts/1433379


Languages

Language:Java 100.0%