OAuth
OAuth 2.0 是目前最流行的授权机制,用来授权第三方应用,获取用户数据,关于Oauth2推荐阅读阮一峰的博客
简单总结起来,oauth2 可以分为授权服务器,资源服务器以及Oauth2客户端三部分(其中客户端部分就属于我们常规用来进行登陆的服务,授权服务器跟资源服务器也可以放到一起实现);
本文先介绍spring security Oauth 客户端,从一个github登陆的简单项目减少一下客户端大致的请求过程,后续的文章会介绍子单元服务器跟授权服务器。
一个简单的github登陆项目
启动本项目访问localhost:8080/index.html 之后会要求登陆github并授权,授权后就可以看到登陆信息了。
启动项目后可以看到项目中有一些过滤器被加载:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5762658b,
org.springframework.security.web.context.SecurityContextPersistenceFilter@2be95d31,
org.springframework.security.web.header.HeaderWriterFilter@788ba63e,
org.springframework.security.web.csrf.CsrfFilter@536d97f8,
org.springframework.security.web.authentication.logout.LogoutFilter@2ab26378,
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@54755dd9,
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@7c3e4b1a,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4d68b571,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2629d5dc,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2aa7399c,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6c1cfa53,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@2596d7f4,
org.springframework.security.web.session.SessionManagementFilter@68ee3b6d,
org.springframework.security.web.access.ExceptionTranslationFilter@3289079a,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3d19d85]
比前面文章分析的springsecurity过滤器链多了两个
OAuth2AuthorizationRequestRedirectFilter
:根据提供请求重定向到第三方授权页过滤器。
OAuth2LoginAuthenticationFilter
:登陆过滤器,处理第三方服务器回调请求
实现 Oauth2 登陆重点也就是在这两个过滤器上。
OAuth2AuthorizationRequestRedirectFilter
OAuth2AuthorizationRequestRedirectFilter 用于处理请求路径为/oauth2/authorization/{registrationId}
的请求。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
// 重定向
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
} catch (Exception failed) {
this.unsuccessfulRedirectForAuthorization(request, response, failed);
return;
}
try {
filterChain.doFilter(request, response);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// Check to see if we need to handle ClientAuthorizationRequiredException
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException) this.throwableAnalyzer
.getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain);
if (authzEx != null) {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request, authzEx.getClientRegistrationId());
if (authorizationRequest == null) {
throw authzEx;
}
this.sendRedirectForAuthorization(request, response, authorizationRequest);
this.requestCache.saveRequest(request, response);
} catch (Exception failed) {
this.unsuccessfulRedirectForAuthorization(request, response, failed);
}
return;
}
if (ex instanceof ServletException) {
throw (ServletException) ex;
} else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
} else {
throw new RuntimeException(ex);
}
}
}
这个过滤器的重点就在于
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
这一句,获取重定向请求。详细代码如下:
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
private String resolveRegistrationId(HttpServletRequest request) {
if (this.authorizationRequestMatcher.matches(request)) {
return this.authorizationRequestMatcher
.matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME);
}
return null;
}
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
if (registrationId == null) {
return null;
}
// 根据 registrationId 获取 ClientRegistration
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
}
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
// 授权码模式
builder = OAuth2AuthorizationRequest.authorizationCode();
Map<String, Object> additionalParameters = new HashMap<>();
if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) &&
clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
addNonceParameters(attributes, additionalParameters);
}
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
addPkceParameters(attributes, additionalParameters);
}
builder.additionalParameters(additionalParameters);
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
// 隐式授权模式
builder = OAuth2AuthorizationRequest.implicit();
} else {
throw new IllegalArgumentException("Invalid Authorization Grant Type (" +
clientRegistration.getAuthorizationGrantType().getValue() +
") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
// 构建授权请求
OAuth2AuthorizationRequest authorizationRequest = builder
.clientId(clientRegistration.getClientId())
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
.redirectUri(redirectUriStr)
.scopes(clientRegistration.getScopes())
.state(this.stateGenerator.generateKey())
.attributes(attributes)
.build();
return authorizationRequest;
}
简单总结,OAuth2AuthorizationRequestRedirectFilter就是供请求重定向到第三方授权页。
OAuth2LoginAuthenticationFilter
OAuth2LoginAuthenticationFilter默认只处理由第三方重定向的路径为/login/oauth2/code/*
形式的请求。过滤器中进行授权结果的解剖,在封装成为Authentication认证对象。
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 获取参数
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_request");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error("authorization_request_not_found");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
String registrationId = (String)authorizationRequest.getAttribute("registration_id");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
OAuth2Error oauth2Error = new OAuth2Error("client_registration_not_found", "Client Registration not found with Id: " + registrationId, (String)null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)).replaceQuery((String)null).build().toUriString();
// 构建授权结果
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken)this.getAuthenticationManager().authenticate(authenticationRequest);
OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(authenticationResult.getPrincipal(), authenticationResult.getAuthorities(), authenticationResult.getClientRegistration().getRegistrationId());
oauth2Authentication.setDetails(authenticationDetails);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(authenticationResult.getClientRegistration(), oauth2Authentication.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
}
}
}
}