In learning Spring Cloud, encountered authorization service oAUth related content, always half-understanding, so I decided to first Spring Security, Spring Security Oauth2 and other permissions, authentication related content, principle and design study and sort out.

Spring Security Analysis (vi) — JWt-based single sign-on (SSO) development and principle analysis

In learning Spring Cloud, encountered authorization service oAUth related content, always half-understanding, so I decided to first Spring Security, Spring Security Oauth2 and other permissions, authentication related content, principle and design study and sort out. This series of articles is written in the process of learning to strengthen the impression and understanding, if there is infringement, please inform.

Project Environment:

  • JDK1.8

  • Spring boot 2.x

  • Spring Security 5.x

Single Sign On, or SSO for short, is one of the more popular solutions for enterprise business integration. SSO is defined as one login in multiple applications that allows users to access all trusted applications. Single sign-on is also essentially a use of OAuth2, so its development relies on the authorization service, as you can see in my last article if it’s not clear.

Single sign-on Demo development

From the definition of single sign-on we know that we need to create a new application, which I will call security-Sso-client. The rest of the development is in this application.

Maven dependency

It mainly relies on spring-boot-starter-security, spring-security-oAuth2-autoconfigure, and spring-security-oAuth2. Spring-security-oauth2-autoconfigure = spring Boot 2.x

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <! - @ EnableOAuth2Sso introduction, Spring Boot 2 x move this annotation to the dependent package - > < the dependency > < groupId > org. Springframework. Security. Request. The Boot < / groupId > <artifactId>spring-security-oauth2-autoconfigure</artifactId> <exclusions> <exclusion> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> </exclusion> < / exclusions > < version > 2.1.7. RELEASE < / version > < / dependency > <! - not the starter, manual configuration - > < the dependency > < groupId > org. Springframework. Security. Request < / groupId > <artifactId>spring-security-oauth2</artifactId> <! - please note that the spring - authorization - oauth2 version Must be higher than the 2.3.2 RELEASE, this is the official a bug: Java. Lang. NoSuchMethodError: Org. Springframework. Data. Redis. Connection. RedisConnection. Set ([[B V B) requirements must be greater than 2.3.5 version, the official explanation: https://github.com/BUG9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/ The open - > < version > 2.3.5. RELEASE < / version > < / dependency >Copy the code

2. Sso @enableoAuth2SSO

Single point base configuration import is implemented by @enableoAuth2SSO, On Spring Boot 2.x and above @enableoAuth2SSO is in the Spring-security-oAuth2-Autoconfigure dependency. Here I have a simple configuration:

@Configuration@EnableOAuth2Ssopublic class ClientSecurityConfig extends WebSecurityConfigurerAdapter {  @Override  public void configure(HttpSecurity http) throws Exception {      http.authorizeRequests()              .antMatchers("/","/error","/login").permitAll()              .anyRequest().authenticated()              .and()              .csrf().disable();  }}
Copy the code

Since there may be some problems during a single point that redirect to /error, we set /error to no access.

Three, test interface and page

The test interface
@RestController@Slf4jpublic class TestController {    @GetMapping("/client/{clientId}")    public String getClient(@PathVariable String clientId) {        return clientId;    }}
Copy the code
The test page
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>OSS-client</title> </head> <body> < h1 > OSS - client < / h1 > < a href = "http://localhost:8091/client/1" > to jump to the OSS - the client - 1 < / a > < a Href = "http://localhost:8092/client/2" > to jump to the OSS - the client - 2 < / a > < / body > < / HTML >Copy the code

4. Single point Configuration File Configure authorization information

Since we need to test a single point between multiple applications, we need at least two single point clients, which I achieved through Spring Boot’s multi-environment configuration.

Application. Yml configuration

We all know that the single point implementation is essentially the authorization code pattern of Oauth2, so we need to configure the address information to access the authorization server, including:

  • Security. The oauth2. Client. The user authorization – uri = / request/the authorize request the address of the certification, obtaining the code code

  • Security. The oauth2. Client. The access token – uri = / request/token request token’s address

  • Security. Oauth2. Resource. JWT. Key – uri = / request/token_key parsing JWT token key needed to address, when the service invoked the interface to get the authorization service JWT key, so be sure to ensure the normal order of the authorization service

  • Security. Oauth2. Client. The client – id = client1 clientId information

  • Security. Oauth2. Client. The client – secret = 123456 clientSecret information

There are a few configurations that need to be explained briefly:

  • Security. Oauth2. Sso. Login – path = / login oauth2 authorization server triggers redirect to the client’s path, the default is/login, the path to the license server path after the callback address (domain name)

  • Server. The servlet. Session. Cookies. Name = OAUTH2CLIENTSESSION solve problems of single machine development, if is a stand-alone development but ignore its configuration

    Auth-server: http://localhost:9090 # Authorization service address Security :oauth2: client: user-authorization-uri: {auth-server}/oauth/authorize # Authorize address Access-tok-uri: {auth-server}/oauth/token # Address of request token resource: JWT: key-uri: {auth-server}/oauth/token: ${auth-server}/oauth/token_key # ${auth-server}/oauth/token_key # ${auth-server}/oauth /login # the path to the login page, that is, the path triggered by the OAuth2 authorization server to redirect to the client. The default is/loginServer :servlet: session: cookie: name: Possible CSRF detected – State parameter was required but no state could be found Problem Spring: Profiles: Active: client1

Application – client1. Yml configuration

Application-client2 is the same as application-client1, except that the port number and client information are different.

server:port: 8091security:oauth2:  client:    client-id: client1    client-secret: 123456
Copy the code

5. Single point testing

The effect is as follows:

[Slightly, the picture can not be transmitted…, please see the original text]

From the renderings, we can see that when we first access the interface of Client2, we jump to the login interface of the authorization service. After login, we successfully jump back to the test interface of Client2 and display the interface return value. When we access client1’s test interface, we directly return the interface return value. This is the effect of single sign-on, and curious students must be asking themselves: how does it work? So let’s take the veil off.

Second, single sign-on principle analysis

A, @ EnableOAuth2Sso

We all know that @enableoAuth2SSO is the most important configuration annotation to implement single sign-on (SSO).

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@EnableOAuth2Client@EnableConfigurationProperties (OAuth2SsoProperties.class)@Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class, ResourceServerTokenServicesConfiguration.class })public @interface EnableOAuth2Sso {}Copy the code

We focus on four configuration file references: ResourceServerTokenServicesConfiguration, OAuth2SsoDefaultConfiguration, OAuth2SsoProperties and @ EnableOAuth2Client:

  • OAuth2SsoDefaultConfiguration single-sign-on core configuration, the internal created SsoSecurityConfigurer object, SsoSecurityConfigurer internal main is one of the core filter configuration OAuth2ClientAuthenticationProcessingFilter this single sign-on.

  • ResourceServerTokenServicesConfiguration internal read our configuration information in the yml

  • OAuth2SsoProperties configured a callback url address, this is the security. The oauth2. Sso. Login – path = / login to match

  • @ EnableOAuth2Client indicate a single point of the client, its main configuration inside the core filter OAuth2ClientContextFilter this single sign-on

Second, the OAuth2ClientContextFilter

OAuth2ClientContextFilter filter is similar to ExceptionTranslationFilter, itself doesn’t do any filter processing, as long as when chain. The doFilter () make a redirect after abnormal processing. But don’t underestimate this redirection, it is the first step in the implementation of single sign-on, remember the first single point to the authorization server login page? And this function is OAuth2ClientContextFilter implementation. Let’s take a look at the source code:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; request.setAttribute(CURRENT_URI, calculateCurrentUri(request)); // 1, record the current address (currentUri) to HttpServletRequest try {chain.dofilter (servletRequest, servletResponse); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer .getFirstThrowableOfType( UserRedirectRequiredException.class, causeChain); if (redirect ! = null) {/ / 2, determine whether the current exception UserRedirectRequiredException object is empty redirectUser (redirect, request, response); // 3, authorize /oauth/authorize} else {if (ex instanceof ServletException) {throw (ServletException) ex; } if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } throw new NestedServletException("Unhandled exception", ex); }}}Copy the code

The Debug to see:

The whole filter is divided into three steps:

  • 1. Log the current address (currentUri) to HttpServletRequest

  • 2, whether the current exception UserRedirectRequiredException object is empty

  • 3, Redirect access authorization service /oauth/authorize

Third, OAuth2ClientAuthenticationProcessingFilter

OAuth2ClientContextFilter filter The work to be done is by getting to the code code calls the authorization service/request/token interface for token information, The obtained token information is parsed into an OAuth2Authentication object. The origins are as follows:

@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { OAuth2AccessToken accessToken; try { accessToken = restTemplate.getAccessToken(); } Catch (OAuth2Exception e) {BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e); publish(new OAuth2AuthenticationFailureEvent(bad)); throw bad; } try { OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue()); / / 2, parsing for OAuth2Authentication authentication token information object and returns the if (authenticationDetailsSource! =null) { request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue()); request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType()); result.setDetails(authenticationDetailsSource.buildDetails(request)); } publish(new AuthenticationSuccessEvent(result)); return result; } catch (InvalidTokenException e) { BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e); publish(new OAuth2AuthenticationFailureEvent(bad)); throw bad; }}Copy the code

Filter 2 features:

  • restTemplate.getAccessToken(); 1. Invoke the authorization service to obtain the token

  • tokenServices.loadAuthentication(accessToken.getValue()); // 2. Parse the token information into an OAuth2Authentication object and return

    After completing the above steps, it will be a normal security authorization process. I will not talk about it here. If you are not clear, you can take a look at the relevant article I wrote.

Four, AuthorizationCodeAccessTokenProvider

In telling OAuth2ClientContextFilter there is nothing, that is UserRedirectRequiredException who is thrown out. It’s not about OAuth2ClientAuthenticationProcessingFilter has said, That is, how it determines whether the current /login is a code step or a token step (of course, it determines whether /login has a code parameter, which is mainly explained here). These two points are designed in AuthorizationCodeAccessTokenProvider this class. When is this class called? Actually OAuth2ClientAuthenticationProcessingFilter hidden in the restTemplate. GetAccessToken (); The method of internal call accessTokenProvider. ObtainAccessToken () here. We’ll look at OAuth2ClientAuthenticationProcessingFilter obtainAccessToken () method inside the source code:

public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request) throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException, OAuth2AccessDeniedException { AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details; If (request. GetAuthorizationCode () = = null) {/ / 1, determine whether the current parameters contain code code if (request) getStateKey () = = null) {throw getRedirectForAuthorization(resource, request); / / 2, does not contain an abnormal UserRedirectRequiredException} obtainAuthorizationCode (resource, request); } return retrieveToken(request, resource, getParametersForTokenRequest(resource, request), getHeadersForTokenRequest(request)); // select * from *;Copy the code

There are 3 steps in the whole method:

  • 1. Check whether the current parameter contains the code

  • 2, does not contain the thrown UserRedirectRequiredException anomalies

  • 3, including continue to obtain the token

Finally, some students may ask, why does the first client single point jump to the authorization service login page to log in, but when asked the second client does not, in fact, the process of the two client single point is the same, both are the authorization code mode, but why does client 2 not need to log in? It’s actually because of Cookies/ sessions, because we’re basically accessing the same 2 clients in the same browser. For those of you who don’t believe me, try accessing two single point clients in two different browsers.

Iii. Personal summary

Single sign-on is essentially an authorization code pattern, so it’s easy to understand. If you have to give a flow chart, it’s the same flow chart:

This article introduces the JWT based single sign-on (SSO) development and principle of the analysis of the development of code can access the code repository, project github address: github.com/BUG9/spring…

PS: In case you can’t find this article, please click “like” to browse and find it