㈠ 單點登錄JWT與Spring Security OAuth
通過 JWT 配合 Spring Security OAuth2 使用的方式,可以避免 每次請求 都 遠程調度 認證授權服務。 資源伺服器 只需要從 授權伺服器 驗證一次,返回 JWT。返回的 JWT 包含了 用戶 的所有信息,包括 許可權信息 。
1. 什麼是JWT
jsON Web Token(JWT)是一種開放的標准(RFC 7519),JWT 定義了一種 緊湊 且 自包含 的標准,旨在將各個主體的信息包裝為 JSON 對象。 主體信息 是通過 數字簽名 進行 加密 和 驗證 的。經常使用 HMAC 演算法或 RSA( 公鑰 / 私鑰 的 非對稱性加密 )演算法對 JWT 進行簽名, 安全性很高 。
2. JWT的結構
JWT 的結構由三部分組成:Header(頭)、Payload(有效負荷)和 Signature(簽名)。因此 JWT 通常的格式是 xxxxx.yyyyy.zzzzz。
2.1. Header
Header 通常是由 兩部分 組成:令牌的 類型 (即 JWT)和使用的 演算法類型 ,如 HMAC、SHA256 和 RSA。例如:
將 Header 用 Base64 編碼作為 JWT 的 第一部分 ,不建議在 JWT 的 Header 中放置 敏感信息 。
2.2. Payload
下面是 Payload 部分的一個示例:
將 Payload 用 Base64 編碼作為 JWT 的 第二部分 ,不建議在 JWT 的 Payload 中放置 敏感信息 。
2.3. Signature
要創建簽名部分,需要利用 秘鑰 對 Base64 編碼後的 Header 和 Payload 進行 加密 ,加密演算法的公式如下:
簽名 可以用於驗證 消息 在 傳遞過程 中有沒有被更改。對於使用 私鑰簽名 的 token,它還可以驗證 JWT 的 發送方 是否為它所稱的 發送方 。
3. JWT的工作方式
客戶端 獲取 JWT 後,對於以後的 每次請求 ,都不需要再通過 授權服務 來判斷該請求的 用戶 以及該 用戶的許可權 。在微服務系統中,可以利用 JWT 實現 單點登錄 。認證流程圖如下:
4. 案例工程結構
工程原理示意圖如下:
5. 構建auth-service授權服務
UserServiceDetail.java
UserRepository.java
實體類 User 和上一篇文章的內容一樣,需要實現 UserDetails 介面,實體類 Role 需要實現 GrantedAuthority 介面。
User.java
Role.java
jks 文件的生成需要使用 Java keytool 工具,保證 Java 環境變數沒問題,輸入命令如下:
其中,-alias 選項為 別名 ,-keyalg 為 加密演算法 ,-keypass 和 -storepass 為 密碼選項 ,-keystore 為 jks 的 文件名稱 ,-validity 為配置 jks 文件 過期時間 (單位:天)。
生成的 jks 文件作為 私鑰 ,只允許 授權服務 所持有,用作 加密生成 JWT。把生成的 jks 文件放到 auth-service 模塊的 src/main/resource 目錄下即可。
對於 user-service 這樣的 資源服務 ,需要使用 jks 的 公鑰 對 JWT 進行 解密 。獲取 jks 文件的 公鑰 的命令如下:
這個命令要求安裝 openSSL 下載地址,然後手動把安裝的 openssl.exe 所在目錄配置到 環境變數 。
輸入密碼 fzp123 後,顯示的信息很多,只需要提取 PUBLIC KEY,即如下所示:
新建一個 public.cert 文件,將上面的 公鑰信息 復制到 public.cert 文件中並保存。並將文件放到 user-service 等 資源服務 的 src/main/resources 目錄下。至此 auth-service 搭建完畢。
maven 在項目編譯時,可能會將 jks 文件 編譯 ,導致 jks 文件 亂碼 ,最後不可用。需要在 pom.xml 文件中添加以下內容:
6. 構建user-service資源服務
注入 JwtTokenStore 類型的 Bean,同時初始化 JWT 轉換器 JwtAccessTokenConverter,設置用於解密 JWT 的 公鑰 。
配置 資源服務 的認證管理,除了 注冊 和 登錄 的介面之外,其他的介面都需要 認證 。
新建一個配置類 GlobalMethodSecurityConfig,通過 @EnableGlobalMethodSecurity 註解開啟 方法級別 的 安全驗證 。
拷貝 auth-service 模塊的 User、Role 和 UserRepository 三個類到本模塊。在 Service 層的 UserService 編寫一個 插入用戶 的方法,代碼如下:
配置用於用戶密碼 加密 的工具類 BPwdEncoderUtil:
實現一個 用戶注冊 的 API 介面 /user/register,代碼如下:
在 Service 層的 UserServiceDetail 中添加一個 login() 方法,代碼如下:
AuthServiceClient 作為 Feign Client,通過向 auth-service 服務介面 /oauth/token 遠程調用獲取 JWT。在請求 /oauth/token 的 API 介面中,需要在 請求頭 傳入 Authorization 信息, 認證類型 ( grant_type )、用戶名 ( username ) 和 密碼 ( password ),代碼如下:
其中,AuthServiceHystrix 為 AuthServiceClient 的 熔斷器 ,代碼如下:
JWT 包含了 access_token、token_type 和 refresh_token 等信息,代碼如下:
UserLoginDTO 包含了一個 User 和一個 JWT 成員屬性,用於返回數據的實體:
登錄異常類 UserLoginException
全局異常處理 切面類 ExceptionHandle
在 Web 層的 UserController 類中新增一個登錄的 API 介面 /user/login 如下:
依次啟動 eureka-service,auth-service 和 user-service 三個服務。
7. 使用Postman測試
因為沒有許可權,訪問被拒絕。在資料庫手動添加 ROLE_ADMIN 許可權,並與該用戶關聯。重新登錄並獲取 JWT,再次請求 /user/foo 介面。
在本案例中,用戶通過 登錄介面 來獲取 授權服務 加密後的 JWT。用戶成功獲取 JWT 後,在以後每次訪問 資源服務 的請求中,都需要攜帶上 JWT。 資源服務 通過 公鑰解密 JWT, 解密成功 後可以獲取 用戶信息 和 許可權信息 ,從而判斷該 JWT 所對應的 用戶 是誰,具有什麼 許可權 。
獲取一次 Token,多次使用, 資源服務 不再每次訪問 授權服務 該 Token 所對應的 用戶信息 和用戶的 許可權信息 。
一旦 用戶信息 或者 許可權信息 發生了改變,Token 中存儲的相關信息並 沒有改變 ,需要 重新登錄 獲取新的 Token。就算重新獲取了 Token,如果原來的 Token 沒有過期,仍然是可以使用的。一種改進方式是在登錄成功後,將獲取的 Token 緩存 在 網關上 。如果用戶的 許可權更改 ,將 網關 上緩存的 Token 刪除 。當請求經過 網關 ,判斷請求的 Token 在 緩存 中是否存在,如果緩存中不存在該 Token,則提示用戶 重新登錄 。
㈡ spring提供的幾種密碼加密方式
第一種:不使用任何加密方式的配置
[html]view plain
<beanid="AuthenticationProvider"
class="org.acegisecurity.providers..DaoAuthenticationProvider">
<propertyname="userDetailsService"ref="userDetailsService"/>
<!--明文加密,不使用任何加密演算法,在不指定該配置的情況下,Acegi默認採用的就是明文加密-->
<!--<propertyname="passwordEncoder"><beanclass="org.acegisecurity.providers.encoding.PlaintextPasswordEncoder">
<propertyname="ignorePasswordCase"value="true"></property></bean></property>-->
</bean>
第二種:MD5方式加密
[html]view plain
<beanid="AuthenticationProvider"class="org.acegisecurity.providers..DaoAuthenticationProvider">
<propertyname="userDetailsService"ref="userDetailsService"/>
<propertyname="passwordEncoder">
<beanclass="org.acegisecurity.providers.encoding.Md5PasswordEncoder">
<!--false表示:生成32位的Hex版,這也是encodeHashAsBase64的,Acegi默認配置;true表示:生成24位的Base64版-->
<propertyname="encodeHashAsBase64"value="false"/>
</bean>
</property>
</bean>
第三種:使用MD5加密,並添加全局加密鹽
Java代碼
[html]view plain
<beanid="AuthenticationProvider"class="org.acegisecurity.providers..DaoAuthenticationProvider">
<propertyname="userDetailsService"ref="userDetailsService"/>
<propertyname="passwordEncoder">
<beanclass="org.acegisecurity.providers.encoding.Md5PasswordEncoder">
<propertyname="encodeHashAsBase64"value="false"/>
</bean>
</property>
<!--對密碼加密演算法中使用特定的加密鹽及種子-->
<propertyname="saltSource">
<beanclass="org.acegisecurity.providers..salt.SystemWideSaltSource">
<propertyname="systemWideSalt"value="acegisalt"/>
</bean>
</property>
</bean>
第四種:使用MD5加密,並添加動態加密鹽
[html]view plain
<beanid="AuthenticationProvider"class="org.acegisecurity.providers..DaoAuthenticationProvider">
<propertyname="userDetailsService"ref="userDetailsService"/>
<propertyname="passwordEncoder">
<beanclass="org.acegisecurity.providers.encoding.Md5PasswordEncoder">
<propertyname="encodeHashAsBase64"value="false"/>
</bean>
</property>
<!--對密碼加密演算法中使用特定的加密鹽及種子-->
<propertyname="saltSource">
<!--通過動態的加密鹽進行加密,該配置通過用戶名提供加密鹽,通過UserDetails的getUsername()方式-->
<beanclass="org.acegisecurity.providers..salt.ReflectionSaltSource">
<propertyname="userPropertyToUse"value="getUsername"/>
</bean>
</property>
</bean>
第五種:使用哈希演算法加密,加密強度為256
[html]view plain
<beanid="AuthenticationProvider"class="org.acegisecurity.providers..DaoAuthenticationProvider">
<propertyname="userDetailsService"ref="userDetailsService"/>
<propertyname="passwordEncoder">
<beanclass="org.acegisecurity.providers.encoding.ShaPasswordEncoder">
<constructor-argvalue="256"/>
<propertyname="encodeHashAsBase64"value="false"/>
</bean>
</property>
</bean>
第六種:使用哈希演算法加密,加密強度為SHA-256
[html]view plain
<beanid="AuthenticationProvider"class="org.acegisecurity.providers..DaoAuthenticationProvider">
<propertyname="userDetailsService"ref="userDetailsService"/>
<propertyname="passwordEncoder">
<beanclass="org.acegisecurity.providers.encoding.ShaPasswordEncoder">
<constructor-argvalue="SHA-256"/>
<propertyname="encodeHashAsBase64"value="false"/>
</bean>
</property>
</bean>
上述配置只是在Acegi通過表單提交的用戶認證信息中的密碼做各種加密操作。而我們存儲用戶密碼的時候,可以通過一下程序完成用戶密碼操作:
[java]view plain
packageorg.hz.test;
importjava.security.NoSuchAlgorithmException;
importorg.springframework.security.authentication.encoding.Md5PasswordEncoder;
importorg.springframework.security.authentication.encoding.ShaPasswordEncoder;
publicclassMD5Test{
publicstaticvoidmd5(){
Md5PasswordEncodermd5=newMd5PasswordEncoder();
//false表示:生成32位的Hex版,這也是encodeHashAsBase64的,Acegi默認配置;true表示:生成24位的Base64版
md5.setEncodeHashAsBase64(false);
Stringpwd=md5.encodePassword("1234",null);
System.out.println("MD5:"+pwd+"len="+pwd.length());
}
publicstaticvoidsha_256(){
ShaPasswordEncodersha=newShaPasswordEncoder(256);
sha.setEncodeHashAsBase64(true);
Stringpwd=sha.encodePassword("1234",null);
System.out.println("哈希演算法256:"+pwd+"len="+pwd.length());
}
publicstaticvoidsha_SHA_256(){
ShaPasswordEncodersha=newShaPasswordEncoder();
sha.setEncodeHashAsBase64(false);
Stringpwd=sha.encodePassword("1234",null);
System.out.println("哈希演算法SHA-256:"+pwd+"len="+pwd.length());
}
publicstaticvoidmd5_SystemWideSaltSource(){
Md5PasswordEncodermd5=newMd5PasswordEncoder();
md5.setEncodeHashAsBase64(false);
//使用動態加密鹽的只需要在注冊用戶的時候將第二個參數換成用戶名即可
Stringpwd=md5.encodePassword("1234","acegisalt");
System.out.println("MD5SystemWideSaltSource:"+pwd+"len="+pwd.length());
}
publicstaticvoidmain(String[]args){
md5();//使用簡單的MD5加密方式
sha_256();//使用256的哈希演算法(SHA)加密
sha_SHA_256();//使用SHA-256的哈希演算法(SHA)加密
md5_SystemWideSaltSource();//使用MD5再加全局加密鹽加密的方式加密
}
}
㈢ spring+mybatis數據源密碼怎樣加密有沒有必要
不需要加密,可以直接放置在spring配置文件中,也可以定義應用程序伺服器數據源,spring利用jndi數據源
㈣ Spring Boot 業務邏輯層
關於業務邏輯層(Service層)
業務邏輯層是被Controller直接調用的層(Controller不允許直接調用持久層),通常,在業務邏輯層中編寫的代碼是為了 保證數據的完整性和安全性 ,使得數據是隨著我們設定的規則而產生或發生變化。
通常,在業務邏輯層的代碼會由介面和實現類組件,其中, 介面被視為是必須的
關於拋出的異常,通常是自定義的異常,並且, 自定義異常 通常是`RuntimeException`的子類,主要原因:
所以,在實際編寫業務邏輯層之前,應該先規劃異常,例如先創建` ServiceException `類:
接下來,再創建具體的對應某種「失敗」的異常,例如,在添加管理員時,可能因為「用戶名已經存在」而失敗,則創建對應的 `UsernameDuplicateException`異常 :
另外,當插入數據時,如果返回的受影響行數不是1時,必然是某種錯誤,則 創建對應的插入數據異常 :
關於抽象方法的參數,應該設計為客戶端提交的數據類型或對應的封裝類型,不可以是數據表對應的實體類型!如果使用封裝的類型,這種類型在類名上應該添加某種後綴,例如` DTO`或其它後綴 ,例如:
並在以上`service`包下創建`impl`子包,再創建`AdminServiceImpl`類:
以上代碼未實現對密碼的加密處理! 關於密碼加密 ,相關的代碼應該定義在別的某個類中,不應該直接將加密過程編寫在以上代碼中,因為加密的代碼需要在多處應用(添加用戶、用戶登錄、修改密碼等),並且,從分工的角度上來看,也不應該是業務邏輯層的任務!所以,在`cn.celinf.boot.demo.util`(包不存在,則創建)下創建`PasswordEncoder`類,用於處理密碼加密:
完成後,需要在`AdminServiceImpl`中自動裝配以上`PasswordEncoder`,並在需要加密時調用`PasswordEncoder`對象的`encode()`方法。
控制器層開發
Spring MVC是用於處理控制器層開發的,在使用Spring Boot時,在`pom.xml`中添加`spring-boot-starter-web`即可整合Spring MVC框架及相關的常用依賴項(包含`jackson-databind`),可以將已存在的`spring-boot-starter`直接改為`spring-boot-starter-web`,因為在`spring-boot-starter-web`中已經包含了`spring-boot-starter`。
先在項目的根包下創建`controller`子包,並在此子包下創建`AdminController`,此類應該添加 `@RestController` 和` @RequestMapping (value = "/admins", proces = "application/json; charset=utf-8")`註解,例如:
由於已經決定了伺服器端響應時,將響應JSON格式的字元串,為保證能夠響應JSON格式的結果,處理請求的方法返回值應該是自定義的數據類型,則從此前學習的`spring-mvc`項目中找到`JsonResult`類及相關類型,復制到當前項目中來。
完成後,運行啟動類,即可啟動整個項目,在`spring-boot-starter-web`中,包含了Tomcat的依賴項,在啟動時,會自動將當前項目打包並部署到此Tomcat上,所以,執行啟動類時,會執行此Tomcat,同時,因為是內置的Tomcat,只為當前項目服務,所以,在將項目部署到Tomcat時,默認已經將Context Path(例如spring_mvc_war_exploded)配置為空字元串,所以,在啟動項目後,訪問的URL中並沒有此前遇到的Context Path值。
當項目啟動成功後,即可在瀏覽器的地址欄中輸入網址進行測試訪問!
【注意】 :如果是未添加的管理員賬號,可以成功執行結束,如果管理員賬號已經存在,由於尚未處理異常,會提示500錯誤。
然後,在`cn.celinf.boot.demo.controller`下創建`handler.GlobalExceptionHandler`類,用於統一處理異常,例如:
完成後,重新啟動項目,當添加管理員時的用戶名沒有被佔用時,將正常添加,當用戶名已經被佔用時,會根據處理異常的結果進行響應!
由於在 統一處理異常的機制下 ,同一種異常,無論是在哪種業務中出現,處理異常時的描述信息都是完全相同的,也無法精準的表達錯誤信息,這是不合適的!另外,基於面向對象的「分工」思想,關於錯誤信息(異常對應的描述信息),應該是由Service來描述,即「誰拋出誰描述」,因為拋出異常的代碼片段是最了解、最明確出現異常的原因的!
為了更好的描述異常的原因,應該在自定義的`ServiceException`和其子孫類異常中添加基於父類的全部構造方法(5個),然後,在`AdminServiceImpl`中,當拋出異常時,可以在異常的構造方法中添加`String`類型的參數,對異常發生的原因進行描述,例如:
最後,在處理異常時,可以 調用異常對象的`getMessage()`方法獲取拋出時封裝的描述信息 ,例如:
完成後,再次重啟項目,當用戶名已經存在時,可以顯示在Service中描述的錯誤信息!
可以看到,無論是成功還是失敗, 響應的JSON中都包含了不必要的數據 (為`null`的數據),這些數據屬性是沒有必要響應到客戶端的,如果需要去除這些不必要的值,可以在對應的屬性上使用註解進行配置,例如:
此註解還可以添加在類上,則作用於當前類中所有的屬性,例如:
即使添加在類上,也只對當前類的3個屬性有效,後續,當響應某些數據時,`data`屬性可能是用戶、商品、訂單等類型,這些類型的 數據中為`null`的部分依然會被響應到客戶端 去,所以,還需要對這些類型也添加相同的註解配置!
以上做法相對比較繁瑣,可以在`application.properties` / `application.yml`中添加全局配置,則作用於當前項目中所有響應時涉及的類,例如在`properties`中配置為:
注意:當你需要在`yml`中添加以上配置時,前綴屬性名可能已經存在,則不允許出現重復的前綴屬性名的:
最後,以上配置只是「默認」配置,如果在某些類型中還有不同的配置需求,仍可以在類或屬性上通過 `@JsonInclude`進行配置 。
15. 解決跨域問題
在使用前後端分離的開發模式下,前端項目和後端項目可能是2個完全不同的項目,並且,各自己獨立開發,獨立部署,在這種做法中,如果前端直接向後端發送非同步請求,默認情況下,在前端會出現類似以下錯誤:
以上錯誤信息的關鍵字是`CORS`,通常稱之為 「跨域問題」 。
在基於Spring MVC框架的項目中,當需要解決跨域問題時,需要一個Spring MVC的配置類(實現了`WebMvcConfigurer`介面的類),並重寫其中的方法,以允許指定條件的跨域訪問,例如:
16. 關於客戶端提交請求參數的格式
通常,客戶端向伺服器端發送請求時,請求參數可以有2種形式,第1種是直接通過`&`拼接各參數與值,例如:
具體使用哪種做法,取決於伺服器端的設計:
- 如果伺服器端處理請求的方法中,在參數前添加了`@RequestBody`,則允許使用以上第2種做法(JSON數據)提交請求參數,不允許使用以上第1種做法(使用`&`拼接)
- 如果沒有使用 `@RequestBody`,則只能使用以上第1種做法