資源文件簡介
文章參考自 https://projects.spring.io/spring-security-oauth/docs/oauth2.html
一個資源服務(可以和授權服務在同一個應用中,當然也可以分離開成爲兩個不同的應用程序)提供一些受token令牌保護的資源,Spring OAuth提供者是通過Spring Security authentication filter 即驗證過濾器來實現的保護,你可以通過 @EnableResourceServer 註解到一個 @Configuration 配置類上,並且必須使用 ResourceServerConfigurer 這個配置對象來進行配置(可以選擇繼承自 ResourceServerConfigurerAdapter 然後覆寫其中的方法,參數就是這個對象的實例),下面是一些可以配置的屬性:
- tokenServices:ResourceServerTokenServices 類的實例,用來實現令牌服務。
- resourceId:這個資源服務的ID,這個屬性是可選的,但是推薦設置並在授權服務中進行驗證。
- 其他的拓展屬性例如 tokenExtractor 令牌提取器用來提取請求中的令牌。
- 請求匹配器,用來設置需要進行保護的資源路徑,默認的情況下是受保護資源服務的全部路徑。
- 受保護資源的訪問規則,默認的規則是簡單的身份驗證(plain authenticated)。
- 其他的自定義權限保護規則通過 HttpSecurity 來進行配置。
@EnableResourceServer 註解自動增加了一個類型爲 OAuth2AuthenticationProcessingFilter 的過濾器鏈,
在XML配置中,使用 標籤元素並指定id爲一個servlet過濾器就能夠手動增加一個標準的過濾器鏈。
ResourceServerTokenServices 是組成授權服務的另一半,如果你的授權服務和資源服務在同一個應用程序上的話,你可以使用 DefaultTokenServices ,這樣的話,你就不用考慮關於實現所有必要的接口的一致性問題,這通常是很困難的。如果你的資源服務器是分離開的,那麼你就必須要確保能夠有匹配授權服務提供的 ResourceServerTokenServices,它知道如何對令牌進行解碼。
在授權服務器上,你通常可以使用 DefaultTokenServices 並且選擇一些主要的表達式通過 TokenStore(後端存儲或者本地編碼)。
RemoteTokenServices 可以作爲一個替代,它將允許資源服務器通過HTTP請求來解碼令牌(也就是授權服務的 /oauth/check_token 端點)。如果你的資源服務沒有太大的訪問量的話,那麼使用RemoteTokenServices 將會很方便(所有受保護的資源請求都將請求一次授權服務用以檢驗token值),或者你可以通過緩存來保存每一個token驗證的結果。
使用授權服務的 /oauth/check_token 端點你需要將這個端點暴露出去,以便資源服務可以進行訪問,這在咱們授權服務配置中已經提到了,下面是一個例子:
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
在這個例子中,我們配置了 /oauth/check_token 和 /oauth/token_key 這兩個端點(受信任的資源服務能夠獲取到公有密匙,這是爲了驗證JWT令牌)。這兩個端點使用了HTTP Basic Authentication 即HTTP基本身份驗證,使用 client_credentials 授權模式可以做到這一點。
配置OAuth-Aware表達式處理器(OAuth-Aware Expression Handler):
你也許希望使用 Spring Security’s expression-based access control 來獲得一些優勢,一個表達式處理器會被註冊到默認的 @EnableResourceServer 配置中,這個表達式包含了 #oauth2.clientHasRole,#oauth2.clientHasAnyRole 以及 #oauth2.denyClient 所提供的方法來幫助你使用權限角色相關的功能(在 OAuth2SecurityExpressionMethods 中有完整的列表)。
在XML配置中你可以註冊一個 OAuth-Aware 表達式處理器即 元素標籤到 常規的 安全配置上。
資源文件實戰
使用之前的授權服務器: https://blog.csdn.net/liaomin416100569/article/details/88529127#_134
授權服務器改造
由於之前在授權服務器使用了jwtstore,使用資源服務器驗證時發現JwtHelper類的驗證token
有個SignatureVerifier發現如何不使用RSA(公私密鑰)verifier永遠是空,永遠是400 null錯誤
public static Jwt decodeAndVerify(String token, SignatureVerifier verifier) {
Jwt jwt = decode(token);
jwt.verifySignature(verifier);
return jwt;
}
生成公私密鑰(注意一定要添加–keyalg RSA 默認是DSA哦,JWT只支持RSA)
C:\Users\Administrator>keytool -genkeypair --keyalg RSA -keystore c:/a.keystore -alias test1
輸入密鑰庫口令:
您的名字與姓氏是什麼?
[Unknown]: a
您的組織單位名稱是什麼?
[Unknown]: a
您的組織名稱是什麼?
[Unknown]: a
您所在的城市或區域名稱是什麼?
[Unknown]: a
您所在的省/市/自治區名稱是什麼?
[Unknown]: a
該單位的雙字母國家/地區代碼是什麼?
[Unknown]: a
CN=a, OU=a, O=a, L=a, ST=a, C=a是否正確?
[否]: y
輸入 <test1> 的密鑰口令
(如果和密鑰庫口令相同, 按回車):
將生成的c:/a.keystore拷貝到授權項目的src/main/resources目錄
改造授權服務器配置方法
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//一定要設置對稱的SigningKey 用於驗證之後的token是否有效的
jwtAccessTokenConverter.setSigningKey("123456");
KeyStore keyStore = KeyStore.getInstance("JCEKS");
keyStore.load(OAuth2AuthorizationServerConfig.class.getResourceAsStream("/a.keystore"),"123456".toCharArray());
PublicKey publicKey = keyStore.getCertificate("test1").getPublicKey();
PrivateKey privateKey = (PrivateKey)keyStore.getKey("test1", "123456".toCharArray());
KeyPair kp=new KeyPair(publicKey,privateKey);
jwtAccessTokenConverter.setKeyPair(kp);
JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter);
endpoints.pathMapping("/oauth/confirm_access ","/extenal/oauth/confirm_access")
.tokenStore(jwtTokenStore)
.tokenEnhancer(jwtAccessTokenConverter)
//添加額外信息到token中,注意jwtAccessTokenConverter不能設置額外的tokenEnhancer否則無法生成jwt的token註釋掉
// .tokenEnhancer(new TokenEnhancer() {
// @Override
// public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
// DefaultOAuth2AccessToken doat= (DefaultOAuth2AccessToken) oAuth2AccessToken;
// Map<String, Object> additionalInfo = new HashMap<>();
// additionalInfo.put("myname", "jiaozi");
// doat.setAdditionalInformation(additionalInfo);
// return doat;
// }
// })
;
}
資源服務器改造
新建maven項目
添加依賴(和資源服務器一致)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.8.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>1.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.6.2</version>
</dependency>
</dependencies>
添加資源服務器配置類OAuth2ResourceServerConfig
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
public RestTemplate restTemplate() {
//httpRequestFactory()
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter;
jsonConverter.setObjectMapper(new ObjectMapper());
List<MediaType> supportedMediaTypes=new ArrayList<>();
supportedMediaTypes.add(new MediaType("application", "json", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET));
supportedMediaTypes.add(new MediaType("text", "javascript", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET));
jsonConverter.setSupportedMediaTypes(supportedMediaTypes);
}
}
return restTemplate;
}
/**
因爲資源服務器和授權服務器分離所以使用RemoteTokenServices
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
RemoteTokenServices remoteTokenServices=new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8889/oauth/check_token");//指定授權服務器檢查token的地址
remoteTokenServices.setClientId("client");
remoteTokenServices.setClientSecret("secret");
remoteTokenServices.setRestTemplate(restTemplate());
remoteTokenServices.setAccessTokenConverter(new JwtAccessTokenConverter());
resources.tokenServices(remoteTokenServices);
}
}
配置application.properties
server.port=8887
spring.thymeleaf.cache=false
spring.thymeleaf.mode=LEGACYHTML5
添加啓動類
@SpringBootApplication
public class ResourceServerMain {
public static void main(String[] args) {
SpringApplication.run(ResourceServerMain.class);
}
}
添加一個控制層的資源
@Controller
public class TestController {
@ResponseBody
@GetMapping("/test")
public String test() {
return "hello";
}
}
開始測試(前面兩步過程參考授權服務器文章)
- 訪問認證服務器獲取授權碼
localhost:8889/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
拷貝授權碼 假設是Ce6BxM - 通過授權碼獲取token
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTI1OTk1MDEsInVzZXJfbmFtZSI6InRlc3QiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiNjQxNzA0MTEtYTQ3Ni00NzQ5LWIzZTItOTM2NzA0NmY5MjAzIiwiY2xpZW50X2lkIjoiY2xpZW50Iiwic2NvcGUiOlsiYWxsIl19.XERRnoc-NVXJ2H6wweDK2hl9Wf7gI48NHY0aMXh2g16Hctwv60-wn41FcQnOVuSxChbBe7oe5kaXQdq7SjbTMbjAD0VPu6B4X18IsTgJ5BP-tGruhWdtxcqCJ_Gg8HRkI-62F_RO7n-B1zkf-ZmvSPO3chvBL7xiH8S0lE0c1b5FXMMIFtoqvSPVPNAt9UDo6p4JiGWwKq9Podo1bH9FxOxoHrVBYb03IRn_ASjd0Vx0iQZVG-J6VTDDUfHcHPc1HwPgaK7aEoaE-9WucsgtXDaa-C69PcU-XHPwxfKDAYrgTIAJ1kux0DLuMjWavALho1BxlVeWImFe3b0WPhKooA","token_type":"bearer","expires_in":43199,"scope":"all","jti":"64170411-a476-4749-b3e2-9367046f9203"}
- 通過授權碼訪問/test
注意訪問的過程是在訪問的請求頭 加上
Authorization: Bearer 你的token
我圖裏面用的post請求
{"timestamp":"2019-03-14T09:40:59.035+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/test"}
我的控制層是get請求使用get請求成功輸出hello
你也可以直接在瀏覽器輸入check_token來檢查token是否正確