接着上一篇博客:https://www.cnblogs.com/wwjj4811/p/14505081.html
JWT介紹
JSON Web Token(JWT)是一個開放的行業標准(RFC 7519),它定義了一種緊湊且獨立的方式,用於在各方之間作為JSON對象安全地傳輸信息。此信息可以通過數字簽名進行驗證和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名,防止被篡改
構成
JWT 有三部分構成:頭部、有效載荷、簽名
-
頭部:包含令牌的類型(JWT)與加密的簽名算法(如 SHA256 或 ES256),Base64編碼后加入第一部分
-
有效載荷:通俗一點講就是token中需要攜帶的信息都將存於此部分,比如:用戶id、權限標識等信息。
注:該部分信息任何人都可以讀出來,所以添加的信息需要加密才會保證信息的安全性
-
簽名:用於防止 JWT 內容被篡改, 會將頭部和有效載荷分別進行 Base64編碼,編碼后用.連接組成新的字符串,然后再使用頭部聲明的簽名算法進行簽名。在具有秘鑰的情況下,可以驗證JWT的准確性,是否被篡改
認證服務器-對稱加密 JWT 令牌
對稱加密就是同一個密鑰可以同時用作信息的加密和解密,這種加密方法稱為對稱加密,也稱為單密鑰加密。
JWT 管理令牌
1.重構 com.wj.oauth2.server.config.TokenConfig 將令牌存儲方式切換為 JWT
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
//jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
// JWT 簽名秘鑰
private static final String SIGNING_KEY = "wj-key";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
}
JWT轉換器添加到令牌端點
修改AuthorizationServerConfig類:
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* 重寫父類的方法
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//密碼模式需要設置此認證管理器
endpoints.authenticationManager(authenticationManager);
// 刷新令牌獲取新令牌時需要
endpoints.userDetailsService(customUserDetailsService);
//設置token存儲策略
endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
//授權碼管理策略,針對授權碼模式有效,會將授權碼放到 auth_code 表,授權后就會刪除它
endpoints.authorizationCodeServices(authorizationCodeServices());
}
測試
使用密碼授權模式獲取access_token,發現access_token已經響應為jwt令牌
檢查 JWT 令牌:http://localhost:8090/auth/oauth/check_token,已經包含了用戶基本信息
資源服務器-對稱加密 JWT 令牌
在 JwtAccessTokenConverter 中使用了一個對稱密鑰來簽署我們的令牌,意味着我們需要為資源服務器使用同樣的密鑰來驗證簽名合法性。
JWT 管理令牌
這部分與認證服務器令牌配置是一樣的, 將 JWT 令牌部分拷貝到cloud-oauth2-resource-product中的com.wj.oauth2.resource.config 包下即可.
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
//jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
// JWT 簽名秘鑰
private static final String SIGNING_KEY = "wj-key";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
}
資源服務器校驗 JWT 令牌
修改ResourceServerConfig配置類:注釋掉tokenService方法,並設置resources的tokenStore,就會自動本地校驗jwt
@Configuration
// 標識為資源服務器, 所有發往當前服務的請求,都會去請求頭里找token,找不到或驗證不通過不允許訪問
@EnableResourceServer
//開啟方法級別權限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//配置當前資源服務器的ID
private static final String RESOURCE_ID = "product-server";
@Autowired
private TokenStore tokenStore;
/**當前資源服務器的一些配置, 如資源服務器ID **/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// 配置當前資源服務器的ID, 會在認證服務器驗證(客戶端表的resources配置了就可以訪問這個服務)
resources.resourceId(RESOURCE_ID)
// 實現令牌服務, ResourceServerTokenServices實例
.tokenStore(tokenStore);
}
/* @Bean
public ResourceServerTokenServices tokenService() {
// 資源服務器去遠程認證服務器驗證 token 是否有效
RemoteTokenServices service = new RemoteTokenServices();
// 請求認證服務器驗證URL,注意:默認這個端點是拒絕訪問的,要設置認證后可訪問
service.setCheckTokenEndpointUrl("http://localhost:8090/auth/oauth/check_token");
// 在認證服務器配置的客戶端id
service.setClientId("wj-pc");
// 在認證服務器配置的客戶端密碼
service.setClientSecret("wj-secret");
return service;
}*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
//不創建session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//資源授權規則
.authorizeRequests()
.antMatchers("/product/**").hasAuthority("product")
//所有的請求對應訪問的用戶都要有all范圍的權限
.antMatchers("/**").access("#oauth2.hasScope('all')");
}
}
測試
密碼認證模式先獲取access_token,再通過 JWT令牌查詢獲取商品資源
為了安全性, JWT令牌每次請求獲取令牌都是響應一個新令牌,因為里面已經包含用戶信息, 而之前的是令牌在有效時間里每次請求都是響應一樣的令牌。
認證服務器-非對稱加密 JWT 令牌
非對稱加密算法需要兩個密鑰:公開密鑰(publickey:簡稱公鑰)和私有密鑰(privatekey:簡稱私鑰)。公鑰與私鑰是一對,如果用私鑰對數據進行加密,只有用對應的公鑰才能解密。
生成密鑰證書
公私密鑰可以用jdk的命令keytool
生成:別名為 oauth2,秘鑰算法為 RSA,秘鑰口令為 oauth2,秘鑰庫(文件)名稱為 oauth2.jks,秘鑰庫(文件)口令為 oauth2
keytool -genkeypair -alias oauth2 -keyalg RSA -keypass oauth2 -keystore oauth2.jks -storepass oauth2
生成后,在命令執行命令的所在目錄下會有一個oauth2.jks文件
將該文件移動到認證服務的resources目錄下:
非對稱加密 JWT 令牌
修改認證服務的TokenConfig類:
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
//return new JdbcTokenStore(dataSource);
//jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "oauth2".toCharArray());
jwtAccessTokenConverter.setKeyPair(keyFactory.getKeyPair("oauth2"));
return jwtAccessTokenConverter;
}
}
pom文件
修改認證服務的pom.xml
<build>
<resources>
<resource>
<!-- 防止JKS被maven錯誤解析 -->
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.2.4.RELEASE</version>
<configuration>
<mainClass>com.wj.oauth2.AuthServerApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
因為有可能jks文件無法正確被maven解析,導致項目啟動報錯。
資源服務器-非對稱加密 JWT 令牌
根據密鑰文件獲取公鑰
OpenSSL 是一個加解密工具包,可以使用 OpenSSL 來獲取公鑰,下載網址:http://slproweb.com/products/Win32OpenSSL.html
安裝完成后,配置openssl環境變量,即安裝目錄\bin
獲取公鑰
進入oauth2.jks所在目錄執行命令:
keytool -list -rfc --keystore oauth2.jks | openssl x509 -inform pem -pubkey
復制出控制台打印出的內容到public.txt,並放到資源服務器的resources目錄下(需要復制出public key的內容)
非對稱加密 JWT 令牌
修改資源服務器的TokenConfig類:
@Slf4j
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
//jwt管理令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
// JWT 簽名秘鑰
private static final String SIGNING_KEY = "wj-key";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
ClassPathResource classPathResource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey=IOUtils.toString(classPathResource.getInputStream(),"UTF-8");
log.info("publicKey:{}" , publicKey);
} catch (IOException e) {
e.printStackTrace();
}
jwtAccessTokenConverter.setVerifierKey(publicKey);
return jwtAccessTokenConverter;
}
}
測試
先請求access_token令牌:發現比對稱加密的字符數目多了不少
發送請求,仍然可以請求成功