1、首先需要建表
- 原因是如果client_id保存在數據庫中,而且不自定義查詢邏輯,就需要使用Oauth2為我們提供的表
- 框架已提前為我們設計好了數據庫表,但對於 MYSQL 來說,默認建表語句中主鍵為 Varchar(256),這超過了最大的主鍵長度,可改成 128,並用 BLOB 替換語句中的 LONGVARBINARY 類型
- https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
-- used in tests that use HSQL create table oauth_client_details ( client_id VARCHAR(256) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(256) ); create table oauth_client_token ( token_id VARCHAR(256), token LONGVARBINARY, authentication_id VARCHAR(256) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256) ); create table oauth_access_token ( token_id VARCHAR(256), token LONGVARBINARY, authentication_id VARCHAR(256) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256), authentication LONGVARBINARY, refresh_token VARCHAR(256) ); create table oauth_refresh_token ( token_id VARCHAR(256), token LONGVARBINARY, authentication LONGVARBINARY ); create table oauth_code ( code VARCHAR(256), authentication LONGVARBINARY ); create table oauth_approvals ( userId VARCHAR(256), clientId VARCHAR(256), scope VARCHAR(256), status VARCHAR(10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP ); -- customized oauth_client_details table create table ClientDetails ( appId VARCHAR(256) PRIMARY KEY, resourceIds VARCHAR(256), appSecret VARCHAR(256), scope VARCHAR(256), grantTypes VARCHAR(256), redirectUrl VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additionalInformation VARCHAR(4096), autoApproveScopes VARCHAR(256) );
授權服務和認證服務
在沒有jwt令牌之前,授權服務也充當認證服務,資源服務器獲取到token,會請求授權/認證服務。來校驗令牌合法性。但是又了jwt令牌之后,資源服務器自己就有能力校驗令牌。所以授權服務就不在充當認證服務了。
Oauth2授權碼模式
1、請求授權(認證)服務,申請授權碼
- oauth/authorize:框架內部url,我們需要根據Oath2協議,傳入相應的參數
- redirect_uri:跳轉uri,當授權碼申請成功后會跳轉到此地址,並在后邊帶上code參數(授權碼)。
GET:localhost:40400/oauth/authorize?client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost
- 如果我們配置的數據庫(oauth_client_details)或者內存中存在XcWebApp這個client_id,就需要用戶來輸入用戶名和密碼驗證身份。當然可以配置二維碼,快捷登錄,成功后點擊
- 密碼和賬號輸入成功后會帶着授權碼重定向到指定的uri下
2、申請令牌
- 拿到授權碼后,Post請求申請令牌。
- 參數:grant_type:授權類型,填寫authorization_code,表示授權碼模式,code:授權碼,就是剛剛獲取的授權碼,注意:授權碼只使用一次就無效了,需要重新申請。redirect_uri:申請授權碼時的跳轉url,一定和申請授權碼時用的redirect_uri一致。
POST:http://localhost:40400/auth/oauth/token
- 此鏈接需要使用 http Basic認證。傳入的是client_id和密碼
Oauth2密碼模式授權
輸入用戶名和密碼申請令牌
密碼模式(Resource Owner Password Credentials)與授權碼模式的區別是申請令牌不再使用授權碼,而是直接通過用戶名和密碼即可申請令牌。
- 需要傳入clientId、clientSecret、username、password、grant_type
- 由於基於http basic認證,clientId、clientSecret就是下面的username/password
- grant_type:密碼模式授權填寫 password
POST:http://localhost:40400/auth/oauth/token
- 並且此鏈接需要使用 http Basic認證。
注意:當令牌沒有過期時同一個用戶再次申請令牌則不再頒發新令牌。
ps:校驗令牌
Get: http://localhost:40400/auth/oauth/check_token?token=
返回結果
{ "companyId": null, "userpic": null, "user_name": "mrt", "scope": [ "app" ], "name": null, "utype": null, "id": null, "exp": 1531254828, "jti": "6a00f227‐4c30‐47dc‐a959‐c0c147806462", "client_id": "XcWebApp" }
exp:過期時間,long類型,距離1970年的秒數(new Date().getTime()可得到當前時間距離1970年的毫秒數)。
user_name: 用戶名
client_id:客戶端Id,在oauth_client_details中配置
scope:客戶端范圍,在oauth_client_details表中配置
jti:與令牌對應的唯一標識
companyId、userpic、name、utype、id:這些字段是本認證服務在Spring Security基礎上擴展的用戶身份信息
ps:刷新令牌
刷新令牌是當令牌快過期時重新生成一個令牌,它於授權碼授權和密碼授權生成令牌不同,刷新令牌不需要授權碼,也不需要賬號和密碼,只需要一個刷新令牌、客戶端id和客戶端密碼。
測試如下:
Post:http://localhost:40400/auth/oauth/token
參數:
grant_type: 固定為 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)
刷新令牌成功,會重新生成新的訪問令牌和刷新令牌,令牌的有效期也比舊令牌長。刷新令牌通常是在令牌快過期時進行刷新。
請求資源
- 方式1:在headers頭中,加入key:Authorization,value:Bearer [access_token]
- 方式2:在params中,加入key:access_token,value:[access_token]
//是否可以不需要登錄,訪問url HttpSecurity http //必須登錄+必須有權限 @PreAuthorize("hasAnyAuthority('course_get_baseinfo')")
- 如果采用jwt令牌,資源服務自己校驗令牌,如果是普通令牌,資源服務發送http請求,調用認證服務進行驗證
解決swagger-ui無法訪問
修改授權配置類ResourceServerConfig的configure方法: 針對swagger-ui的請求路徑進行放行:
//Http安全配置,對每個到達系統的http請求鏈接進行校驗 @Override public void configure(HttpSecurity http) throws Exception { //所有請求必須認證通過 http.authorizeRequests() //下邊的路徑放行 .antMatchers("/v2/api‐docs", "/swagger‐resources/configuration/ui", "/swagger‐resources","/swagger‐resources/configuration/security", "/swagger‐ui.html","/webjars/**").permitAll() .anyRequest().authenticated(); }
注意: 通過上邊的配置雖然可以訪問swagger-ui,但是無法進行單元測試,除非去掉認證的配置或在上邊配置中添加所有請求均放行("/**")。
Oauth自帶url
TokenEndpoint類處理 @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
JWT令牌資源服務授權配置
1、配置公鑰
認證服務生成令牌采用非對稱加密算法,認證服務采用私鑰加密生成令牌,對外向資源服務提供公鑰,資源服務使 用公鑰 來校驗令牌的合法性。
將公鑰拷貝到 publickey.txt文件中,將此文件拷貝到資源服務工程的classpath下

2、添加依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring‐cloud‐starter‐oauth2</artifactId> </dependency>
4、在config包下創建ResourceServerConfig類:
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //公鑰 private static final String PUBLIC_KEY = "publickey.txt"; //定義JwtTokenStore ,使用jwt令牌 @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } //定義JJwtAccessTokenConverter,使用jwt令牌 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } /** * 獲取非對稱加密公鑰 Key * @return 公鑰 Key */ private String getPubKey() { Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining("\n")); } catch (IOException ioe) { return null; } } //Http安全配置,對每個到達系統的http請求鏈接進行校驗 @Override public void configure(HttpSecurity http) throws Exception { //所有請求必須認證通過http.authorizeRequests().anyRequest().authenticated(); } }
資源服務授權測試
請求時沒有攜帶令牌則報錯:
{ "error": "unauthorized", "error_description": "Full authentication is required to access this resource" }
使用:在http header中添加 Authorization: Bearer 令牌(固定格式)

JWT研究
傳統校驗令牌的方法
傳統授權方法的問題是用戶每次請求資源服務,資源服務都需要攜帶令牌訪問認證服務去校驗令牌的合法性,並根據令牌獲取用戶的相關信息,性能低下。
使用JWT
思路是,用戶認證通過會得到一個JWT令牌,JWT令牌中已經包括了用戶相關的信息,客戶端只需要攜帶JWT訪問資源服務,資源服務根據事先約定的算法自行完成令牌校驗,無需每次都請求認證服務完成授權。
JSON Web Token(JWT)是一個開放的行業標准(RFC 7519),它定義了一種簡介的、自包含的協議格式,用於 在通信雙方傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT可以使用HMAC算法或使用RSA的公 鑰/私鑰對來簽名,防止被篡改
JWT令牌的優點:
1、jwt基於json,非常方便解析。2、可以在令牌中自定義豐富的內容,易擴展。3、通過非對稱加密算法及數字簽名技術,JWT防止篡改,安全性高。4、資源服務使用JWT可不依賴認證服務即可完成授權。
缺點:
1、JWT令牌較長,占存儲空間比較大。
JWT令牌結構
JWT令牌由三部分組成,每部分中間使用點(.)分隔,比如:xxxxx.yyyyy.zzzzz
- Header
頭部包括令牌的類型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA) 一個例子如下:下邊是Header部分的內容
{ "alg": "HS256", "typ": "JWT" }
將上邊的內容使用Base64Url編碼,得到一個字符串就是JWT令牌的第一部分。
- Payload
第二部分是負載,內容也是一個json對象,它是存放有效信息的地方,它可以存放jwt提供的現成字段,比 如:iss(簽發者),exp(過期時間戳), sub(面向的用戶)等,也可自定義字段。
此部分不建議存放敏感信息,因為此部分可以解碼還原原始內容。 最后將第二部分負載使用Base64Url編碼,得到一個字符串就是JWT令牌的第二部分。 一個例子:
{ "sub": "1234567890", "name": "456", "admin": true }
- Signature
第三部分是簽名,此部分用於防止jwt內容被篡改。這個部分使用base64url將前兩部分進行編碼,編碼后使用點(.)連接組成字符串,最后使用header中聲明 簽名算法進行簽名。
一個例子:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
base64UrlEncode(header):jwt令牌的第一部分。base64UrlEncode(payload):jwt令牌的第二部分。secret:簽名所使用的密鑰。
普通令牌只是唯一標識了用戶信息,資源服務只能通過請求認證服務來獲取用戶信息,而jwt令牌已經存儲了用戶信息,只要資源服務解析這個令牌就可以拿到用戶信息
JWT入門
Spring Security 提供對JWT的支持,本節我們使用Spring Security 提供的JwtHelper來創建JWT令牌,校驗JWT令牌等操作。
生成私鑰和公鑰
JWT令牌生成采用非對稱加密算法
1、生成密鑰證書
下邊命令生成密鑰證書,采用RSA 算法每個證書包含公鑰和私鑰
keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystore
Keytool 是一個java提供的證書管理工具
-alias:密鑰的別名
-keyalg:使用的hash算法
-keypass:密鑰的訪問密碼
-keystore:密鑰庫文件名,xc.keystore保存了生成的證書
-storepass:密鑰庫的訪問密碼
查詢證書信息:
keytool -list -keystore xc.keystore
刪除別名
keytool -delete -alias xckey -keystore xc.keystore
2、導出公鑰
openssl是一個加解密工具包,這里使用openssl來導出公鑰信息。 安裝 openssl:http://slproweb.com/products/Win32OpenSSL.html,配置openssl的path環境變量(mac自帶)
cmd進入xc.keystore文件所在目錄執行如下命令
keytool ‐list ‐rfc ‐‐keystore xc.keystore | openssl x509 ‐inform pem ‐pubkey
將下面的紅色框文字復制成一行,保存一個文件(publickey.txt),這個文件用於放到資源服務中
生成jwt令牌
在認證工程創建測試類,測試jwt令牌的生成與驗證。
//生成一個jwt令牌 @Test public void testCreateJwt(){ //證書文件 String key_location = "xc.keystore"; //密鑰庫密碼 String keystore_password = "xuechengkeystore"; //訪問證書路徑 ClassPathResource resource = new ClassPathResource(key_location); //密鑰工廠 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystore_password.toCharArray()); //密鑰的密碼,此密碼和別名要匹配 String keypassword = "xuecheng"; //密鑰別名 String alias = "xckey"; //密鑰對(密鑰和公鑰) KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypassword.toCharArray()); //私鑰 RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate(); //定義payload信息 Map<String, Object> tokenMap = new HashMap<>(); tokenMap.put("id", "123"); tokenMap.put("name", "mrt"); tokenMap.put("roles", "r01,r02"); tokenMap.put("ext", "1"); //生成jwt令牌 Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(aPrivate)); //取出jwt令牌 String token = jwt.getEncoded(); System.out.println("token="+token); }
驗證jwt令牌
//資源服務使用公鑰驗證jwt的合法性,並對jwt解碼
@Test public void testVerify(){
//jwt令牌(由上面生成) String token ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHQiOiIxIiwicm9sZXMiOiJyMDEscjAyIiwibmFtZSI6Im1ydCIsI mlkIjoiMTIzIn0.KK7_67N5d1Dthd1PgDHMsbi0UlmjGRcm_XJUUwseJ2eZyJJWoPP2IcEZgAU3tUaaKEHUf9wSRwaDgwhrw fyIcSHbs8oy3zOQEL8j5AOjzBBs7vnRmB7DbSaQD7eJiQVJOXO1QpdmEFgjhc_IBCVTJCVWgZw60IEW1_Lg5tqaLvCiIl26K 48pJB5f‐le2zgYMzqR1L2LyTFkq39rG57VOqqSCi3dapsZQd4ctq95SJCXgGdrUDWtD52rp5o6_0uq‐ mrbRdRxkrQfsa1j8C5IW2‐T4eUmiN3f9wF9JxUK1__XC1OQkOn‐ZTBCdqwWIygDFbU7sf6KzfHJTm5vfjp6NIA"; //公鑰 String publickey = "‐‐‐‐‐BEGIN PUBLIC KEY‐‐‐‐‐ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijyxMdq4S6L1Af1rtB8SjCZHNgsQG8JTfGy55eYvzG0B/E4AudR2 prSRBvF7NYPL47scRCNPgLnvbQczBHbBug6uOr78qnWsYxHlW6Aa5dI5NsmOD4DLtSw8eX0hFyK5Fj6ScYOSFBz9cd1nNTvx 2+oIv0lJDcpQdQhsfgsEr1ntvWterZt/8r7xNN83gHYuZ6TM5MYvjQNBc5qC7Krs9wM7UoQuL+s0X6RlOib7/mcLn/lFLsLD dYQAZkSDx/6+t+1oHdMarChIPYT1sx9Dwj2j2mvFNDTKKKKAq0cv14Vrhz67Vjmz2yMJePDqUi0JYS2r0iIo7n8vN7s83v5u OQIDAQAB‐‐‐‐‐END PUBLIC KEY‐‐‐‐‐"; //校驗jwt(如果jwt令牌錯誤,執行下面的代碼會報錯) Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey)); //獲取jwt原始內容 String claims = jwt.getClaims(); //jwt令牌 String encoded = jwt.getEncoded(); System.out.println(encoded); }
認證接口開發
執行流程:
1、用戶登錄,請求認證服務
2、認證服務認證通過,生成jwt令牌,將jwt令牌及相關信息寫入Redis,並且將身份令牌寫入cookie
3、用戶訪問資源頁面,帶着cookie到網關
4、網關從cookie獲取token,並查詢Redis校驗token,如果token不存在則拒絕訪問,否則放行
5、用戶退出,請求認證服務,清除redis中的token,並且刪除cookie中的token使用redis存儲用戶的身份令牌有以下作用:
1、實現用戶退出注銷功能,服務端清除令牌后,即使客戶端請求攜帶token也是無效的。
2、由於jwt令牌過長,不宜存儲在cookie中,所以將jwt令牌存儲在redis,由客戶端請求服務端獲取並在客戶端存儲。
spring security 認證流程
認證服務
spring security 自動調用UserDetailServiceImpl(它繼承了UserDetailService接口)
用戶認證授權流程
用戶登陸首頁--首頁訪問認證服務--認證服務訪問用戶中心查詢用戶是否存在--如果存在,將jwt令牌(長令牌,包含用戶身份信息)等所有的令牌都存放到了redis,將token(短令牌(jti),用戶身份令牌)存放到cookie返回給用戶
訪問課程管理前端,通過前端訪問用戶中心--首先訪問認證服務接口通過token查詢jwt令牌,將令牌添加到header中--訪問網關,網關通過token在redis中校驗令牌是否有效--有效,請求微服務(用戶中心),用戶中心從header中拿到jwt令牌(每一個微服務自己都可以校驗令牌合法性)