在年初的时候我参与了一个项目,当时是很多家公司协同完成这个项目,其中一个公司专门负责登录这块的内容,需要我们的后端接入他们的单点登录(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 Boot | 2.6.3 |
Spring Cloud Alibaba |
2021.0.1.0 |
Spring Cloud | 2021.0.1 |
java | 1.8 |
redis | 6.2 |
3.3. pom依赖
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>
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<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}。
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<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));
});
});
});
}
}
4.1.5. 编写OAuth2User实现类
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;
}
}
4.1.6. 编写url白名单配置类
@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;
}
}
4.1.7. 编写userInfo过滤器
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();
}
}
4.1.8. 编写ReactiveOAuth2UserService实现类
@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");
}
});
}
}
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博客文章来源:https://www.toymoban.com/news/detail-766846.html
第15章 Spring Security OAuth2 初始_authorizeexchange-CSDN博客文章来源地址https://www.toymoban.com/news/detail-766846.html
到了这里,关于【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!