7.2 构建使用 JWT 令牌存储的 OAuth2 安全认证
前言
《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;
}