Spring Security Oauth2實踐(2) - 客戶端對接

距離上次寫分享已有半年沒有更新了,由於工作上的新項目、生活等方面的原因,遲遲未整理Spring Security Oauth2這塊的內容。最近全國各地都在抗疫中,大家工作生活都受到影響,基本不能出門,儘量在家辦公。當然我也不例外,利用比平時上班省下的些許通勤時間整理這篇分享。

上篇文章分享瞭如何創建Oauth2授權服務器及最常用的授權碼模式的使用過程,那麼在實際項目中如何對接授權服務器實現對某些受保護資源的訪問?相信大家都使用過各種平臺的登錄,比如CSDN註冊/登錄,除了服務本身提供的賬號方式,還可以通過社交賬號的如qq/github/baidu等方式登錄,這種登錄方式就是CSDN利用這些社交服務商提供的Oauth2授權服務來獲取當前服務的信息,從而完成CSDN註冊/綁定。在這個過程中,CSDN扮演的則是Oauth2客戶端或第三方應用,本文將創建一個demo項目,分享實現Oauth2客戶端獲取授權的基本過程。
CSDN登錄頁

Oauth2客戶端實現方式

Oauth2客戶端對接實現一般有兩種方式:

  1. 調用授權服務器提供的授權碼接口、獲取token接口等來對接(交互過程可參考Spring Security Oauth2實踐(1) - 授權碼模式),屬於最常用的方式,筆者在實際開發小應用項目過程中也用到這種方式,理解起來也很方便,缺點是需要自定義大量編碼、封裝拓展性依賴開發人員水平、url 302轉發、登錄狀態維護等需要額外開發控制、多個Oauth2 Provider時可能需要重新編碼對接,關於這塊過程可參考Spring Boot+OAuth2使用GitHub登錄自己的服務.這裏不再提供實現。

  2. 基於Spring Security Oauth2生態提供的方式對接,框架本身將大量的調用交互細節封裝,並且維護用戶認證登錄狀態,提供了很多拓展接口/類方便自定義業務邏輯,開發往往只需要實現特定的接口並注入到相應的業務中就可以完成。缺點在於門檻較高、理解起來需要一點時間、涉及到Spring Security認證狀態維護、過濾器等細節往往需要閱讀源碼才理解怎麼使用。一旦理解原理、配置使用等,則後續拓展將會非常方便,本文將採用這種方式實踐。

Spring Security Oauth2客戶端實踐

值得注意的是,Spring Security OAuth2提供了一套客戶端實現(本文采用的框架),Spring Boot也有它自己的方式,這種方式可參考Spring Boot官方指南中,實現、配置等與前者有區別。

Oauth2授權服務器使用上篇分享搭建的環境,SpringBoot採用2.x。

Maven依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.oauth.client</groupId>
	<artifactId>oauth-client-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<name>oauth-client-demo</name>
	<description>oauth-client-demo</description>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.11.RELEASE</version>
		<relativePath />
	</parent>
	<properties>
		<java.version>8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security.oauth.boot</groupId>
			<artifactId>spring-security-oauth2-autoconfigure</artifactId>
			<version>2.1.4.RELEASE</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

項目配置文件

application.yml配置文件,其中${oauth2.client}${oauth2.resource}配置是Oauth2授權服務器發放的客戶端應用信息和要訪問的被保護資源。

server:
  port: 9201
  servlet:
    context-path: /oauth-client1
    session:
      #自定義Session名稱,以區分瀏覽器其它的登錄Session
      cookie:
        name: APPSESSION

oauth2:
  client:
    clientId: B5CDC04D8D8D419DA406364168F276A2
    clientSecret: E2DA5B2DEDD548AEB7ABA689436C2E2C
    accessTokenUri: http://localhost:9011/auth/oauth/token
    userAuthorizationUri: http://localhost:9011/auth/oauth/authorize
    clientAuthenticationScheme: form
    tokenName: access_token
    scope: userProfile
  resource:
    userInfoUri: http://localhost:9011/auth/userInfo

logging:
  # level下配置鍵值對
  level:
    root: debug

通過@EnableOAuth2Client配置Spring Security Oauth2

@EnableOAuth2Client配置表示在Spring Security應用創建一個Oauth2客戶端,它可以對接一個或多個授權服務器獲取授權碼認證。該配置需要創建一個OAuth2ClientContextFilter過濾器,並需要注入OAuth2ClientContextOauth2Client上下文,這樣就可以使用OAuth2RestTemplate來調用授權服務器的一系列認證服務。具體過程如下

@Configuration
@EnableOAuth2Client
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	OAuth2ClientContext oauth2ClientContext;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.antMatchers("/", "/login**", "/error**").permitAll()
				.anyRequest().authenticated()
		        .and()
				.logout().logoutUrl("/logout").logoutSuccessUrl("/")
		        .and()
				.addFilterBefore(oauth2ClientFilter(), BasicAuthenticationFilter.class);

	}

	@Bean
	@ConfigurationProperties("oauth2.client")
	public AuthorizationCodeResourceDetails githubClient() {
		return new AuthorizationCodeResourceDetails();
	}

	@Bean
	@ConfigurationProperties("oauth2.resource")
	public ResourceServerProperties githubResource() {
		return new ResourceServerProperties();
	}
	
	// 過濾器註冊bean
	@Bean
	public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(
			OAuth2ClientContextFilter filter) {
		FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
		registration.setFilter(filter);
		registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
		return registration;
	}	
	
	private Filter oauth2ClientFilter() {
		//配置在Oauth2授權服務器上註冊的redirect_url,可理解爲認證完成後的回調/過程接口
		OAuth2ClientAuthenticationProcessingFilter oauth2ClientFilter = new OAuth2ClientAuthenticationProcessingFilter(
				"/login/demo");
		OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(githubClient(), oauth2ClientContext);
		oauth2ClientFilter.setRestTemplate(restTemplate);
		UserInfoTokenServices tokenServices = new UserInfoTokenServices(githubResource().getUserInfoUri(),
				githubClient().getClientId());
		// 可配置用戶授權後的回調業務,比如將用戶的信息保存在服務中
		//tokenServices.setPrincipalExtractor();
		tokenServices.setRestTemplate(restTemplate);
		oauth2ClientFilter.setTokenServices(tokenServices);
		return oauth2ClientFilter;
	}	
}

服務Controller接口

服務接口,爲前端提供服務。未認證前訪問這些接口則會報403(禁止訪問),前端接收到狀態後可跳轉到上文中的/login/demo到授權服務器登錄獲取權限。

@Controller
public class AppController {

	// 主頁
	@RequestMapping("/")
	public void indexHome(Principal principal, HttpServletResponse response) throws IOException {
		if (null == principal) {
			// 未登錄時則跳轉,實際過程一般是由前端控制,後臺返回 401即可,前端處理跳轉到index,用戶點擊通過其它應用授權
			response.sendRedirect("login/demo");
			return;
		}
		response.getWriter().write("welcome " + principal.getName() + " !!!");
	}
	// 測試接口
	@RequestMapping("loginUser")
	@ResponseBody
	public Object getUser(Principal principal) {
		return principal;
	}
}

測試

整體交互流程

交互過程

具體登錄過程

  1. 訪問 http://localhost:9201/oauth-client1/

  2. 跳轉到 http://localhost:9011/auth/login 認證服務服務器登錄頁面

登錄頁面

  1. 輸入賬密登錄成功,跳轉到http://localhost:9011/auth/oauth/authorize?client_id=B5CDC04D8D8D419DA406364168F276A2&redirect_uri=http://localhost:9201/oauth-client1/login/demo&response_type=code&scope=userProfile&state=3CWGVv請求授權;

  2. 確認授權頁面是否授權
    授權確認頁

  3. 獲取到授權碼

  4. 登錄成功後最終302跳轉到主頁

登錄成功

  1. 測試其它接口也可訪問

測試接口

總結

文章提供了一個基於Spring Security框架的Oauth2客戶端demo,由於框架封裝,搭建成功後整體看起來非常簡潔,從最終的交互過程來看框架對於Oauth2授權碼模式標準也基本全部實現。如果需要了解框架是如何工作的,則需要深入源碼,比如訪問授權接口過程、維護認證狀態等過程源碼,由於時間資源有限,筆者搭建Demo、調試過程中研究了部分源碼,但未整理成文,有機會再分享。
實際生產落地實踐則需要考慮更多,例如如何保存從資源服務器獲取的用戶信息與平臺綁定、url接口權限、應用登錄狀態維護、前端分離交互等。

參考

https://www.baeldung.com/sso-spring-security-oauth2
https://www.concretepage.com/spring-boot/spring-boot-enableoauth2client
https://www.cnblogs.com/cjsblog/p/9241217.html

筆者寫本文時,抗疫已取得重大進展,天佑中華,希望能儘快取得最終勝利,加油!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章