? ? ? ? 在年初的時(shí)候我參與了一個(gè)項(xiàng)目,當(dāng)時(shí)是很多家公司協(xié)同完成這個(gè)項(xiàng)目,其中一個(gè)公司專門負(fù)責(zé)登錄這塊的內(nèi)容,需要我們的后端接入他們的單點(diǎn)登錄(OAuth2 授權(quán)碼模式),這塊接入工作是由我來負(fù)責(zé),我們的項(xiàng)目是微服務(wù)架構(gòu),經(jīng)過網(wǎng)上各種查閱資料發(fā)現(xiàn)網(wǎng)關(guān)作為OAuth2 Client接入單點(diǎn)登錄,將用戶信息解析傳遞給下游微服務(wù)是最佳方案,在本文中我將詳細(xì)講解怎么基于Spring Cloud Gateway 接入第三方單點(diǎn)登錄。
- 如文章中有明顯錯(cuò)誤或者用詞不當(dāng)?shù)牡胤?,歡迎大家在評論區(qū)批評指正,我看到后會(huì)及時(shí)修改。
- 如想要和博主進(jìn)行技術(shù)棧方面的討論和交流可私信我。
目錄
1. 前言
2. 流程圖
3. 開發(fā)環(huán)境搭建
3.1. 項(xiàng)目結(jié)構(gòu)
3.2. 所用版本工具
3.3.?pom依賴
4. 核心代碼
4.1. 網(wǎng)關(guān)模塊核心代碼
4.1.1. 編寫網(wǎng)關(guān)yml配置
4.1.2. 編寫Security授權(quán)配置主文件
4.1.3. 編寫認(rèn)證過濾器
4.1.4. 重寫DefaultServerOAuth2AuthorizationRequestResolver
?4.1.5. 編寫OAuth2User實(shí)現(xiàn)類
?4.1.6. 編寫url白名單配置類
4.1.7.? 編寫userInfo過濾器
?4.1.8. 編寫ReactiveOAuth2UserService實(shí)現(xiàn)類
4.2. 資源服務(wù)器核心代碼
4.2.1. 編寫資源服務(wù)器yml
4.2.2. 編寫資源服務(wù)器測試controller
5. 登錄測試
6. 參考鏈接
1. 前言
????????Spring Cloud Gateway是Spring Cloud生態(tài)系統(tǒng)中的一個(gè)組件,主要用于構(gòu)建微服務(wù)架構(gòu)中的網(wǎng)關(guān)服務(wù)。它提供了一種靈活而強(qiáng)大的方式來路由請求、過濾請求以及添加各種功能,如負(fù)載均衡、熔斷、安全性等。通過將Spring Cloud Gateway作為OAuth2 Client,可以實(shí)現(xiàn)用戶在系統(tǒng)中的統(tǒng)一認(rèn)證體驗(yàn)。用戶只需要一次登錄,即可訪問多個(gè)微服務(wù),避免了在每個(gè)服務(wù)中都進(jìn)行獨(dú)立的認(rèn)證,下游微服務(wù)只需要專注自己的業(yè)務(wù)代碼即可。
2. 流程圖
? ? ? ? 讓我們來先看一下基于網(wǎng)關(guān)集成單點(diǎn)登錄的流程圖(OAuth2授權(quán)碼模式),我這邊只是一個(gè)大致流程,想要看完整細(xì)致流程的同學(xué)可以去看一下大佬寫的這篇文章:Spring Cloud Gateway作為OAuth2 Client_oauth2客戶端接口為什么跳轉(zhuǎn)到login_羅小爬EX的博客-CSDN博客
3. 開發(fā)環(huán)境搭建
3.1. 項(xiàng)目結(jié)構(gòu)
基于Spring Cloud Gateway作為OAuth2 Client接入單點(diǎn)登錄的項(xiàng)目結(jié)構(gòu)如下圖所示:
由上圖可以看出這個(gè)項(xiàng)目(demo)是微服務(wù)組織架構(gòu),這里我只創(chuàng)建了兩個(gè)moudle(父模塊不算)即網(wǎng)關(guān)和資源服務(wù)器。
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.? 網(wǎng)關(guān)模塊依賴
<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. 資源服務(wù)器模塊依賴
<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. 網(wǎng)關(guān)模塊核心代碼
4.1.1. 編寫網(wǎng)關(guān)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 # 會(huì)話存儲類型
redis:
cleanup-cron: 0 * * * * *
flush-mode: on_save # 會(huì)話刷新模式
namespace: gateway:session # 用于存儲會(huì)話的鍵的命名空間
save-mode: on_set_attribute
redis:
host: localhost
port: 6379
# password: 123456
security:
filter:
order: 5
oauth2:
client:
registration:
gas:
provider: gas
client-id: 在第三方授權(quán)中心獲取的 client-id
client-secret: 在第三方授權(quán)中心獲?。ㄗ远x)的 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: 填寫第三方認(rèn)證地址
#
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授權(quán)配置主文件
@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
// 發(fā)起 OAuth2 登錄的地址(服務(wù)端)
.authorizationRequestResolver(saveRequestServerOAuth2AuthorizationRequestResolver)
// OAuth2 外部用戶登錄授權(quán)后的跳轉(zhuǎn)地址(服務(wù)端)
.authenticationMatcher(new PathPatternParserServerWebExchangeMatcher(
oauth2LoginEndpoint))
)
.cors().disable();
return http.build();
}
/**
* OAuth2 Client Authorization Endpoint /oauth2/authoriztion/{clientRegId}
* 請求解析器擴(kuò)展實(shí)現(xiàn) - 支持提取query參數(shù)redirect_uri,用作后續(xù)OAuth2認(rèn)證完成后網(wǎng)關(guān)重定向到該指定redirect_uri。
* 適用場景:前端應(yīng)用 -> 網(wǎng)關(guān) -> 網(wǎng)關(guān)返回401 -> 前端應(yīng)用重定向到/oauth2/authorization/{clientRegId}?redirect_uri=http://登錄后界面 -> 網(wǎng)關(guān)完成OAuth2認(rèn)證后再重定向回http://登錄后界面
*/
@Bean
@Primary
public ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {
return new SaveRequestServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
}
/**
* 自定義UserInfo過濾器工廠
*/
@Bean
public UserInfoRelayGatewayFilterFactory userInfoRelayGatewayFilterFactory() {
return new UserInfoRelayGatewayFilterFactory();
}
}
4.1.3. 編寫認(rèn)證過濾器
@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();
// 響應(yīng)對象
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");
// 轉(zhuǎn)換響應(yīng)消息內(nèi)容對象為字節(jié)
byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
// 設(shè)置響應(yīng)對象狀態(tài)碼 401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 設(shè)置響應(yīng)對象內(nèi)容并且指定編碼,否則在瀏覽器中會(huì)中文亂碼
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
// 返回響應(yīng)對象
return response.writeWith( Mono.just(buffer) );
}
return chain.filter(exchange);
}).then(Mono.fromRunnable(() -> {
log.info("this is a post filter");
}));
}
}
上述代碼的主要功能為攔截進(jìn)入網(wǎng)關(guān)的每一個(gè)請求,若沒有身份憑證(令牌)則返回/oauth2/authorization/{clientRegId}。
4.1.4. 重寫DefaultServerOAuth2AuthorizationRequestResolver
public class SaveRequestServerOAuth2AuthorizationRequestResolver extends DefaultServerOAuth2AuthorizationRequestResolver {
private static final Log logger = LogFactory.getLog(SaveRequestServerOAuth2AuthorizationRequestResolver.class);
/**
* redirect uri參數(shù)名稱
*/
private static final String PARAM_REDIRECT_URI = "redirect_uri";
/**
* WebSession對應(yīng)的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參數(shù)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
//即后續(xù)認(rèn)證成功后可重定向回前端指定頁面
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實(shí)現(xiàn)類
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實(shí)現(xiàn)類
@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"),"服務(wù)器返回錯(cuò)誤的jwt");
}
});
}
}
4.2. 資源服務(wù)器核心代碼
4.2.1. 編寫資源服務(wù)器yml
server:
port: 8090
servlet:
context-path: /resource
4.2.2. 編寫資源服務(wù)器測試controller
@RestController
public class ArticleController {
@GetMapping("/user-info")
public String getUserName( @RequestHeader String userInfo){
return userInfo;
}
}
5. 登錄測試
1. 直接訪問資源服務(wù)器接口
由上圖可看出無法直接訪問資源服務(wù)器接口,前端接收到此返回信息后根據(jù)data中返回的路徑加上redirect_uri(http://127.0.0.1:8888/oauth2/authorization/gas?redirect_uri=http://www.baidu.com),發(fā)送頁面請求后可跳轉(zhuǎn)至登錄中心,認(rèn)證成功后界面會(huì)重定向至redirect_uri所指定的界面(我這里寫的百度)。
跳轉(zhuǎn)至登錄界面進(jìn)行認(rèn)證。
認(rèn)證成功后重定向至redirect_uri所指定的界面(百度)。
2. 再次訪問資源服務(wù)器接口
訪問接口成功。
6. 參考鏈接
Spring Cloud Gateway作為OAuth2 Client_oauth2客戶端接口為什么跳轉(zhuǎn)到login_羅小爬EX的博客-CSDN博客
將Spring Cloud Gateway 與OAuth2模式一起使用_jwk-set-uri_ReLive27的博客-CSDN博客文章來源:http://www.zghlxwxcb.cn/news/detail-766846.html
第15章 Spring Security OAuth2 初始_authorizeexchange-CSDN博客文章來源地址http://www.zghlxwxcb.cn/news/detail-766846.html
到了這里,關(guān)于【OAuth2系列】Spring Cloud Gateway 作為OAuth2 Client接入第三方單點(diǎn)登錄代碼實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!