Spring Security Oauth2 之 理解OAuth 2.0授权流程

发布于:2024-04-17 ⋅ 阅读:(25) ⋅ 点赞:(0)

1. Oauth 定义

1.1 角色
OAuth定义了四个角色:
资源所有者

一个能够授权访问受保护资源的实体。当资源所有者是一个人时,它被称为最终用户。

资源服务器

托管受保护资源的服务器能够使用访问令牌接受和响应受保护的资源请求。

客户

代表资源所有者及其授权的应用程序进行受保护的资源请求。术语客户端并不意味着任何特定的实现特征(例如,应用程序是在服务器,台式机还是其他设备上执行的)。

授权服务器

服务器在成功认证资源所有者并获得授权后向客户端发放访问令牌。
1.2 协议流程

图1中所示的抽象OAuth 2.0流程描述了四个角色之间的交互,并包含以下步骤:

(A) 客户端请求资源所有者的授权。授权请求可以直接给资源所有者(如图所示),或者优选间接地通过授权服务器作为中介。
(B) 客户端接收授权许可,这是一种代表资源所有者授权的凭证,使用本规范中定义的四种授权类型之一或使用扩展授权类型表示。授权授予类型取决于客户端用于请求授权的方法以及授权服务器支持的类型。
(C) 客户端通过向授权服务器进行认证并携带授权来请求访问令牌。
(D) 授权服务器对客户端进行身份验证并验证授权,并且如果有效则发出访问令牌。
(E) 客户端从资源服务器请求受保护的资源并通过携带访问令牌进行认证。
(F) 资源服务器验证访问令牌,并且如果有效,则为该请求提供服务。

客户从资源所有者(步骤(A)和(B)中描述)获得授权许可的首选方法是使用授权服务器作为中介
2 模式

oauth2根据使用场景不同,分成了4种模式
● 客户端模式(client credentials):标准的 Server 授权模式,非常适合 Server 端的 Web 应用。一旦资源的拥有者授权访问他们的数据之后,他们将会被重定向到 Web 应用并在 URL 的查询参数中附带一个授权码(code)。在客户端里,该 code 用于请求访问令牌(access_token)。并且该令牌交换的过程是两个服务端之前完成的,防止其他人甚至是资源拥有者本人得到该令牌。另外,在该授权模式下可以通过 refresh_token 来刷新令牌以延长访问授权时间,也是最为复杂的一种方式。
● 密码模式(resource owner password credentials) : 自己有一套用户体系,这种模式要求用户提供用户名和密码来交换访问令牌(access_token)。该模式仅用于非常值得信任的用户,例如API提供者本人所写的移动应用。虽然用户也要求提供密码,但并不需要存储在设备上。因为初始验证之后,只需将 OAuth 的令牌记录下来即可。如果用户希望取消授权,因为其真实密码并没有被记录,因此无需修改密码就可以立即取消授权。token本身也只是得到有限的授权,因此相比最传统的 username/password 授权,该模式依然更为安全。
● 授权码模式(authorization code) :该模式是所有授权模式中最简单的一种,并为运行于浏览器中的脚本应用做了优化。当用户访问该应用时,服务端会立即生成一个新的访问令牌(access_token)并通过URL的#hash段传回客户端。这时,客户端就可以利用JavaScript等将其取出然后请求API接口。该模式不需要授权码(code),当然也不会提供refresh token以获得长期访问的入口。
● 简化模式(implicit) :没有用户的概念,一种基于 APP 的密钥直接进行授权,因此 APP 的权限非常大。它适合像数据库或存储服务器这种对 API 的访问需求。

Oauth基于客户端与认证服务器验证的能力定义了两种客户端类型(以及,维护客户端认证信息的能力): 客户端模式、密码模式。

基础参数定义:
grant_type (发放令牌类型)、
client_id (客户端标识id)
username(用户帐号)
password (用户密码)
client_secret(客户端标识密钥)
refresh_token (刷新令牌)
scope(表示权限范围,可选项)
Oauth2 Client 集成

pom.xml

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
package com.lvyuanj.upms.oauthclient.config;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;

@Configuration
public class UpmsOauth2LoginConfig {

	@EnableWebSecurity
	public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

		@Autowired
		private UpmsLogoutHandler upmsLogoutHandler;

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.authorizeRequests()
				.anyRequest().authenticated()
				.and().logout().addLogoutHandler(upmsLogoutHandler)
				.and()
				.oauth2Login();
		}
		@Override
		public void configure(WebSecurity web) throws Exception {
			web.ignoring().mvcMatchers("/js/**","/css/**");
		}
	}

	@Bean
	public OAuth2AuthorizedClientService authorizedClientService(
			ClientRegistrationRepository clientRegistrationRepository) {
		return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
	}

	@Bean
	public OAuth2AuthorizedClientRepository authorizedClientRepository(
			OAuth2AuthorizedClientService authorizedClientService) {
		return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
	}

	@Bean
	public ClientRegistrationRepository clientRegistrationRepository() {
		return new InMemoryClientRegistrationRepository(this.upmsOauth2ClientRegistration());
	}

	@Bean
	@ConfigurationProperties(prefix = "spring.security.oauth2.client.registration.upms")
	public UpmsClientRegistration upmsClientRegistration(){
		return new UpmsClientRegistration();
	}
	
	@Bean
	@ConfigurationProperties(prefix = "spring.security.oauth2.client.provider.upms")
	public UpmsClientProvider upmsClientProvider(){
		return new UpmsClientProvider();
	}
	
	private ClientRegistration upmsOauth2ClientRegistration() {
		return ClientRegistration.withRegistrationId(this.upmsClientRegistration().getProvider())
				.clientId(this.upmsClientRegistration().getClientId())
				.clientSecret(this.upmsClientRegistration().getClientSecret())
				.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
				.authorizationGrantType(new AuthorizationGrantType(this.upmsClientRegistration().getAuthorizationGrantType()))
				.redirectUriTemplate(this.upmsClientRegistration().getRedirectUriTemplate())
				.scope(this.upmsClientRegistration().getScope())
				.authorizationUri(this.upmsClientProvider().getAuthorizationUri())
				.tokenUri(this.upmsClientProvider().getTokenUri())
				.userInfoUri(this.upmsClientProvider().getUserInfoUri())
				.userNameAttributeName(this.upmsClientProvider().getUserNameAttribute())
				.clientName(this.upmsClientRegistration().getClientName())
				.build();
	}
}

oauth registration config

package com.lvyuanj.upms.oauthclient.config;

import lombok.Data;

@Data
public class UpmsClientProvider {

    private String authorizationUri;

    private String tokenUri;

    private String userInfoUri;

    private String userNameAttribute;
}

package com.lvyuanj.upms.oauthclient.config;


import lombok.Data;

@Data
public class UpmsClientRegistration {

    private String provider;

    private String clientId;

    private String clientSecret;

    private String clientName;

    private String authorizationGrantType;

    private String redirectUriTemplate;

    private String scope;

}

退出登陆配置

package com.lvyuanj.upms.oauthclient.config;

import com.lvyuanj.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Service
public class UpmsLogoutHandler implements LogoutHandler {

	@Value("${com.lvyuanj.upms.client.upms.logout-uri}")
	private String logoutUrl;

	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		try {
			String access_token = (String) request.getSession().getAttribute("access_token");
			String params = "";
			if(StringUtils.isNotBlank(access_token)){
				params = "?access_token="+access_token;
			}
			response.sendRedirect(logoutUrl+params);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

登陆成功跳转到index

package com.lvyuanj.upms.oauthclient.controller;
 
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Map;

@Slf4j
@Controller
public class OAuth2Controller {
 
	@Autowired
	private OAuth2AuthorizedClientService authorizedClientService;
	
	@RequestMapping("/")
	public String index(Model model, OAuth2AuthenticationToken authentication, HttpServletRequest request) {
		log.debug("authentication:"+ JSONObject.toJSONString(authentication));
		OAuth2AuthorizedClient authorizedClient = this.getAuthorizedClient(authentication);
		if(null != authorizedClient){
			String access_token = authorizedClient.getAccessToken().getTokenValue();
			log.debug("access_token: "+ access_token);
			request.getSession().setAttribute("access_token", access_token);
			model.addAttribute("userName", authentication.getName());
			model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
			return "index";
		}else {
			return "logout";
		}
	}
 
	private OAuth2AuthorizedClient getAuthorizedClient(OAuth2AuthenticationToken authentication) {
		return this.authorizedClientService.loadAuthorizedClient(
			authentication.getAuthorizedClientRegistrationId(), authentication.getName());
    }
	
	@RequestMapping("/userinfo")
	public String userinfo(Model model,OAuth2AuthenticationToken authentication) {
		// authentication.getAuthorizedClientRegistrationId() returns the
		// registrationId of the Client that was authorized during the Login flow
		OAuth2AuthorizedClient authorizedClient =
			this.authorizedClientService.loadAuthorizedClient(
				authentication.getAuthorizedClientRegistrationId(),authentication.getName());
		OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
		System.out.println(accessToken.getTokenValue());
		Map userAttributes = Collections.emptyMap();
		String userInfoEndpointUri = authorizedClient.getClientRegistration()
			.getProviderDetails().getUserInfoEndpoint().getUri();
		if (!StringUtils.isEmpty(userInfoEndpointUri)) {// userInfoEndpointUri is optional for OIDC Clients
			userAttributes = WebClient.builder()
				.filter(oauth2Credentials(authorizedClient))
				.build().get().uri(userInfoEndpointUri).retrieve().bodyToMono(Map.class).block();
		}
		model.addAttribute("userAttributes", userAttributes);
		return "userinfo";
	}
	
	private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) {
		return ExchangeFilterFunction.ofRequestProcessor(
			clientRequest -> {
			ClientRequest authorizedRequest = ClientRequest.from(clientRequest)
			.header(HttpHeaders.AUTHORIZATION, "Bearer " + 
			authorizedClient.getAccessToken().getTokenValue()).build();
			return Mono.just(authorizedRequest);
		});
    }
}

Spring Security Oauth2 Client 非常重要的过滤器:OAuth2LoginAuthenticationFilter

org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter#attemptAuthentication方法

    public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";    --在oauth2登陆成功之后,此过滤器拦截回调接口地址
    
    @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {

		MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
		if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {  // 判断回调数据是否有CODE 或者 STATE 参数
			OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
       
		OAuth2AuthorizationRequest authorizationRequest =
				this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
		if (authorizationRequest == null) {
			OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}

		String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID);
		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
		if (clientRegistration == null) {
			OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
					"Client Registration not found with Id: " + registrationId, null);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
				.replaceQuery(null)
				.build()
				.toUriString();
		OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);

		OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
				clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
		authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));

		OAuth2LoginAuthenticationToken authenticationResult =
			(OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);

		OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
			authenticationResult.getPrincipal(),
			authenticationResult.getAuthorities(),
			authenticationResult.getClientRegistration().getRegistrationId());

		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
			authenticationResult.getClientRegistration(),
			oauth2Authentication.getName(),
			authenticationResult.getAccessToken(),
			authenticationResult.getRefreshToken());

		this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);

		return oauth2Authentication;
	}