微服务授权认证机制
1. 背景
OAuth2.0是当前业界标准的授权协议,它的核心是若干个针对不同场景的令牌颁发和管理流程;而JWT是一种轻量级、自包含的令牌,可用于在微服务间安全地传递用户信息。OAuth 2.0的模式一共有四种,这里只假设客户直接访问公司自己的门户网站,即第一方Web应用,可以选择OAuth 2.0的资源拥有者凭据模式。 OAuth2令牌 + JWT混合模式,IDP(Identity Provider)要支持OAuth 2.0 授权协议处理,及从OAuth2令牌到JWT令牌的转换。最后以代码方式实现了授权、认证过程。
2. Overview
先做一个概览,下面是Access token + JWT混合模式流程图:
Client : 资源拥有者
IDP:授权服务
Providers: 受保护资源
3. 认证授权流程
3.1 获取Access Token
- 从身份认证服务器获取身份验证OAuth凭据client id 和 secret
- 请求Access Token: Token API in gateway generate OAuth2 access token for the Client Id, Secret and Scopes combination.
实验 - 基于spring security oauth2 获取access token
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
</dependencies>
Authorization 配置类:
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
public OAuth2AuthorizationConfig(AuthenticationConfiguration authenticationConfiguration) throws Exception {
this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpointsConfigurer) {
endpointsConfigurer.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
clientDetailsServiceConfigurer.inMemory()
.withClient("clientapp")
.secret("{bcrypt}$2a$10$OL6aAqldM9QMoK/D3Y0xA.KIaWw9kOBs83exhr4DUACcHq5ZeCI7C")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read_userinfo", "read_contacts");
}
}
获取access_token实验结果:
将Resource server 配置类与Authorization集成在同一个ms中,使用access token直接获取资源。但大型分布式微服务的授权和认证还是推荐 OAuth2 + JWT混合模式。
@Configuration
@EnableResourceServer
public class OAuth2ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/api/**");
}
}
调用资源API:
@Controller
public class UserController {
@RequestMapping("/api/userinfo")
public ResponseEntity<UserInfo> getUserInfo() {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String email = user.getUsername() + "@spring2go.com";
UserInfo userInfo = new UserInfo();
userInfo.setName(user.getUsername());
userInfo.setEmail(email);
return ResponseEntity.ok(userInfo);
}
}
实验结果:
通过调用资源API,获取到了用户名和邮箱信息
3.2 令牌转换
- Gateway 验证访问令牌
- Gateway 根据客户端发送的访问令牌获取的用户详细信息生成JWT
- JWT将使用RSA算法和私钥进行签名,并将其放在HTTP头中发送
- Gateway与Providers sidecar共享公钥,以进行JWT签名验证
3.3. 服务调用
- Gateway进行路由、负载均衡以及限流
- Provider sidecar将从Gateway获得公钥并用于JWT签名验证
- 当事务完成时(成功或错误),网关将事务日志发送给日志系统,其中包含一些头的详细信息、运行时间、状态等
实验 - JWT 生成及验证
Java中对JWT的支持可以使用JJWT(实现了JOSE规范)开源库;
import依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt-api.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt-api.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-gson</artifactId>
<version>${jjwt-api.version}</version>
</dependency>
jwt的生成:
private static final String JWT_SECRET = "test_jwt_secret_test_jwt_secret_test_jwt_secret";
public static SecretKey generalKey() {
byte[] encodeKey = JWT_SECRET.getBytes(StandardCharsets.UTF_8);
//这里只是简单的实验,生产中要使用非对称加密~
SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
return key;
}
/**
* 签发JWT, 创建token的方法
*
* @param id jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
* @param iss jwt签发者
* @param subject jwt所面向的用户, payload中记录的public claims. 即为JWTSubject的信息
* @param ttlMillis 有效期,单位毫秒
* @return token
*/
public static String createJWT(String id, String iss, String subject, long ttlMillis) {
long currentMillis = System.currentTimeMillis();
Date now = new Date();
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.serializeToJsonWith(new GsonSerializer<>(new Gson()))
.setId(id)//身份标识
.setIssuer(iss)
.setSubject(subject)
.setIssuedAt(now)
.signWith(secretKey);
if (ttlMillis >= 0) {
long expMillis = currentMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate);
}
return builder.compact();
}
jwt的校验:
public static JWTResult validateJWT(String jwtStr) {
JWTResult checkResult = new JWTResult();
Claims claims;
try {
claims = parseJWT(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) {
checkResult.setErrCode(JWT_ERR_CODE_EXPIRE);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(JWT_ERR_CODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}
public static Claims parseJWT(String jwt) {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();//token中记录的payload数据
}
4. 安全问题
- 只允许特定身份的客户端访问,令牌请求启用Mutual TLS认证,请求需要同时确认服务端和调用者的身份。
- 为了避免重放攻击,在token中加入时间戳,保证token快速过期,过期后采用refresh_token刷新获取新的token。
- 采用公钥、私钥非对称加密,分布式场景下,建议选择 RS256 。
- 避免敏感信息保存在 JWT 中,JWS 方式下的 JWT 的 Payload 信息是公开的,不能将敏感信息保存在这里,如有需要,请使用 JWE 。