在年初的时候我参与了一个项目,当时是很多家公司协同完成这个项目,其中一个公司专门负责登录这块的内容,需要我们的后端接入他们的单点登录(OAuth2 授权码模式),这块接入工作是由我来负责,我们的项目是微服务架构,经过网上各种查阅资料发现网关作为OAuth2 Client接入单点登录,将用户信息解析传递给下游微服务是最佳方案,在本文中我将详细讲解怎么基于Spring Cloud Gateway 接入第三方单点登录。

如文章中有明显错误或者用词不当的地方,欢迎大家在评论区批评指正,我看到后会及时修改。如想要和博主进行技术栈方面的讨论和交流可私信我。

目录

1. 前言

2. 流程图

3. 开发环境搭建

3.1. 项目结构

3.2. 所用版本工具

3.3. pom依赖

4. 核心代码

4.1. 网关模块核心代码

4.1.1. 编写网关yml配置

4.1.2. 编写Security授权配置主文件

4.1.3. 编写认证过滤器

4.1.4. 重写DefaultServerOAuth2AuthorizationRequestResolver

 4.1.5. 编写OAuth2User实现类

 4.1.6. 编写url白名单配置类

4.1.7.  编写userInfo过滤器

 4.1.8. 编写ReactiveOAuth2UserService实现类

4.2. 资源服务器核心代码

4.2.1. 编写资源服务器yml

4.2.2. 编写资源服务器测试controller

5. 登录测试

6. 参考链接

1. 前言

        Spring Cloud Gateway是Spring Cloud生态系统中的一个组件,主要用于构建微服务架构中的网关服务。它提供了一种灵活而强大的方式来路由请求、过滤请求以及添加各种功能,如负载均衡、熔断、安全性等。通过将Spring Cloud Gateway作为OAuth2 Client,可以实现用户在系统中的统一认证体验。用户只需要一次登录,即可访问多个微服务,避免了在每个服务中都进行独立的认证,下游微服务只需要专注自己的业务代码即可。

2. 流程图

        让我们来先看一下基于网关集成单点登录的流程图(OAuth2授权码模式),我这边只是一个大致流程,想要看完整细致流程的同学可以去看一下大佬写的这篇文章:Spring Cloud Gateway作为OAuth2 Client_oauth2客户端接口为什么跳转到login_罗小爬EX的博客-CSDN博客

3. 开发环境搭建

3.1. 项目结构

基于Spring Cloud Gateway作为OAuth2 Client接入单点登录的项目结构如下图所示:

由上图可以看出这个项目(demo)是微服务组织架构,这里我只创建了两个moudle(父模块不算)即网关和资源服务器。

3.2. 所用版本工具

依赖版本Spring Boot2.6.3 Spring Cloud Alibaba 2021.0.1.0Spring Cloud 2021.0.1java1.8redis6.2

3.3. pom依赖

1. 父模块依赖

8

8

UTF-8

UTF-8

1.8

2021.0.1

2021.0.1.0

org.springframework.cloud

spring-cloud-dependencies

${spring-cloud.version}

pom

import

com.alibaba.cloud

spring-cloud-alibaba-dependencies

${cloud-alibaba.version}

pom

import

2.  网关模块依赖

org.springframework.cloud

spring-cloud-starter-gateway

org.springframework.boot

spring-boot-starter-oauth2-client

org.springframework.boot

spring-boot-starter-data-redis

org.springframework.session

spring-session-data-redis

org.springframework.cloud

spring-cloud-starter-bootstrap

org.projectlombok

lombok

org.springframework.cloud

spring-cloud-starter-loadbalancer

3. 资源服务器模块依赖

org.springframework.boot

spring-boot-starter-web

org.springframework.cloud

spring-cloud-starter-bootstrap

4. 核心代码

4.1. 网关模块核心代码

4.1.1. 编写网关yml配置

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

4.1.2. 编写Security授权配置主文件

@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();

}

}

4.1.3. 编写认证过滤器

@Component

@Order(Ordered.HIGHEST_PRECEDENCE)

@Slf4j

public class CustomWebFilter implements WebFilter {

@Autowired

private UrlConfig urlConfig;

@Override

public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {

// 请求对象

ServerHttpRequest request = exchange.getRequest();

// 响应对象

ServerHttpResponse response = exchange.getResponse();

return exchange.getSession().flatMap(webSession -> {

for (int i = 0; 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}。

4.1.4. 重写DefaultServerOAuth2AuthorizationRequestResolver

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 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));

});

});

});

}

}

 4.1.5. 编写OAuth2User实现类

public class CustomUser implements OAuth2User, Serializable {

private Map attributes;

private Collection authorities;

private String name;

public CustomUser(Map attributes, Collection authorities, String name) {

this.attributes = attributes;

this.authorities = authorities;

this.name = name;

}

public CustomUser() {

}

@Override

public Map getAttributes() {

return attributes;

}

@Override

public Collection getAuthorities() {

return authorities;

}

@Override

public String getName() {

return name;

}

public void setAttributes(Map attributes) {

this.attributes = attributes;

}

public void setAuthorities(Collection authorities) {

this.authorities = authorities;

}

public void setName(String name) {

this.name = name;

}

}

 4.1.6. 编写url白名单配置类

@Configuration

@ConfigurationProperties(prefix = "system")

public class UrlConfig {

// 配置文件使用list接收

private List whiteList;

public List getWhiteList() {

return whiteList;

}

public void setWhiteList(List whiteList) {

this.whiteList = whiteList;

}

}

4.1.7.  编写userInfo过滤器

public class UserInfoRelayGatewayFilterFactory extends AbstractGatewayFilterFactory {

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 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();

}

}

 4.1.8. 编写ReactiveOAuth2UserService实现类

@Component

public class CustomOAuth2UserService implements ReactiveOAuth2UserService {

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> STRING_OBJECT_MAP = new ParameterizedTypeReference>() {

};

private static final ParameterizedTypeReference> STRING_STRING_MAP = new ParameterizedTypeReference>() {

};

private WebClient webClient = WebClient.create();

@Override

public Mono 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 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");

}

});

}

}

4.2. 资源服务器核心代码

4.2.1. 编写资源服务器yml

server:

port: 8090

servlet:

context-path: /resource

4.2.2. 编写资源服务器测试controller

@RestController

public class ArticleController {

@GetMapping("/user-info")

public String getUserName( @RequestHeader String userInfo){

return userInfo;

}

}

5. 登录测试

1. 直接访问资源服务器接口

由上图可看出无法直接访问资源服务器接口,前端接收到此返回信息后根据data中返回的路径加上redirect_uri(http://127.0.0.1:8888/oauth2/authorization/gas?redirect_uri=http://www.baidu.com),发送页面请求后可跳转至登录中心,认证成功后界面会重定向至redirect_uri所指定的界面(我这里写的百度)。

跳转至登录界面进行认证。

认证成功后重定向至redirect_uri所指定的界面(百度)。

2. 再次访问资源服务器接口

访问接口成功。

6. 参考链接

Spring Cloud Gateway作为OAuth2 Client_oauth2客户端接口为什么跳转到login_罗小爬EX的博客-CSDN博客

将Spring Cloud Gateway 与OAuth2模式一起使用_jwk-set-uri_ReLive27的博客-CSDN博客

第15章 Spring Security OAuth2 初始_authorizeexchange-CSDN博客

精彩链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: 

发表评论

返回顶部暗黑模式