Spring Security 解析(五) —— Spring Security Oauth2 開發
在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth2 等權限、認證相關的內容、原理及設計學習並整理一遍。本系列文章就是在學習的過程中加強印象和理解所撰寫的,如有侵權請告知。
項目環境:
- JDK1.8
- Spring boot 2.x
- Spring Security 5.x
前面幾篇文章基本上已經把Security的核心內容講得差不多了,那么從本篇文章我們開始接觸Spring Security Oauth2 相關的內容,這其中包括后面的 Spring Social (其本質也是基於Oauth2)。有一點要說明的是,我們是在原有的Spring-Security 項目上繼續開發,存在一些必要的重構,但不影響前面Security的功能。
一、 Oauth2 與 Spring Security Oauth2
Oauth2
有關於Oauth2 的 資料,網上很多,但最值得推薦的還是 阮一峰老師的 理解OAuth 2.0,在這里我就不重復描述Oauth2了,但我還是有必要提下其中的重要的點:
圖片中展示的流程是授權碼模式的流程,其中最核心正如圖片展示的一樣:
- 資源所有者(Resource Owner): 可以理解為用戶
- 服務提供商(Provider): 分為認證服務器(Authorization server)和 資源服務器(Resource server)。怎么理解認證、資源服務器呢,很簡單,比如我們手機某個APP通過QQ來登陸,在我們跳轉到一個QQ授權的頁面以及登陸的操作都是在認證服務器上做的,后面我們登陸成功后能夠看到我們的頭像等信息,這些信息就是登陸成功后去資源服務器獲取到的。
- 第三方應用: 可以理解就是我們正在使用的某個APP,用戶通過這個APP發起QQ授權登陸。
Spring Security Oauth2
Spring 官方出品的 一個實現 Oauth2 協議的技術框架,后面的系列文章其實都是在解析它是如何實現Oauth2的。如果各位有時間的話可以看下Spring Security Oauth2 官方文檔,我的文章分析也是依靠文檔來的。
最后,我個人總結這2者的區別:
- Oauth2 不是一門技術框架, 而是一個協議,它僅僅只是制定好了協議的標准設計思想,你可以用Java實現,也可以用其他任何語言實現。
- Spring Security Oauth2 是一門技術框架,它是依據Oauth2協議開發出來的。
一、 Spring Security Oauth2 開發
在微服務開發的過程中,一般會把授權服務器和資源服務器拆分成2個應用程序,所以本項目采用這種設計結構,不過在開發前,我們需要做一步重要得步驟,就是項目重構。
一、 項目重構
為什么要重構呢?因為我們是將授權和資源2個服務器拆分了,之前開發的一些配置和功能是可以在2個服務器共用的,所以我們可以講公共的配置和功能可以單獨羅列出來,以及后面我們開發Spring Security Oauth2 得一些公共配置(比如Token相關配置)。 我們新建 security-core 子模塊,將之前開發的短信等功能代碼遷移到這個子模塊中。最終得到以下項目結構:
遷移完成后,原先項目模塊更換模塊名為 security-oauth2-authorization ,即 授權服務應用,並且 在pom.xml 中引用 security-core 依賴,遷移后該模塊的項目結構如下:
我們可以發現,遷移后的項目內部只有 Security相關的配置代碼和測試接口,以及靜態的html。
二、 授權服務器開發
一、Maven依賴
在 security-core 模塊的pom.xml 中引用 以下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 不是starter,手動配置 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<!--請注意下 spring-authorization-oauth2 的版本 務必高於 2.3.2.RELEASE,這是官方的一個bug:
java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
要求必須大於2.3.5 版本,官方解釋:https://github.com/BUG9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open
-->
<version>2.3.5.RELEASE</version>
</dependency>
這里新增 spring-security-oauth2 依賴,一個針對token 的存儲策略分別引用了 redis 和 jwt 依賴。
注意這里 spring-security-oauth2 版本必須高於 2.3.5 版本 ,否自使用 redis 存儲token 策略會報出:
org.springframework.data.redis.connection.RedisConnection.set([B[B)V 異常
security-oauth2-authorization 模塊的 pom 引用 security-core :
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 這里去掉 spring-security-oauth2 主要是其內部的版本是 低於2.3.5
(security-core 本身 引用的就是 2.3.5 ,但為什么這邊看到的卻是低於其版本,暫時沒找到原因,可能統一版本管理 platform-bom 的問題吧)
,為了防止出現異常這里去掉,再單獨引用-->
<dependency>
<groupId>com.zhc</groupId>
<artifactId>security-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 不是starter,手動配置 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<!--請注意下 spring-authorization-oauth2 的版本 務必高於 2.3.2.RELEASE,這是官方的一個bug:
java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
要求必須大於2.3.5 版本,官方解釋:https://github.com/BUG9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open
-->
<version>2.3.5.RELEASE</version>
</dependency>
二、配置授權認證 @EnableAuthorizationServer
在Spring Security Oauth2 中有一個 @EnableAuthorizationServer ,只要我們 在項目中引用到了這個注解,那么一個基本的授權服務就配置好了,但是實際項目中並不這樣做。比如要配置redis和jwt 2種存儲token策略共存,通過繼承 AuthorizationServerConfigurerAdapter 來實現。 下列代碼是我的一個個性化配置:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Resource
private AuthenticationManager authenticationManager; // 1、引用 authenticationManager 支持 Password 授權模式
private final Map<String, TokenStore> tokenStoreMap; // 2、獲取到系統所有的 token存儲策略對象 TokenStore ,這里我配置了 redisTokenStore 和 jwtTokenStore
@Autowired(required = false)
private AccessTokenConverter jwtAccessTokenConverter; // 3、 jwt token的增強器
/**
* 4、由於存儲策略時根據配置指定的,當使用redis策略時,tokenEnhancerChain 是沒有被注入的,所以這里設置成 required = false
*/
@Autowired(required = false)
private TokenEnhancerChain tokenEnhancerChain; // 5、token的增強器鏈
@Autowired
private PasswordEncoder passwordEncoder;
@Value("${spring.security.oauth2.storeType}")
private String storeType = "jwt"; // 6、通過獲取配置來判斷當前使用哪種存儲策略,默認jwt
@Autowired
public AuthorizationServerConfiguration(Map<String, TokenStore> tokenStoreMap) {
this.tokenStoreMap = tokenStoreMap;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//7、 配置一個客戶端,支持客戶端模式、密碼模式和授權碼模式
clients.inMemory() // 采用內存方式。也可以采用 數據庫方式
.withClient("client1") // clientId
.authorizedGrantTypes("client_credentials", "password", "authorization_code", "refresh_token") // 授權模式
.scopes("read") // 權限范圍
.redirectUris("http://localhost:8091/login") // 授權碼模式返回code碼的回調地址
// 自動授權,無需人工手動點擊 approve
.autoApprove(true)
.secret(passwordEncoder.encode("123456"))
.and()
.withClient("client2")
.authorizedGrantTypes("client_credentials", "password", "authorization_code", "refresh_token")
.scopes("read")
.redirectUris("http://localhost:8092/login")
.autoApprove(true)
.secret(passwordEncoder.encode("123456"));
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 設置token存儲方式,這里提供redis和jwt
endpoints
.tokenStore(tokenStoreMap.get(storeType + "TokenStore"))
.authenticationManager(authenticationManager);
if ("jwt".equalsIgnoreCase(storeType)) {
endpoints.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(tokenEnhancerChain);
}
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer// 開啟/oauth/token_key驗證端口無權限訪問
.tokenKeyAccess("permitAll()")
// 開啟/oauth/check_token驗證端口認證權限訪問
.checkTokenAccess("isAuthenticated()")
//允許表單認證 請求/oauth/token的,如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的會走ClientCredentialsTokenEndpointFilter
.allowFormAuthenticationForClients();
}
}
這里的配置分3部分:
- ClientDetailsServiceConfigurer: 配置客戶端信息。 可以采用內存方式、JDBC方式等等,我們還可以像UserDetailsService一樣定制ClientDetailsService。
- AuthorizationServerEndpointsConfigurer : 配置 授權節點信息。這里主要配置 tokenStore
- AuthorizationServerSecurityConfigurer: 授權節點的安全配置。 這里開啟/oauth/token_key驗證端口無權限訪問(單點客戶端啟動時會調用該接口獲取jwt的key,所以這里設置成無權限訪問)以及 /oauth/token配置支持allowFormAuthenticationForClients(url中有client_id和client_secret的會走ClientCredentialsTokenEndpointFilter)
三、配置 TokenStore
在配置授權認證時,依賴注入了 tokenStore 、jwtAccessTokenConverter、tokenEnhancerChain,但這些對象是如何配置並注入到Spring 容器的呢?且看下面代碼:
@Configuration
public class TokenStoreConfig {
/**
* redis連接工廠
*/
@Resource
private RedisConnectionFactory redisConnectionFactory;
/**
* 使用redisTokenStore存儲token
*
* @return tokenStore
*/
@Bean
@ConditionalOnProperty(prefix = "spring.security.oauth2", name = "storeType", havingValue = "redis")
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
@Bean
PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* jwt的配置
*
* 使用jwt時的配置,默認生效
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.security.oauth2", name = "storeType", havingValue = "jwt", matchIfMissing = true)
public static class JwtTokenConfig {
@Resource
private SecurityProperties securityProperties;
/**
* 使用jwtTokenStore存儲token
* 這里通過 matchIfMissing = true 設置默認使用 jwtTokenStore
*
* @return tokenStore
*/
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 用於生成jwt
*
* @return JwtAccessTokenConverter
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
//生成簽名的key,這里使用對稱加密
accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey());
return accessTokenConverter;
}
/**
* 用於擴展JWT
*
* @return TokenEnhancer
*/
@Bean
@ConditionalOnMissingBean(name = "jwtTokenEnhancer")
public TokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhance();
}
/**
* 自定義token擴展鏈
*
* @return tokenEnhancerChain
*/
@Bean
public TokenEnhancerChain tokenEnhancerChain() {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(new JwtTokenEnhance(), jwtAccessTokenConverter()));
return tokenEnhancerChain;
}
}
}
注意: 該配置類適用於均適用於授權和資源服務器,所以該配置類是放在 security-core 模塊
四、新增 application.yml 配置
spring:
redis:
host: 127.0.0.1
port: 6379
security:
oauth2:
storeType: redis
jwt:
SigningKey: oauth2
五、啟動測試
1、 授權碼模式:grant_type=authorization_code
(1)瀏覽器上訪問/oauth/authorize 獲取授權碼:
http://localhost:9090/oauth/authorize?response_type=code&client_id=client1&scope=read&state=test&redirect_uri=http://localhost:8091/login
如果是沒有登陸過,則跳轉到登陸界面(這里賬戶密碼登陸和短信驗證碼登陸均可),成功跳轉到 我們設置的回調地址(我們這個是單點登陸客戶端),我們可以從瀏覽器地址欄看到 code碼
(2)Postman請求/oauth/token 獲取token:
localhost:9090/oauth/token?grant_type=authorization_code&code=i4ge7B&redirect_uri=http://localhost:8091/login
注意在 Authorization 填寫 client信息,下面是 curl 請求:
curl -X POST \
'http://localhost:9090/oauth/token?grant_type=authorization_code&code=Q38nnC&redirect_uri=http://localhost:8091/login' \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Authorization: Basic Y2xpZW50MToxMjM0NTY=' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Length: ' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Cookie: remember-me=; JSESSIONID=F6F6DE2968113DDE4613091E998D77F4' \
-H 'Host: localhost:9090' \
-H 'Postman-Token: f37b9921-4efe-44ad-9884-f14e9bd74bce,3c80ffe3-9e1c-4222-a2e1-9694bff3510a' \
-H 'User-Agent: PostmanRuntime/7.16.3' \
-H 'cache-control: no-cache'
響應報文:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiI5MDAxIiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjE1Njg2NDY0NzksImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImY5ZDBhNmZhLTAxOWYtNGU5Ny1iMmI4LWI1OTNlNjBiZjk0NiIsImNsaWVudF9pZCI6ImNsaWVudDEiLCJ1c2VybmFtZSI6IjkwMDEifQ.4BjG_LggZt2RJr0VzXTSmsk71EIUDGvrQsL_OPsg8VA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiI5MDAxIiwic2NvcGUiOlsicmVhZCJdLCJhdGkiOiJmOWQwYTZmYS0wMTlmLTRlOTctYjJiOC1iNTkzZTYwYmY5NDYiLCJleHAiOjE1NzExOTUyNzksImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjU1NTRmYjdkLTBhZGItNGI4MS1iOGNlLWIwOTk2NjM1OTI4MCIsImNsaWVudF9pZCI6ImNsaWVudDEiLCJ1c2VybmFtZSI6IjkwMDEifQ.TA1frc46XRkNgl3Y_n72rM0nZ5QceWH3zJFmR7CkHQ4",
"expires_in": 43199,
"scope": "read",
"username": "9001",
"jti": "f9d0a6fa-019f-4e97-b2b8-b593e60bf946"
}
2、 密碼模式: grant_type=password
Postman:
http://localhost:9090/oauth/token?username=user&password=123456&grant_type=password&scope=read&client_id=client1&client_secret=123456
curl:
curl -X POST \
'http://localhost:9090/oauth/token?username=user&password=123456&grant_type=password&scope=read&client_id=client1&client_secret=123456' \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Length: ' \
-H 'Cookie: remember-me=; JSESSIONID=F6F6DE2968113DDE4613091E998D77F4' \
-H 'Host: localhost:9090' \
-H 'Postman-Token: f41c7e67-1127-4b65-87ed-21b3e00cfae3,08168e2e-1818-42f8-b4c4-cafd4aa0edc4' \
-H 'User-Agent: PostmanRuntime/7.16.3' \
-H 'cache-control: no-cache'
3、 客戶端模式 : grant_type=client_credentials
Postman:
localhost:9090/oauth/token?scope=read&grant_type=client_credentials
注意在 Authorization 填寫 client信息,下面是 curl 請求:
curl:
curl -X POST \
'http://localhost:9090/oauth/token?scope=read&grant_type=client_credentials' \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Authorization: Basic Y2xpZW50MToxMjM0NTY=' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Length: 35' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Cookie: remember-me=; JSESSIONID=F6F6DE2968113DDE4613091E998D77F4' \
-H 'Host: localhost:9090' \
-H 'Postman-Token: a8d3b4a2-7aee-4f0d-8959-caa99a412012,f5e41385-b2b3-48d2-aa65-8b1d1c075cab' \
-H 'User-Agent: PostmanRuntime/7.16.3' \
-H 'cache-control: no-cache' \
-d 'username=zhoutaoo&password=password'
授權服務開發,我們繼續延用之前的security配置,包括 UserDetailsService及其登陸配置,在其基礎上我們新增了授權配置,完成整個授權服務的搭建及測試。
三、 資源服務器開發
由於資源服務器是個權限的應用程序,我們新建 security-oauth2-authentication 子模塊作為資源服務器應用。
一、Maven依賴
security-oauth2-authentication 模塊 pom 引用 security-core:
<dependency>
<groupId>com.zhc</groupId>
<artifactId>security-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 不是starter,手動配置 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<!--請注意下 spring-authorization-oauth2 的版本 務必高於 2.3.2.RELEASE,這是官方的一個bug:
java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
要求必須大於2.3.5 版本,官方解釋:https://github.com/BUG9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open
-->
<version>2.3.5.RELEASE</version>
</dependency>
二、配置授權服務 @EnableResourceServer
整個資源服務的配置主要分3個點:
- @EnableResourceServer 必須的,是整個資源服務器的基礎
- tokenStore 由於授權服務器采用了不同的tokenStore,所以我們解析token也得根據配置的存儲策略來
- HttpSecurity 一般來說只要是資源服務器,其內部的接口均需要認證后才可訪問,這里簡單配置了以下。
@Configuration
@EnableResourceServer // 1
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private final Map<String,TokenStore> tokenStoreMap;
@Value("${spring.security.oauth2.storeType}")
private String storeType = "jwt";
@Autowired
public ResourceServerConfiguration(Map<String, TokenStore> tokenStoreMap) {
this.tokenStoreMap = tokenStoreMap;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStoreMap.get(storeType + "TokenStore")); // 2
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().anyRequest()
.and()
.anonymous()
.and()
.authorizeRequests()
//配置oauth2訪問(測試接口)控制,必須認證過后才可以訪問
.antMatchers("/oauth2/**").authenticated(); // 3
}
}
三、配置 application.yml
由於授權服務器采用不同tokenStore,所以這里也要引用 其 配置:
spring:
redis:
host: 127.0.0.1
port: 6379
security:
oauth2:
storeType: jwt
jwt:
SigningKey: oauth2
四、測試接口
@RestController
@RequestMapping("/oauth2")
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class TestEndpoints {
@GetMapping("/getUser")
@PreAuthorize("hasAnyAuthority('user')")
public String getUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "User: " + authentication.getPrincipal().toString();
}
}
五、啟動測試
我們將從授權服務器獲取到的token進行訪問測試接口:
Postman:
http://localhost:8090/oauth2/getUser?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU2ODY0ODEyMCwianRpIjoiNDQ0NWQ1ZDktYWZlMC00N2Y1LTk0NGItZTEyNzI1NzI1M2M1IiwiY2xpZW50X2lkIjoiY2xpZW50MSIsInVzZXJuYW1lIjoiY2xpZW50MSJ9.pOnIcmjy2ex7jlXvAGslEN89EyFPYPbW-l4f_cyK17k
curl:
curl -X GET \
'http://localhost:8090/oauth2/getUser?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU2ODY0ODEyMCwianRpIjoiNDQ0NWQ1ZDktYWZlMC00N2Y1LTk0NGItZTEyNzI1NzI1M2M1IiwiY2xpZW50X2lkIjoiY2xpZW50MSIsInVzZXJuYW1lIjoiY2xpZW50MSJ9.pOnIcmjy2ex7jlXvAGslEN89EyFPYPbW-l4f_cyK17k' \
-H 'Accept: */*' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Cookie: remember-me=; JSESSIONID=F6F6DE2968113DDE4613091E998D77F4' \
-H 'Host: localhost:8090' \
-H 'Postman-Token: 07ec53c7-9051-439b-9603-ef0fe93664fa,e4a5b46e-feb7-4bf8-ab53-0c33aa44f661' \
-H 'User-Agent: PostmanRuntime/7.16.3' \
-H 'cache-control: no-cache'
四、 個人總結
Spring security Oauth2 就是一套標准的Oauth2實現,我們可以通過開發進一步的了解Oauth2的,但整體上涉及到的技術還是很多的,比如redis、jwt等等。本文僅僅只是簡單的演示Spring security Oauth2 Demo,希望對你有幫助,如果你還對想深入解析下Spring Security Oauth2,那么請繼續關注我,后續會解析其原理。
本文介紹Spring security Oauth2開發的代碼可以訪問代碼倉庫 ,項目的github 地址 : https://github.com/BUG9/spring-security
如果您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!