在微服务架构中,如果把鉴权放在各服务中将会导致代码冗余且增加工作量。
如果在网关鉴权,不仅能保证所有接口有统一的鉴权标准,也能避免各服务鉴权不一致导致的漏洞。
下面记录一下用 Spring Security Oauth2 + Spring Cloud Gateway 鉴权的方法。
基本思路
微服务请求通过网关时,由网关解析用户携带的 token,提取出关键信息,整合到 Headers 中再提交给微服务。
下面的例子仅用到了用户ID与用户名,如有需要可以自行拓展。
具体实现
依赖引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.3</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</exclusion>
</exclusions>
</dependency>
这里如果直接引入 spring-security-oauth2-jose
会一起把 spring-security
引入,这样就导致网关服务被 security 默认开启的请求必须经过验证影响,因此一定要用 exclusions 标签排除。
JWT解析器
@Configuration
class JWTConfig(private val remoteAuthorizationService: RemoteAuthorizationService) {
@Bean
fun jwkSource(): ImmutableJWKSet<SecurityContext> {
return ImmutableJWKSet(JWKSet.parse(remoteAuthorizationService.getJWKSetString()))
}
@Bean
fun jwtDecoder(jwkSource: ImmutableJWKSet<SecurityContext>): JwtDecoder {
val jwsAlgs= mutableSetOf<JWSAlgorithm>()
jwsAlgs.addAll(JWSAlgorithm.Family.RSA)
jwsAlgs.addAll(JWSAlgorithm.Family.EC)
jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA)
val jwtProcessor = DefaultJWTProcessor<SecurityContext>()
val jwsKeySelector: JWSKeySelector<SecurityContext> = JWSVerificationKeySelector(jwsAlgs, jwkSource)
jwtProcessor.jwsKeySelector = jwsKeySelector
// Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead
jwtProcessor.setJWTClaimsSetVerifier { _: JWTClaimsSet?, _: SecurityContext? -> }
return NimbusJwtDecoder(jwtProcessor)
}
}
由于网关服务和认证服务是分离的,因此这里将通过 RPC 调用的方式获取到认证服务的 JWKSet,如果你用的是其他方式生成的 token,这里换成对应的解析器即可。
上面的 jwtDecoder 等效于认证服务中的 OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource)
,但是网关服务最好不要引入过多的依赖,所以这里只是将 OAuth2AuthorizationServerConfiguration
中的代码一模一样抄过来了。
网关过滤器
接下来需要写一个自定义的过滤器来拦截请求,解析其中的 token 并将用户信息提取出来。
@Component
class JwtAuthenticationFilter(private val jwtDecoder: JwtDecoder) : AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config>(Config::class.java) {
private val logger = logger()
override fun apply(config: Config): GatewayFilter {
return GatewayFilter { exchange, chain ->
val request = exchange.request
val cookies = request.cookies
val token = cookies.getFirst("ticket")
var authorized = false
var userId = ""
var username = ""
val mutatedRequest = if (token != null) {
try {
val jwtClaims = jwtDecoder.decode(token.value).claims
userId = jwtClaims["userId"].toString()
username = jwtClaims["username"].toString()
authorized = true
exchange.request.mutate()
.header("X-User-Id", userId)
.header("X-Username", username)
.build()
} catch (e: Exception) {
logger.info("Could not parse jwt token: ${token.value}, exception: ${e.message}")
request
}
} else {
// Original
request
}
chain.filter(exchange.mutate().request(mutatedRequest).build())
}
}
class Config {}
}
这里最关键的部分就是通过 exchange.request.mutate()
创建一个可变请求,并且将提取出来的用户信息放入请求头,最后将这个新的 request 发送到微服务。
微服务
在各微服务中,从 Headers 中获取用户信息,可以通过 @RequestHeader
直接获取,也可以自己封装一个对象,用 ArgumentResolver
注入,下面记录一下后者的实现方法。
先写一个数据类保存用户信息:
data class AuthPrincipal(
val userId: Long,
val username: String
)
再写一个自定义的函数参数解析器:
class AuthPrincipalArgumentResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.parameterType == AuthPrincipal::class.java
}
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): Any? {
return AuthPrincipal(
userId = webRequest.getHeader("X-User-Id")?.toLong() ?: -1,
username = webRequest.getHeader("X-Username") ?: ""
)
}
}
别忘了添加到 Spring MVC 中:
@Configuration
class SpringMvcConfig : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(AuthPrincipalArgumentResolver())
}
}
总结
网关鉴权不仅能解决鉴权方式不一致的问题,也能提高微服务的性能,避免在每个服务中进行重复的 token 解析。
同时网关层也能对 token 进行缓存,结合负载均衡进一步提高性能。