前言
《Spring Microservices in Action》
《Spring Cloud Alibaba 微服務原理與實戰》
《B站 尚矽谷 SpringCloud 框架開發教程 周陽》
JWT 為 OAuth2 令牌提供規範標準,並且可以自定義 JWT 令牌;
1. JWT 令牌儲存基礎知識
1.1 JSON Web Token
- OAuth2 是一個基於令牌的驗證框架,但它並沒有為如何定義其規範中的令牌提供任何標準;
- 由此出現了 JSON Web Token ( JWT )新標準;
- JWT是因特網工程任務組( Internet Engineering Task Force, IETF )提出的開放標準(RFC-7519 ),旨在為 0Auth2令牌提供標準結構
- JWT 令牌的特點:小巧、密碼簽名、自包含、可擴充套件;
- Spring Cloud Security 為 JWT 提供了開箱即用的支援;
2. 構建使用 JWT 令牌儲存的 OAuth2 伺服器
2.1 引入 pom.xml 依賴檔案
<!-- JWT OAuth2 庫 -->
<dependency>
<groupid>org.springframework.security</groupid>
<artifactid>spring-security-jwt</artifactid>
</dependency>
<!--security 通用安全庫-->
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-security</artifactid>
</dependency>
<!--oauth2.0-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
2.2 建立 JWT 令牌儲存
在 security 包或 config 包下;
@Configration
public class JWTTokenStoreConfig{
@Autowired
private ServiceConfig serviceConfig;
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
//用於從出示給服務的令牌中讀取資料
@Bean
@Primary //用於告訴 Spring,如果有多個特定型別的 bean(本例中為 DefaultTokenService),那麼使用 @Primary 標註的 Bean 型別自動注入
public DefaultTikenServices tokenServices(){
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
//在 JWT 和 OAuth2 伺服器之間充當翻譯
JwtAccessTokenConverter conver = new JwtAccessTokenConverter();
//定義將用於簽署令牌的簽名金鑰
conver.setSigningKey(serviceConfig.getJwtSigningKey());
}
@Bean
public TokenEnhancer jwtTokenEnhancer(){
return new JWTTokenEnhancer();
}
}
- 該類用於定義 Spring 將如何管理 JWT 令牌的建立、簽名和翻譯;
2.3 將 JWT 掛載到驗證服務中
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private DefaultTokenServices tokenServices;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
endpoints.tokenStore(tokenStore) //JWT,5.2中定義的命令儲存將在這裡注入
.accessTokenConverter(jwtAccessTokenConverter) //JWT,鉤子,用於告訴 Spring Security OAuth2 程式碼使用 JWT
.tokenEnhancer(tokenEnhancerChain) //JWT
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("eagleeye")
.secret("thisissecret")
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
}
3. 在受保護服務中使用 JWT
3.1 引入 pom.xml 依賴檔案
- 同 OAuth2 伺服器依賴;
<!-- JWT OAuth2 庫 -->
<dependency>
<groupid>org.springframework.security</groupid>
<artifactid>spring-security-jwt</artifactid>
</dependency>
<!--security 通用安全庫-->
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-security</artifactid>
</dependency>
<!--oauth2.0-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
3.2 在受保護服務中建立 JWTTokenStoreConfig 類
- 同本篇《5.2 建立 JWT 令牌儲存》;
@Configuration
public class JWTTokenStoreConfig {
@Autowired
private ServiceConfig serviceConfig;
//JWT
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
//JWT
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
//JWT
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(serviceConfig.getJwtSigningKey());
return converter;
}
}
3.3 建立自定義的 RestTemplate 類以注入 JWT 令牌
可以在主程式類裡,也可以在主程式類所在包及其子包裡;
@Primary
@Bean
public RestTemplate getCustomRestTemplate() {
RestTemplate template = new RestTemplate();
List interceptors = template.getInterceptors();
if (interceptors == null) {
//UserContextInterceptor 會將 Authorization 首部注入每個 REST 呼叫
template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
} else {
interceptors.add(new UserContextInterceptor());
template.setInterceptors(interceptors);
}
return template;
}
3.4 UserContextInterceptor 將注入 JWT 令牌到 REST 呼叫
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
HttpHeaders headers = request.getHeaders();
headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
//將授權令牌新增到 HTTP 首部
headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
return execution.execute(request, body);
}
}
3.5 擴充套件 JWT 令牌(也叫自定義、增強)
- 通過向被保護服務新增一個 Spring OAuth2 令牌增強類,擴充套件 JWT 令牌;
//需要擴充套件 TokenEnhancer 類
public class JWTTokenEnhancer implements TokenEnhancer {
@Autowired
private OrgUserRepository orgUserRepo;
//基於使用者名稱查詢使用者的組織 ID
private String getOrgId(String userName){
UserOrganization orgUser = orgUserRepo.findByUserName( userName );
return orgUser.getOrganizationId();
}
//進行這種增強,需要覆蓋 enhance() 方法
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
String orgId = getOrgId(authentication.getName());
additionalInfo.put("organizationId", orgId);
//所有附加屬性放在 HashMap 中,並設定在傳入該方法的 accessToken 變數上
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
3.6 告訴被保護服務使用 JWTTokenEnhancer 類
- 首先需要公開 TokenEnhancer 類;
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return new JWTTokenEnhancer();
}
- 將 TokenEnhancer 類掛鉤在驗證服務中。類似《2.3 將 JWT 掛載到驗證服務中》;
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private DefaultTokenServices tokenServices;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
//自動裝配 TokenEnhancer 類
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//允許開發人員掛鉤多個令牌增強器
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
endpoints.tokenStore(tokenStore) //JWT
.accessTokenConverter(jwtAccessTokenConverter) //JWT
.tokenEnhancer(tokenEnhancerChain) //JWT,將令牌增強器鏈掛鉤到傳入 configure() 方法的 endpoints 引數
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("eagleeye")
.secret("thisissecret")
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
}
3.7 使用 JJWT 庫在 Zuul 閘道器裡解析 JWT 令牌中解析自定義欄位
以下配置均在在 Zuul 閘道器服務裡進行;
3.7.1 在 Zuul 閘道器裡新增 pom.xml 依賴檔案
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
3.7.2 新增一個新方法,用來解析 serviceId
private String getOrganizationId(){
String result="";
if (filterUtils.getAuthToken()!=null){
//從 HTTP 首部 Authorization 解析出令牌
String authToken = filterUtils.getAuthToken().replace("Bearer ","");
try {
//傳入用於簽署令牌的簽名金鑰,使用 JWTS 類解析令牌
Claims claims = Jwts.parser()
.setSigningKey(serviceConfig.getJwtSigningKey().getBytes("UTF-8"))
.parseClaimsJws(authToken).getBody();
//從令牌中提取 xxxId
result = (String) claims.get("organizationId");
}
catch (Exception e){
e.printStackTrace();
}
}
return result;
}