之前虽然单独讲过Security Client和Resource Server的对接,但是都是基于Spring webmvc的,Gateway这种非阻塞式的网关是基于webflux的,对于集成Security相关内容略有不同,且涉及到代理其它微服务,所以会稍微比较麻烦些,今天就带大家来实现Gateway网关对接OAuth2认证服务。
在本次示例中网关既是客户端(OAuth2 Client Server)又是资源服务(OAuth2 Resource Server),Client服务负责认证,Resource负责鉴权,这样如果有在浏览器直接访问网关的需要可以直接在浏览器由框架引导完成OAuth2认证过程。
框架 | 版本号 |
---|---|
Spring Boot | 3.1.0 |
Nacos Server | 2.2.1 |
Spring Cloud | 2022.0.4 |
Spring Cloud Alibaba | 2022.0.0.0 |
Spring Security | 6.1.0 |
Spring OAuth2 Client | 6.1.0 |
Spring OAuth2 Resource Server | 6.1.0 |
读者可以自选版本使用,作为对接方版本问题不大;不确定Spring Cloud Alibaba 在部署时会不会有Spring Boot的版本限制,如果3.1.x无法使用请降级至3.0.10版本,开发时测试都是没问题的。
这就是网关通过认证服务获取认证信息的一个流程,基本上只需要添加配置文件即可由框架引导进行OAuth2认证流程。
gateway-example # 父模块 │ ├─gateway-client-example # 网关 │ ├─normal-resource-example # webmvc资源服务 │ ├─webflux-resource-example # webflux资源服务 │ └─pom.xml # 公共依赖,依赖管理
引入Spring Boot、Spring Cloud、Spring Cloud Alibaba,如下
4.0.0 org.springframework.boot spring-boot-starter-parent 3.1.0 com.example gateway-example 0.0.1 pom gateway-example gateway-example gateway-client-example normal-resource-example webflux-resource-example 17 2.0 2022.0.4 2022.0.0.0 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring-cloud-alibaba.version} pom import
里边的modules标签是在新建module时自动添加的
Spring Cloud 相关依赖已经在parent模块中引入,所以该模块只需要引入Gateway、Client依赖,pom如下
4.0.0 com.example gateway-example 0.0.1 gateway-client-example gateway-client-example gateway-client-example org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-webflux org.springframework.cloud spring-cloud-starter-gateway io.projectreactor reactor-test test org.springframework.cloud spring-cloud-starter-loadbalancer org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import java.util.Collection; import java.util.HashSet; import java.util.Set; /** * 客户端配置 * * @author vains */ @Configuration public class ClientServerConfig { /** * 解析用户权限信息(当在浏览器中直接访问接口,框架自动调用OIDC流程登录时会用到该配置) * * @return GrantedAuthoritiesMapper */ @Bean public GrantedAuthoritiesMapper userAuthoritiesMapper() { return (authorities) -> { SetmappedAuthorities = new HashSet<>(); authorities.forEach(authority -> { if (authority instanceof OAuth2UserAuthority oAuth2UserAuthority) { // 从认证服务获取的用户信息中提取权限信息 Object userAuthorities = oAuth2UserAuthority.getAttributes().get("authorities"); if (userAuthorities instanceof Collection> collection) { // 转为SimpleGrantedAuthority的实例并插入mappedAuthorities中 collection.stream().filter(a -> a instanceof String) .map(String::valueOf) .map(SimpleGrantedAuthority::new) .forEach(mappedAuthorities::add); } } }); return mappedAuthorities; }; } }
该配置会在获取到用户信息后解析用户的权限信息,详见文档
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.web.server.SecurityWebFilterChain; import reactor.core.publisher.Mono; /** * 资源服务器配置 * * @author vains */ @Configuration @EnableWebFluxSecurity @EnableReactiveMethodSecurity public class ResourceServerConfig { /** * 配置认证相关的过滤器链 * * @param http Spring Security的核心配置类 * @return 过滤器链 */ @Bean public SecurityWebFilterChain defaultSecurityFilterChain(ServerHttpSecurity http) { // 禁用csrf与cors http.csrf(ServerHttpSecurity.CsrfSpec::disable); http.cors(ServerHttpSecurity.CorsSpec::disable); // 开启全局验证 http.authorizeExchange((authorize) -> authorize //全部需要认证 .anyExchange().authenticated() ); // 开启OAuth2登录 http.oauth2Login(Customizer.withDefaults()); // 设置当前服务为资源服务,解析请求头中的token http.oauth2ResourceServer((resourceServer) -> resourceServer // 使用jwt .jwt(jwt -> jwt // 请求中携带token访问时会触发该解析器适配器 .jwtAuthenticationConverter(grantedAuthoritiesExtractor()) ) /* // xhr请求未携带Token处理 .authenticationEntryPoint(this::authenticationEntryPoint) // 权限不足处理 .accessDeniedHandler(this::accessDeniedHandler) // Token解析失败处理 .authenticationFailureHandler(this::failureHandler) */ ); return http.build(); } /** * 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key * * @return jwt解析器适配器 ReactiveJwtAuthenticationConverterAdapter */ public Converter> grantedAuthoritiesExtractor() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 设置解析权限信息的前缀,设置为空是去掉前缀 grantedAuthoritiesConverter.setAuthorityPrefix(""); // 设置权限信息在jwt claims中的key grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); } }
需要注意的是开启方法级别鉴权的注解变了,webflux的注解和webmvc的注解不一样,并且过滤器链也换成SecurityWebFilterChain了;jwt自定义解析器的方式也不一致,实现方式见上方代码
说明文档如下
EnableReactiveMethodSecurity注解文档
Jwt解析器适配器详见文档
spring: cloud: nacos: serverAddr: 127.0.0.1:8848 config: import: - nacos:gateway.yml?refresh=true application: name: gateway
添加客户端与资源服务配置,并添加其它资源服务的代理配置
server: port: 7000 spring: security: oauth2: # 资源服务器配置 resourceserver: jwt: # Jwt中claims的iss属性,也就是jwt的签发地址,即认证服务器的根路径 # 资源服务器会进一步的配置,通过该地址获取公钥以解析jwt issuer-uri: http://192.168.119.1:8080 client: provider: # 认证提供者,自定义名称 custom-issuer: # Token签发地址(认证服务地址) issuer-uri: http://192.168.119.1:8080 # 获取用户信息的地址,默认的/userinfo端点需要IdToken获取,为避免麻烦自定一个用户信息接口 user-info-uri: ${spring.security.oauth2.client.provider.custom-issuer.issuer-uri}/user user-name-attribute: name registration: messaging-client-oidc: # oauth认证提供者配置,和上边配置的认证提供者关联起来 provider: custom-issuer # 客户端名称,自定义 client-name: gateway # 客户端id,从认证服务申请的客户端id client-id: messaging-client # 客户端秘钥 client-secret: 123456 # 客户端认证方式 client-authentication-method: client_secret_basic # 获取Token使用的授权流程 authorization-grant-type: authorization_code # 回调地址,这里设置为Spring Security Client默认实现使用code换取token的接口,当前服务(gateway网关)的地址 redirect-uri: http://127.0.0.1:7000/login/oauth2/code/messaging-client-oidc scope: - message.read - message.write - openid - profile cloud: gateway: default-filters: # 令牌中继 - TokenRelay= # 代理路径,代理至服务后会去除第一个路径的内容 - StripPrefix=1 routes: # 资源服务代理配置 - id: resource uri: lb://resource predicates: - Path=/resource/** # 资源服务代理配置 - id: webflux uri: lb://webflux-resource predicates: - Path=/webflux/**
注意:配置文件中令牌中继(TokenRelay)的配置就是添加一个filter:TokenRelay=; 当网关引入spring-boot-starter-oauth2-client依赖并设置spring.security.oauth2.client.*属性时,会自动创建一个TokenRelayGatewayFilterFactory过滤器,它会从认证信息中获取access token,并放入下游请求的请求头中。 详见Gateway关于TokenRelay的文档
在pom.xml中添加web依赖,如下
4.0.0 com.example gateway-example 0.0.1 normal-resource-example normal-resource-example normal-resource-example org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; /** * 资源服务器配置 * * @author vains */ @Configuration @EnableWebSecurity @EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true) public class ResourceServerConfig { /** * 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key * * @return jwt解析器 JwtAuthenticationConverter */ @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 设置解析权限信息的前缀,设置为空是去掉前缀 grantedAuthoritiesConverter.setAuthorityPrefix(""); // 设置权限信息在jwt claims中的key grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } }
spring: cloud: nacos: serverAddr: 127.0.0.1:8848 config: import: - nacos:resource.yml?refresh=true application: name: resource
server: port: 7100 spring: security: oauth2: # 资源服务器配置 resourceserver: jwt: # Jwt中claims的iss属性,也就是jwt的签发地址,即认证服务器的根路径 # 资源服务器会进一步的配置,通过该地址获取公钥以解析jwt issuer-uri: http://192.168.119.1:8080
注意端口,不能与网关和认证服务重复
pom.xml添加webflux依赖,如下
4.0.0 com.example gateway-example 0.0.1 webflux-resource-example webflux-resource-example webflux-resource-example org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
跟网关的资源服务配置差不多,如下
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.web.server.SecurityWebFilterChain; import reactor.core.publisher.Mono; /** * 资源服务器配置 * * @author vains */ @Configuration @EnableWebFluxSecurity @EnableReactiveMethodSecurity public class ResourceServerConfig { /** * 配置认证相关的过滤器链 * * @param http Spring Security的核心配置类 * @return 过滤器链 */ @Bean public SecurityWebFilterChain defaultSecurityFilterChain(ServerHttpSecurity http) { // 禁用csrf与cors http.csrf(ServerHttpSecurity.CsrfSpec::disable); http.cors(ServerHttpSecurity.CorsSpec::disable); // 开启全局验证 http.authorizeExchange((authorize) -> authorize //全部需要认证 .anyExchange().authenticated() ); // 设置当前服务为资源服务,解析请求头中的token http.oauth2ResourceServer((resourceServer) -> resourceServer // 使用jwt .jwt(jwtSpec -> jwtSpec // 设置jwt解析器适配器 .jwtAuthenticationConverter(grantedAuthoritiesExtractor()) ) ); return http.build(); } /** * 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key * * @return jwt解析器适配器 ReactiveJwtAuthenticationConverterAdapter */ public Converter> grantedAuthoritiesExtractor() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 设置解析权限信息的前缀,设置为空是去掉前缀 grantedAuthoritiesConverter.setAuthorityPrefix(""); // 设置权限信息在jwt claims中的key grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); } }
spring: cloud: nacos: serverAddr: 127.0.0.1:8848 config: import: - nacos:webflux.yml?refresh=true application: name: webflux-resource
server: port: 7200 spring: security: oauth2: # 资源服务器配置 resourceserver: jwt: # Jwt中claims的iss属性,也就是jwt的签发地址,即认证服务器的根路径 # 资源服务器会进一步的配置,通过该地址获取公钥以解析jwt issuer-uri: http://192.168.119.1:8080
与webmvc的资源服务的配置是一样的,注意端口不能与其它服务端口冲突
package com.example.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * 测试接口 * * @author vains */ @RestController public class TestController { @GetMapping("/test01") @PreAuthorize("hasAnyAuthority('message.write')") public String test01() { return "test01"; } @GetMapping("/test02") @PreAuthorize("hasAnyAuthority('test02')") public String test02() { return "test02"; } @GetMapping("/app") @PreAuthorize("hasAnyAuthority('app')") public String app() { return "app"; } }
package com.example.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; /** * 测试接口 * * @author vains */ @RestController public class TestController { @GetMapping("/test01") @PreAuthorize("hasAnyAuthority('message.write')") public Monotest01() { return Mono.just("test01"); } @GetMapping("/test02") @PreAuthorize("hasAnyAuthority('test02')") public Mono test02() { return Mono.just("test02"); } @GetMapping("/app") @PreAuthorize("hasAnyAuthority('app')") public Mono app() { return Mono.just("app"); } }
依次启动三个服务,顺序无所谓
被重定向至登录了,携带X-Requested-With请求头访问,代表当前是xhr请求
响应401,框架有区分是浏览器请求还是xhr请求,对于浏览器请求会重定向到页面,对于xhr请求默认会响应401状态码,可自己实现异常处理,这里错误信息在请求头中是因为没有重写异常处理,网关资源服务配置代码中有注释。
浏览器打开地址:http://127.0.0.1:7000/resource/app
请求到达网关后检测到未登录会引导用户进行OAuth2认证流程
登录提交后认证服务重定向授权申请接口,校验通过后会生成code并携带code重定向至回调地址,注意,这里的回调地址是网关的服务地址,由网关中的OAuth2 Client处理,如图
网关会根据code换取token,获取token后根据token获取用户信息,并调用网关客户端配置中自定义的userAuthoritiesMapper解析权限信息。
响应403,并将错误信息放入响应头中
响应401并在响应头中提示token已过期
响应401并在响应头中提示token无法解析
响应403并提示权限不足
响应200并正确响应接口信息
本文带大家简单实现了Spring Cloud Gateway对接认证服务,Gateway中添加客户端主要是为了如果代理服务有静态资源(html、css、image)时可以直接发起OAuth2授权流程,在浏览器登录后直接访问,同时也是开启令牌中继的必要依赖;引入Resource Server依赖是当需要对网关的接口鉴权时可以直接使用,如果网关只负责转发应该是可以去掉资源服务相关依赖和配置的,由各个被代理的微服务对自己的接口进行鉴权。这些东西在之前基本都是讲过的内容,所以本文很多地方都是一笔带过的,如果某些地方不清楚可以针对性的翻翻之前的文章,也可以在评论区中提出。
如果有什么问题或者需要补充的请在评论区指出,谢谢。
Gitee仓库地址
Gateway令牌中继文档
OAuth2登录后用户权限解析文档
webflux开启方法鉴权EnableReactiveMethodSecurity注解说明文档
webflux的Jwt解析器适配器说明文档
webflux对接OAuth2 Client文档
webflux对接OAuth2 Resource Server文档