SpringSecurityOauth2+Gateway 鉴权

在微服务架构中,如果把鉴权放在各服务中将会导致代码冗余且增加工作量。

如果在网关鉴权,不仅能保证所有接口有统一的鉴权标准,也能避免各服务鉴权不一致导致的漏洞。

下面记录一下用 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 进行缓存,结合负载均衡进一步提高性能。

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇