赞
踩
在年初的时候我参与了一个项目,当时是很多家公司协同完成这个项目,其中一个公司专门负责登录这块的内容,需要我们的后端接入他们的单点登录(OAuth2 授权码模式),这块接入工作是由我来负责,我们的项目是微服务架构,经过网上各种查阅资料发现网关作为OAuth2 Client接入单点登录,将用户信息解析传递给下游微服务是最佳方案,在本文中我将详细讲解怎么基于Spring Cloud Gateway 接入第三方单点登录。
- 如文章中有明显错误或者用词不当的地方,欢迎大家在评论区批评指正,我看到后会及时修改。
- 如想要和博主进行技术栈方面的讨论和交流可私信我。
目录
4.1.4. 重写DefaultServerOAuth2AuthorizationRequestResolver
4.1.8. 编写ReactiveOAuth2UserService实现类
Spring Cloud Gateway是Spring Cloud生态系统中的一个组件,主要用于构建微服务架构中的网关服务。它提供了一种灵活而强大的方式来路由请求、过滤请求以及添加各种功能,如负载均衡、熔断、安全性等。通过将Spring Cloud Gateway作为OAuth2 Client,可以实现用户在系统中的统一认证体验。用户只需要一次登录,即可访问多个微服务,避免了在每个服务中都进行独立的认证,下游微服务只需要专注自己的业务代码即可。
让我们来先看一下基于网关集成单点登录的流程图(OAuth2授权码模式),我这边只是一个大致流程,想要看完整细致流程的同学可以去看一下大佬写的这篇文章:Spring Cloud Gateway作为OAuth2 Client_oauth2客户端接口为什么跳转到login_罗小爬EX的博客-CSDN博客
基于Spring Cloud Gateway作为OAuth2 Client接入单点登录的项目结构如下图所示:
由上图可以看出这个项目(demo)是微服务组织架构,这里我只创建了两个moudle(父模块不算)即网关和资源服务器。
依赖 | 版本 |
---|---|
Spring Boot | 2.6.3 |
Spring Cloud Alibaba | 2021.0.1.0 |
Spring Cloud | 2021.0.1 |
java | 1.8 |
redis | 6.2 |
1. 父模块依赖
- <properties>
- <maven.compiler.source>8</maven.compiler.source>
- <maven.compiler.target>8</maven.compiler.target>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- <java.version>1.8</java.version>
- <spring-cloud.version>2021.0.1</spring-cloud.version>
- <cloud-alibaba.version>2021.0.1.0</cloud-alibaba.version>
- </properties>
- <dependencyManagement>
- <dependencies>
- <!-- springCloud -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-dependencies</artifactId>
- <version>${spring-cloud.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-alibaba-dependencies</artifactId>
- <version>${cloud-alibaba.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- </dependencies>
- </dependencyManagement>
2. 网关模块依赖
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-gateway</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-oauth2-client</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.springframework.session</groupId>
- <artifactId>spring-session-data-redis</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-bootstrap</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-loadbalancer</artifactId>
- </dependency>
- </dependencies>
3. 资源服务器模块依赖
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-bootstrap</artifactId>
- </dependency>
- </dependencies>
- server:
- reactive:
- session:
- cookie:
- http-only: true
- port: 8888
- system:
- whiteList:
- - "/auth"
- - "/oauth2"
- - "/favicon.ico"
- - "/login"
- spring:
- cloud:
- gateway:
- routes:
- - id: geoscene-back-resource
- uri: http://127.0.0.1:8090
- predicates:
- - Path=/resource/**
- filters:
- - TokenRelay
- - UserInfoRelay
- session:
- store-type: redis # 会话存储类型
- redis:
- cleanup-cron: 0 * * * * *
- flush-mode: on_save # 会话刷新模式
- namespace: gateway:session # 用于存储会话的键的命名空间
- save-mode: on_set_attribute
- redis:
- host: localhost
- port: 6379
- # password: 123456
- security:
- filter:
- order: 5
- oauth2:
- client:
- registration:
- gas:
- provider: gas
- client-id: 在第三方授权中心获取的 client-id
- client-secret: 在第三方授权中心获取(自定义)的 client-secret
- redirect-uri: http://127.0.0.1:8888/login/oauth2/code/gas
- authorization-grant-type: authorization_code
- client-authentication-method: client_secret_basic
- scope: userinfo
- provider:
- gas:
- issuer-uri: 填写第三方认证地址
- #
- logging:
- level:
- root: INFO
- org.springframework.web: INFO
- org.springframework.security: INFO
- org.springframework.security.oauth2: INFO
- org.springframework.cloud.gateway: INFO
- @Configuration(proxyBeanMethods = false)
- @EnableWebFluxSecurity
- public class Oauth2ClientSecurityConfig {
- private String oauth2LoginEndpoint = "/login/oauth2/code/gas";
-
- @Bean
- public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver) {
- http
- .authorizeExchange(authorize -> authorize
- .pathMatchers("/auth/**", "/oauth2/**"
- ).permitAll()
- .anyExchange().authenticated()
- )
- .oauth2Login(oauth2Login -> oauth2Login
- // 发起 OAuth2 登录的地址(服务端)
- .authorizationRequestResolver(saveRequestServerOAuth2AuthorizationRequestResolver)
- // OAuth2 外部用户登录授权后的跳转地址(服务端)
- .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher(
- oauth2LoginEndpoint))
- )
- .cors().disable();
- return http.build();
- }
-
- /**
- * OAuth2 Client Authorization Endpoint /oauth2/authoriztion/{clientRegId}
- * 请求解析器扩展实现 - 支持提取query参数redirect_uri,用作后续OAuth2认证完成后网关重定向到该指定redirect_uri。
- * 适用场景:前端应用 -> 网关 -> 网关返回401 -> 前端应用重定向到/oauth2/authorization/{clientRegId}?redirect_uri=http://登录后界面 -> 网关完成OAuth2认证后再重定向回http://登录后界面
- */
- @Bean
- @Primary
- public ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {
- return new SaveRequestServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
- }
-
- /**
- * 自定义UserInfo过滤器工厂
- */
- @Bean
- public UserInfoRelayGatewayFilterFactory userInfoRelayGatewayFilterFactory() {
- return new UserInfoRelayGatewayFilterFactory();
- }
-
- }
- @Component
- @Order(Ordered.HIGHEST_PRECEDENCE)
- @Slf4j
- public class CustomWebFilter implements WebFilter {
- @Autowired
- private UrlConfig urlConfig;
- @Override
- public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
- // 请求对象
- ServerHttpRequest request = exchange.getRequest();
- // 响应对象
- ServerHttpResponse response = exchange.getResponse();
-
- return exchange.getSession().flatMap(webSession -> {
-
- for (int i = 0; i <urlConfig.getWhiteList().size() ; i++) {
- if (request.getURI().getPath().contains(urlConfig.getWhiteList().get(i))) {
- return chain.filter(exchange);
- }
- }
-
- if( webSession.getAttribute("SPRING_SECURITY_CONTEXT")==null||!((SecurityContext)webSession.getAttribute("SPRING_SECURITY_CONTEXT")).getAuthentication().isAuthenticated()){
- JSONObject message = new JSONObject();
- message.put("code", 401);
- message.put("status","fail");
- message.put("message", "缺少身份凭证");
- message.put("data", "http://127.0.0.1:8888/oauth2/authorization/gas");
- // 转换响应消息内容对象为字节
- byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
- DataBuffer buffer = response.bufferFactory().wrap(bits);
- // 设置响应对象状态码 401
- response.setStatusCode(HttpStatus.UNAUTHORIZED);
- // 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
- response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
- // 返回响应对象
- return response.writeWith( Mono.just(buffer) );
- }
- return chain.filter(exchange);
- }).then(Mono.fromRunnable(() -> {
- log.info("this is a post filter");
- }));
- }
- }
上述代码的主要功能为拦截进入网关的每一个请求,若没有身份凭证(令牌)则返回/oauth2/authorization/{clientRegId}。
- public class SaveRequestServerOAuth2AuthorizationRequestResolver extends DefaultServerOAuth2AuthorizationRequestResolver {
- private static final Log logger = LogFactory.getLog(SaveRequestServerOAuth2AuthorizationRequestResolver.class);
- /**
- * redirect uri参数名称
- */
- private static final String PARAM_REDIRECT_URI = "redirect_uri";
- /**
- * WebSession对应的saveRequest属性名
- * 完全沿用(兼容)WebSessionServerRequestCache定义
- */
- private static final String DEFAULT_SAVED_REQUEST_ATTR = "SPRING_SECURITY_SAVED_REQUEST";
- private String sessionAttrName = DEFAULT_SAVED_REQUEST_ATTR;
-
- /**
- * Creates a new instance
- *
- * @param clientRegistrationRepository the repository to resolve the
- * {@link ClientRegistration}
- */
- public SaveRequestServerOAuth2AuthorizationRequestResolver(
- ReactiveClientRegistrationRepository clientRegistrationRepository) {
- super(clientRegistrationRepository);
- }
- @Override
- public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange) {
-
- return super.resolve(exchange)
- .doOnNext(OAuth2AuthorizationRequest -> {
- // 获取query参数redirect_uri
- Optional.ofNullable(exchange.getRequest())
- .map(ServerHttpRequest::getQueryParams)
- .map(queryParams -> queryParams.get(PARAM_REDIRECT_URI))
- .filter(redirectUris -> !CollectionUtils.isEmpty(redirectUris))
- .map(redirectUris -> redirectUris.get(0))
- .ifPresent(redirectUri -> {
- //若redirect_uri非空,则覆盖Session中的SPRING_SECURITY_SAVED_REQUEST为redirect_uri
- //即后续认证成功后可重定向回前端指定页面
- exchange.getSession().subscribe(webSession -> {
- webSession.getAttributes().put(this.sessionAttrName, redirectUri);
- logger.debug(LogMessage.format("SCG OAuth2 authorization endpoint queryParam redirect_uri added to WebSession: '%s'", redirectUri));
- });
- });
- });
- }
-
- }
-
- public class CustomUser implements OAuth2User, Serializable {
- private Map<String, Object> attributes;
- private Collection<? extends GrantedAuthority> authorities;
- private String name;
-
- public CustomUser(Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities, String name) {
- this.attributes = attributes;
- this.authorities = authorities;
- this.name = name;
- }
-
- public CustomUser() {
- }
-
- @Override
- public Map<String, Object> getAttributes() {
- return attributes;
- }
-
- @Override
- public Collection<? extends GrantedAuthority> getAuthorities() {
- return authorities;
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- public void setAttributes(Map<String, Object> attributes) {
- this.attributes = attributes;
- }
-
- public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
- this.authorities = authorities;
- }
-
- public void setName(String name) {
- this.name = name;
- }
- }
- @Configuration
- @ConfigurationProperties(prefix = "system")
- public class UrlConfig {
- // 配置文件使用list接收
- private List<String> whiteList;
- public List<String> getWhiteList() {
- return whiteList;
- }
- public void setWhiteList(List<String> whiteList) {
- this.whiteList = whiteList;
- }
- }
- public class UserInfoRelayGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
-
- private final static String USER_INFO_HEADER = "userInfo";
-
- public UserInfoRelayGatewayFilterFactory() {
- super(Object.class);
- }
-
- public GatewayFilter apply() {
- return apply((Object) null);
- }
-
- @Override
- public GatewayFilter apply(Object config) {
- return (exchange, chain) -> exchange.getPrincipal()
- // .log("token-relay-filter")
- .filter(principal -> principal instanceof OAuth2AuthenticationToken)
- .cast(OAuth2AuthenticationToken.class)
- //.flatMap(authentication -> authorizedClient(exchange, authentication))
- .map(OAuth2AuthenticationToken::getPrincipal)
- .map(oAuth2User -> withUserInfoHeader(exchange, oAuth2User))
- .defaultIfEmpty(exchange)
- .flatMap(chain::filter);
- }
-
- private ServerWebExchange withUserInfoHeader(ServerWebExchange exchange, OAuth2User oAuth2User) {
- //String userName = oAuth2User.getName();
- Map<String, Object> userAttrs = oAuth2User.getAttributes();
- if (oAuth2User instanceof OidcUser) {
- userAttrs = ((OidcUser) oAuth2User).getUserInfo().getClaims();
- }
- String userAttrsJson = JsonUtils.toJson(userAttrs);
- return exchange.mutate()
- .request(r -> r.headers(headers -> headers.add(USER_INFO_HEADER, userAttrsJson)))
- .build();
- }
-
- }
- @Component
- public class CustomOAuth2UserService implements ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> {
-
- private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
-
- private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
-
- private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
-
- private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<Map<String, Object>>() {
- };
-
- private static final ParameterizedTypeReference<Map<String, String>> STRING_STRING_MAP = new ParameterizedTypeReference<Map<String, String>>() {
- };
-
- private WebClient webClient = WebClient.create();
-
- @Override
- public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
- return Mono.fromCallable(() -> {
- String tokenStr = userRequest.getAccessToken().getTokenValue();
- try {
- SignedJWT sjwt = SignedJWT.parse(tokenStr);
- JWTClaimsSet claims = sjwt.getJWTClaimsSet();
- claims.getSubject();
- Collection<? extends GrantedAuthority> res = new ArrayList<>();
- CustomUser customUser=new CustomUser( claims.getClaims(),res,claims.getSubject());
- return customUser;
-
- } catch (ParseException e) {
- e.printStackTrace();
- throw new OAuth2AuthenticationException(new OAuth2Error("500"),"服务器返回错误的jwt");
- }
- });
- }
-
- }
- server:
- port: 8090
- servlet:
- context-path: /resource
- @RestController
- public class ArticleController {
- @GetMapping("/user-info")
- public String getUserName( @RequestHeader String userInfo){
- return userInfo;
- }
- }
1. 直接访问资源服务器接口
由上图可看出无法直接访问资源服务器接口,前端接收到此返回信息后根据data中返回的路径加上redirect_uri(http://127.0.0.1:8888/oauth2/authorization/gas?redirect_uri=http://www.baidu.com),发送页面请求后可跳转至登录中心,认证成功后界面会重定向至redirect_uri所指定的界面(我这里写的百度)。
跳转至登录界面进行认证。
认证成功后重定向至redirect_uri所指定的界面(百度)。
2. 再次访问资源服务器接口
访问接口成功。
Spring Cloud Gateway作为OAuth2 Client_oauth2客户端接口为什么跳转到login_罗小爬EX的博客-CSDN博客
将Spring Cloud Gateway 与OAuth2模式一起使用_jwk-set-uri_ReLive27的博客-CSDN博客
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。