Click the blue character above to set the star
link :zyc.red/Spring/Security/OAuth2/OAuth2-Client/
Preface
OAuth( Open licensing ) It's an open standard , Allow users to authorize third-party websites to access information they store on other service providers , Instead of providing a user name and password to a third-party website or sharing all of their data . There's a lot about it online OAuth Explanation of the agreement , I'm not going to explain it in detail here OAuth Related concepts , Please refer to the relevant information by yourself , Otherwise, the rest of this article may be difficult to understand .
Spring-Security Yes OAuth2.0 Support for
As of the date of this article ,Spring Has been provided for OAuth Support provided (spring-security-oauth:https://github.com/spring-projects/spring-security-oauth), But the project has been abandoned , because Spring-Security The project provides the latest OAuth2.0 Support . If you use expired Spring-Security-OAuth, Please refer to 《OAuth 2.0 Migration guide :https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide》,
This article will OAuth2.0 Principle analysis of client mode in , combination Spring The official guide provides a simple one based on spring-boot And oauth2.0 Integration of third-party application login cases (spring-boot-oauth2:https://spring.io/guides/tutorials/spring-boot-oauth2/), Step by step analysis of the principle of its internal implementation .
The official account has also released nearly 100 articles Spring Boot Related practical articles , Pay attention to WeChat public number Java Back end , reply 666 Download this technical stack manual .
establish GitHub OAuth Apps
stay Github OAuth Apps Create a new application in
This app is equivalent to our own app ( client ), Registered in Github( Authorization server ) It's in , If our users have github If you have an account number , It can be based on oauth2 To log in to our system , Instead of the original user name and password . In the example of the official guide , Use spring-security and oauth2 Social login only needs to be done on your pom Add the following dependencies to the file :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<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>
Then fill in the configuration file with the clientId and clientSecret:
spring:
security:
oauth2:
client:
registration:
github:
clientId: github-client-id
clientSecret: github-client-secret
And then it's like normal spring-security The application is the same , Inherit WebSecurityConfigurerAdapter, Simple configuration is enough :
@SpringBootApplication
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(a -> a
.antMatchers("/", "/error", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(e -> e
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
)
.oauth2Login();
}
}
That is to say, we just need to add maven Dependency and inheritance WebSecurityConfigurerAdapter Do some simple configuration , One oauth2 The client application is built . Next, follow the steps in the guide and click on the page github Log in and our page will jump to github Authorization landing page , Wait for the user's authorization to complete and the browser will be redirected to our callback URL The final request user The information endpoint can access the just logged in github User information , The whole application is so simple to build , What's the principle behind it ? Next we start to analyze .
Same as before , In the configuration file, we will security The log level of is set to debug
logging:
level:
org.springframework.security: debug
After restarting the application , From the output of the console, we can see that it is similar to the normal spring-security The difference in application is that the following filters are added to the whole filter chain :
OAuth2AuthorizationRequestRedirectFilter
OAuth2LoginAuthenticationFilter
lenovo oauth2 The authorization code pattern and the names of the two filters , be familiar with spring-security My classmates must have some ideas in their hearts . Yes, it is ,spring-security The support of client mode is based on these two filters . Now let's recall the execution process of authorization code mode
The user clicks the three party application login button on the client page ( The client is what we just registered github application )
The page will jump to the authorized party page of three party application registration ( The authorization server is github)
After user login Authorization ,github Call the callback address of our application ( We just registered github The callback address filled in when applying )
In the callback address of step 3 github Will code Put the parameters in url in , Then our client will take this internally code Call again github
Of access_token Address acquisition token
That's the standard authorization_code Authorization mode ,OAuth2AuthorizationRequestRedirectFilter The function of the above step is 1.2 The combination of steps , When the user clicks on the page github to grant authorization url after ,OAuth2AuthorizationRequestRedirectFilter Match this request , And then it will put the clientId、scope And construct a state Parameters ( prevent csrf attack ) To join together into one url Redirect to github Authorization of url,OAuth2LoginAuthenticationFilter The role of the top 3.4 The combination of steps , When the user github After the authorization page is authorized github Call callback address ,OAuth2LoginAuthenticationFilter Match this callback address , The address of the callback is resolved code And state After the parameters are verified, take this inside code The remote invocation github Of access_token Address , Get access_token After through OAuth2UserService Get the corresponding user information ( Inside is to take access_token The remote invocation github User information endpoint for ) Finally, the user information is constructed into Authentication By SecurityContextPersistenceFilter Filter saved to HttpSession in .
Let's take a look at the internal execution of these two filters :
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
...... Omitted code
@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;
}
...... Omitted code
}
adopt authorizationRequestResolver The parser parses the request , The default implementation of the parser is DefaultOAuth2AuthorizationRequestResolver, The core parsing method is as follows :
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
if (registrationId == null) {
return null;
}
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)) {
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;
}
DefaultOAuth2AuthorizationRequestResolver Determine whether the request is an authorization request , Finally return to a OAuth2AuthorizationRequest The object is to OAuth2AuthorizationRequestRedirectFilter, If OAuth2AuthorizationRequest Not for null Words , State that the current request is an authorization request , Then it's time to redirect the request to the authorization endpoint of the authorization server , Let's take a look at OAuth2AuthorizationRequestRedirectFilter Send redirection logic :
private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
OAuth2AuthorizationRequest authorizationRequest) throws IOException {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
}
this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
}
1. If the current authorization request is an authorization code type, then the request information needs to be saved , Because the authorization server calls back next, we need to use the parameters of the authorization request for verification and other operations ( comparison state), This is through authorizationRequestRepository Save the authorization request , The default way to save is through HttpSessionOAuth2AuthorizationRequestRepository Save in httpsession Medium , The specific preservation logic is very simple , I won't go into details here .
2. After saving, it is time to start redirecting to the authorized service endpoint , The default here is authorizationRedirectStrategy yes DefaultRedirectStrategy, The logic of redirection is simple , adopt response.sendRedirect Method to redirect the front page to the specified authorization
public void sendRedirect(HttpServletRequest request, HttpServletResponse response,
String url) throws IOException {
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (logger.isDebugEnabled()) {
logger.debug("Redirecting to '" + redirectUrl + "'");
}
response.sendRedirect(redirectUrl);
}
OAuth2AuthorizationRequestRedirectFilter The processing logic is over , Let's make a summary of its handling process
a. Through internal OAuth2AuthorizationRequestResolver Parse the current request , Return to one OAuth2AuthorizationRequest object , If the current request is an authorization endpoint request , Then a constructed object is returned , Including our client_id、state、redirect_uri Parameters , If the object is null Words , Then it means that the current request is not an authorization endpoint request .
Note that if OAuth2AuthorizationRequestResolver Not for null Words ,OAuth2AuthorizationRequestResolver It will be stored internally in httpsession In this way, when the authorization server calls our callback address, we can start from httpsession The request will be state Contrast in case csrf attack .
b. If the first step returns OAuth2AuthorizationRequest Objects are not as null Words , And then it's going to go through response.sendRedirect Method will OAuth2AuthorizationRequest The request from the authorization endpoint in is sent to the response header of the front end, and then the browser will be redirected to the authorization page , Waiting for user authorization .
OAuth2LoginAuthenticationFilter
public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Override
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(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 = authorizationRequest.getAttribute(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);
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;
}
}
OAuth2LoginAuthenticationFilter It's very simple , The address of the authorization server is the response , The core is OAuth2LoginAuthenticationProvider Yes OAuth2LoginAuthenticationToken Certification of ,
OAuth2LoginAuthenticationToken
OAuth2LoginAuthenticationProvider
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
... Omitted code
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
(OAuth2LoginAuthenticationToken) authentication;
if (authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) {
return null;
}
OAuth2AccessTokenResponse accessTokenResponse;
try {
OAuth2AuthorizationExchangeValidator.validate(
authorizationCodeAuthentication.getAuthorizationExchange());
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
accessTokenResponse.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
... Omitted code
}
OAuth2LoginAuthenticationProvider The execution logic of is very simple , First, through code obtain access_token, And then through access_token Get user information , This and the standard oauth2 Authorization code pattern is consistent .
Automatic configuration
stay spring In the example of the guide , We found that it was just a simple configuration oauth2Login() Method , A complete oauth2 The authorization process is established , In fact, it's all due to spring-boot Of autoconfigure, We find spring-boot-autoconfigure.jar In bag security.oauth2.client.servlet package , You can find spring-boot Several autoconfiguration classes are provided for us :
OAuth2ClientAutoConfiguration
OAuth2ClientRegistrationRepositoryConfiguration
OAuth2WebSecurityConfiguration
among OAuth2ClientAutoConfiguration Imported OAuth2ClientRegistrationRepositoryConfiguration and OAuth2WebSecurityConfiguration Configuration of
OAuth2ClientRegistrationRepositoryConfiguration:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
class OAuth2ClientRegistrationRepositoryConfiguration {
@Bean
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
OAuth2ClientRegistrationRepositoryConfiguration The client Constructed as ClientRegistration And save it in memory . There's a hidden CommonOAuth2Provider class , This is an enumeration class , There are several commonly used parameters of three-party login authorization server, such as GOOGLE、GITHUB、FACEBOO、OKTA
CommonOAuth2Provider
public enum CommonOAuth2Provider {
GOOGLE {
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
builder.scope("openid", "profile", "email");
builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
builder.userNameAttributeName(IdTokenClaimNames.SUB);
builder.clientName("Google");
return builder;
}
},
GITHUB {
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
builder.scope("read:user");
builder.authorizationUri("https://github.com/login/oauth/authorize");
builder.tokenUri("https://github.com/login/oauth/access_token");
builder.userInfoUri("https://api.github.com/user");
builder.userNameAttributeName("id");
builder.clientName("GitHub");
return builder;
}
},
FACEBOOK {
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL);
builder.scope("public_profile", "email");
builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");
builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");
builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email");
builder.userNameAttributeName("id");
builder.clientName("Facebook");
return builder;
}
},
OKTA {
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
builder.scope("openid", "profile", "email");
builder.userNameAttributeName(IdTokenClaimNames.SUB);
builder.clientName("Okta");
return builder;
}
};
private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";
protected final ClientRegistration.Builder getBuilder(String registrationId,
ClientAuthenticationMethod method, String redirectUri) {
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId);
builder.clientAuthenticationMethod(method);
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
builder.redirectUriTemplate(redirectUri);
return builder;
}
public abstract ClientRegistration.Builder getBuilder(String registrationId);
}
That's why we don't configure github The reason why the authorization endpoint can jump to the authorization page .
OAuth2WebSecurityConfiguration
OAuth2WebSecurityConfiguration Configure some web Related to the class , Like how to save and get authorized clients , And default oauth2 Client related configuration
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ClientRegistrationRepository.class)
class OAuth2WebSecurityConfiguration {
@Bean
@ConditionalOnMissingBean
OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
@Bean
@ConditionalOnMissingBean
OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.oauth2Login(Customizer.withDefaults());
http.oauth2Client();
}
}
}
Reference resources :
Integrate GitHub and QQ Social login
https://github.com/Allurx/spring-security-oauth2-demo/tree/master/spring-security-oauth2-client
spring-security-oauth Update the route
https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update
spring-security Yes oauth2.0 Authorization server support
https://github.com/spring-projects/spring-security/issues/6320
Use spring-boot and oauth2.0 Building social logins
https://spring.io/guides/tutorials/spring-boot-oauth2/
Recommended reading
Code comparison tool , I'll use this 6 individual
Share my favorite 5 A free online SQL Database environment , It's so convenient !
Spring Boot A combination of three moves , Hand in hand to teach you to play elegant back-end interface
MySQL 5.7 vs 8.0, You choose the one ? Net friend : I'm going to stay where I am ~
Last , I recommend you an interesting and interesting official account : The guy who wrote the code ,7 Old programmers teach you to write bug, reply interview | resources Send you a complete set of Development Notes There's a surprise