序
Spring Security OAuth2的demo在前幾篇文章中已經講過了,在那些模式中使用的都是RemoteTokenService調用授權服務器來校驗token,返回校驗通過的用戶信息供上下文中獲取
這種方式會加重授權服務器的負載,你想啊,當用戶沒授權時候獲取token得找授權服務器,有token了訪問資源服務器還要訪問授權服務器,相當於說每次請求都要訪問授權服務器,這樣對授權服務器的負載會很大
常規的方式有兩種來解決這個問題:
- 使用JWT作為Token傳遞
- 使用Redis存儲Token,資源服務器本地訪問Redis校驗Token
使用JWT與Redis都可以在資源服務器中進行校驗Token,從而減少授權服務器的工作量
JWT默認使用HMACSHA256對稱加密算法,以下記錄下默認算法實現與非對稱RSA算法的集成,使用不同算法加解密測試方法是一致的,所以放在文章最后
授權服務器整合JWT——對稱加解密算法
授權服務器整體代碼結構
pom.xml中引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<!-- Spring Security OAuth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
SecurityConfig配置,主要需要顯式聲明AuthenticationManager和UserDetailsService這兩個bean
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public UserDetailsService userDetailsService(){ //主要是配置這個Bean,用於授權服務器配置中注入
return super.userDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// @formatter: off
auth.inMemoryAuthentication()
.withUser("hellxz")
.password(passwordEncoder().encode("xyz"))
.authorities(Collections.emptyList());
// @formatter: on
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //所有請求都需要通過認證
.and()
.httpBasic() //Basic提交
.and()
.csrf().disable(); //關跨域保護
}
}
授權服務器配置AuthorizationConfig
@Configuration
@EnableAuthorizationServer //開啟授權服務
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
public UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允許表單提交
security.allowFormAuthenticationForClients()
.checkTokenAccess("permitAll()")
.tokenKeyAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// @formatter: off
clients.inMemory()
.withClient("client-a") //client端唯一標識
.secret(passwordEncoder.encode("client-a-secret")) //client-a的密碼,這里的密碼應該是加密后的
.authorizedGrantTypes("authorization_code", "password", "refresh_token") //授權模式標識,這里主要測試用password模式,另外refresh_token不是一種模式,但是可以使用它來刷新access_token(在它的有效期內)
.scopes("read_user_info") //作用域
.resourceIds("resource1") //資源id
.redirectUris("http://localhost:9001/callback"); //回調地址
// @formatter: on
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenStore(jwtTokenStore()) //設置jwtToken為tokenStore
.accessTokenConverter(jwtAccessTokenConverter());//設置access_token轉換器
}
/**
* jwt訪問token轉換器
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("my-sign-key"); //資源服務器需要配置此選項方能解密jwt的token
return converter;
}
/**
* jwt的token存儲對象
*/
@Bean
public JwtTokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
這里主要是在configure(AuthorizationServerEndpointsConfigurer endpoints)
授權服務的端點配置中加入JWT的tokenStore和access_token的轉換器,以及這二者的聲明Bean方法
這里使用的是默認對稱MAC算法,即加密解密使用相同的密鑰
啟動類就不說了,開啟@SpringBootApplicatin的main方法
資源服務器整合JWT——對稱加解密算法
資源服務器主要就一個資源配置類
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
//設置創建session策略
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
//@formatter:off
//所有請求必須授權
http.authorizeRequests()
.anyRequest().authenticated();
//@formatter:on
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(jwtTokenStore());
}
/**
* jwt訪問token轉換器
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("my-sign-key"); //與授權服務器相同的signingKey
return converter;
}
/**
* jwt的token存儲對象
*/
@Bean
public JwtTokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
配置JWT的TokenStore和AccessTokenConverter與授權服器相同,添加啟動類完成配置
OAuth整合JWT——非對稱加解密RSA
本部分基於對稱加密部分,僅展示需要修改的部分
首先使用keytool生成jks (Java Key Store) 密鑰,按提示輸入姓氏等信息
keytool -genkeypair -alias hellxz-jwt -validity 3650 -keyalg RSA -keypass hellxzTest -keystore hellxz-jwt.jks -storepass hellxzTest
生成的私鑰文件會在當前目錄,把hellxz-jwt.jks復制到授權服務器的resources目錄下
授權服務器需修改jwtAccessTokenConverter()
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory storeKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource("hellxz-jwt.jks"), "hellxzTest".toCharArray());
converter.setKeyPair(storeKeyFactory.getKeyPair("hellxz-jwt"));
return converter;
}
在hellxz-jwt.jks同目錄下,執行命令生成公鑰
➜ keytool -list -rfc --keystore hellxz-jwt.jks | openssl x509 -inform pem -pubkey
輸入密鑰庫口令: hellxzTest
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxU7zulFUVBXmZD28xwM4
ul5e9yFrToLgWKHlNLlp904/GbiWBoZ4tcBcNq3VxLGBN9VOqfP1P5C7fRgz95UI
7ShKCKgsFFGL2rAqsplMDClN/adfsxmpF06rVIkGgce9tR0Q0iONcaN+b/lArK4T
Au76QsQwn9MLXlznVfczclZOZSfDNju+1JuBzqt6fEPWqalBUVYdV0zCUDG8ikN1
l9D0m1tSSaKpiTrU2yEUGUji+79Ury7Y8BClEX6d4CTl9TQAhL5g32GoJEc0S2y+
0bqeqUsv1nUt9KiJT9kiOvA+Q7o2T8OHuqQT9le7kvmIi4gSX5vSNvvZagE2Uglh
zQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDUTCCAjmgAwIBAgIEePeDczANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJD
TjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEKMAgGA1UEChMB
MDEKMAgGA1UECxMBMDEOMAwGA1UEAxMFemhhbmcwHhcNMTkxMjE1MDUyOTM2WhcN
MjkxMjEyMDUyOTM2WjBZMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHYmVpamluZzEQ
MA4GA1UEBxMHYmVpamluZzEKMAgGA1UEChMBMDEKMAgGA1UECxMBMDEOMAwGA1UE
AxMFemhhbmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFTvO6UVRU
FeZkPbzHAzi6Xl73IWtOguBYoeU0uWn3Tj8ZuJYGhni1wFw2rdXEsYE31U6p8/U/
kLt9GDP3lQjtKEoIqCwUUYvasCqymUwMKU39p1+zGakXTqtUiQaBx721HRDSI41x
o35v+UCsrhMC7vpCxDCf0wteXOdV9zNyVk5lJ8M2O77Um4HOq3p8Q9apqUFRVh1X
TMJQMbyKQ3WX0PSbW1JJoqmJOtTbIRQZSOL7v1SvLtjwEKURfp3gJOX1NACEvmDf
YagkRzRLbL7Rup6pSy/WdS30qIlP2SI68D5DujZPw4e6pBP2V7uS+YiLiBJfm9I2
+9lqATZSCWHNAgMBAAGjITAfMB0GA1UdDgQWBBQF96rK7n0XufnvtJuH9tD9Ixza
6zANBgkqhkiG9w0BAQsFAAOCAQEAuMzWZJhej6+4TGgodQKQ5L5RBtOUbesxA1Ue
s9iA4m/jNZnVCXJE0nY47YVzBCIkIsYALswGooMj1PIJxEMpggXVmIuiJpaPgg+4
sthzISxKzX0ru8IrJTapaglMi74ai6S73LTBSke9GEPgWWnbtdUZoUSiSNt1oJ0J
EhFHdPuzxc36neDFRBOBxW4w3qhsTlKTN2wJm1nLV96nFKmqJhQJhhKt6ihe7hMg
qWxzNsWAqv9gJNdKZt5teqwNKT6H7r1NX5oJkJ0Kn1dZy0O3rDDd5E0KDKkMtwOh
3deJH6Uvtt/dw/drzJlByNDEPp6hYGQu2dW5JG5uiHuzFHnJeA==
-----END CERTIFICATE-----
復制公鑰部分到public.cert放到資源服務器的resources目錄
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxU7zulFUVBXmZD28xwM4
ul5e9yFrToLgWKHlNLlp904/GbiWBoZ4tcBcNq3VxLGBN9VOqfP1P5C7fRgz95UI
7ShKCKgsFFGL2rAqsplMDClN/adfsxmpF06rVIkGgce9tR0Q0iONcaN+b/lArK4T
Au76QsQwn9MLXlznVfczclZOZSfDNju+1JuBzqt6fEPWqalBUVYdV0zCUDG8ikN1
l9D0m1tSSaKpiTrU2yEUGUji+79Ury7Y8BClEX6d4CTl9TQAhL5g32GoJEc0S2y+
0bqeqUsv1nUt9KiJT9kiOvA+Q7o2T8OHuqQT9le7kvmIi4gSX5vSNvvZagE2Uglh
zQIDAQAB
-----END PUBLIC KEY-----
修改資源服務器jwtAccessTokenConverter()方法
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
測試驗證
發送POST請求http://localhost:8080/oauth/token?username=hellxz&password=xyz&scope=read_user_info&grant_type=password
返回結果
帶token訪問資源服務器
測試通過
另外使用JWT應設置盡量短的過期時間,因為JWT的token無法手動revoke,只能等待其到達過期時間失效