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

笔者写本文时,抗疫已取得重大进展,天佑中华,希望能尽快取得最终胜利,加油!!!

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