Spring Security Analysis: Development and Principle Analysis of Single Sign-on (SSO) Based on JWT

Keywords: Java Spring github Session Redis

Spring Security Analysis (6) - Development and Principle Analysis of Single Sign-on (SSO) Based on JWT

When learning Spring Cloud, when encountering the related content of authorized service oauth, he always knows half of it, so he decided to study and collate the content, principle and design of Spring Security, Spring Security Oauth2 and other permissions, authentication-related content. This series of articles is written to enhance the impression and understanding in the process of learning. If there is any infringement, please let me know.

Project environment:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

Single Sign On (SSO) is one of the most popular solutions for business integration. SSO is defined as an application system in which users can access all trusted applications with only one login.
Single sign-on is essentially the use of OAuth2, so its development depends on authorization authentication services, if not clear, you can see my last article.

1. Development of single landing Demo

From the definition of single sign-on, we know that we need to build a new application. I named it security-sso-client. The next development is on this application.

Maven Dependence

_mainly depends on spring-boot-starter-security, spring-security-oauth2-autoconfigure and spring-security-oauth2. Spring-security-oauth2-autoconfigure is only available in 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 Introduce, Spring Boot 2.x Move this annotation to the dependency package-->
      <dependency>
          <groupId>org.springframework.security.oauth.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>
      <!-- No starter,Manual configuration -->
      <dependency>
          <groupId>org.springframework.security.oauth</groupId>
          <artifactId>spring-security-oauth2</artifactId>
          <!--Attention, please. spring-authorization-oauth2 The version must be higher than 2.3.2.RELEASEļ¼ŒThis is the official one. bug:
          java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
          Requirements must be greater than 2.3.5 Version, official explanation: https://github.com/BUG9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open
          -->
          <version>2.3.5.RELEASE</version>
      </dependency>

2. Single point configuration @EnableOAuth2Sso

_Single-point basic configuration introduction is implemented by @EnableOAuth2Sso, and @EnableOAuth2Sso in Spring Boot 2.x and above is in spring-security-oauth2-autoconfiguration dependency. Here I have a simple configuration:

@Configuration
@EnableOAuth2Sso
public class ClientSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  public void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
              .antMatchers("/","/error","/login").permitAll()
              .anyRequest().authenticated()
              .and()
              .csrf().disable();
  }
}

_Because there may be some problems during a single point, we redirect to / error, so we set / error as unauthorized access.

3. Test interface and page

Test interface
@RestController
@Slf4j
public class TestController {

    @GetMapping("/client/{clientId}")
    public String getClient(@PathVariable String clientId) {
        return clientId;
    }

}
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 "> Jump to OSS-client-1</a>
  <a href="http://Localhost: 8092/client/2 "> Jump to OSS-client-2</a>
  </body>
  </html>

IV. Single-Point Configuration File Configuration Authorization Information

_Since we need to test single points between multiple applications, we need at least two single-point clients, which I implemented through Spring Book's multi-environment configuration.

application.yml configuration

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

  • Security.oauth2.client.user-authorization-uri=/oauth/authorize requesting authentication address, i.e. obtaining code code code
  • Security.oauth2.client.access-token-uri=/oauth/token request token address
  • Security.oauth2.resource.jwt.key-uri=/oauth/token_key resolves the address of the key required by the JWT token. When the service starts, the authorization service will be invoked to obtain jwt key, so it is necessary to ensure that the authorization service is normal.
  • Security. oauth2. client. client-ID = client1 clientId information
  • Security. oauth2. client. client-secret = 123456 client Secret information

Several of these configurations need to be explained briefly:

  • Security.oauth2.sso.login-path=/login OAuth2 authorization server triggers a redirection path to the client, default to / login, which is the same path as the path after the authorization server's callback address (domain name).
  • server.servlet.session.cookie.name = OAUTH2CLIENTSESSION solves the problems of stand-alone development, but its configuration can be ignored if it is not stand-alone development.
auth-server: http://Localhost: 9090# authorization service address


security:
oauth2:
  client:
    user-authorization-uri: ${auth-server}/oauth/authorize #Address Requesting Authentication
    access-token-uri: ${auth-server}/oauth/token #The address of the request token
  resource:
    jwt:
      key-uri: ${auth-server}/oauth/token_key #Resolve the address of the key required by the jwt token. When the service starts, the authorization service will be called to obtain the jwt key. So it is necessary to ensure that the authorization service is normal.
  sso:
    login-path: /login #The path to the login page, where the OAuth2 authorization server triggers redirection to the client, defaults to / login

server:
servlet:
  session:
    cookie:
      name: OAUTH2CLIENTSESSION  # Solve the Possible CSRF detected - state parameter was required but no state could be found ed problem
spring:
profiles:
  active: client1
  

Application-client 1.yml configuration

Application-client 2 and application-client 1 are the same, but the port number and client information are different, so they are not repeated here.

server:
port: 8091

security:
oauth2:
  client:
    client-id: client1
    client-secret: 123456
  

V. Single point testing

The results are as follows:

_From the effect diagram, we can see that when we first visited the interface of client 2, we jumped to the login interface of authorized service. After completing the login, we successfully jumped back to the test interface of client 2, and displayed the return value of the interface. At this point, when we access the test interface of client1, we return the interface return value directly. This is the effect of single-point landing, curious students will ask in their hearts: how is it achieved? Then let's unveil it.

2. Analysis of Single-point Landing Principle

I @EnableOAuth2Sso

We all know that @EnableOAuth2Sso is the core configuration annotation for single sign-on. So let's look at the source code of @EnableOAuth2Sso:

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

}

Among them, we are concerned about the reference of four configuration files: ResourceServer Token Services Configuration, OAuth2SsoDefault Configuration, OAuth2SsoProperties and @Enable OAuth2Client:

  • The core configuration of OAuth2SsoDefaultConfiguration single-point login is that the SsoSecurity Configurer object is created internally. The main configuration of SsoSecurity Configurer is OAuth2Client Authentication Processing Filter, which is one of the core filters of single-point login.
  • ResourceServer Token Services Configuration reads the information we configure in yml internally
  • OAuth2SsoProperties configures the callback address url, which is matched by security.oauth2.sso.login-path=/login
  • @ Enable OAuth2Client identifies a single-point client, and its internal configuration is mainly OAuth2ClientContextFilter, one of the core filters for single-point login.

2. OAuth2ClientContextFilter

_OAuth2ClientContextFilter filter is similar to Exception TranslationFilter. It does not do any filtering itself, as long as it makes a redirection when chain.doFilter() has an exception. But don't underestimate this redirection process. It's the first step to achieve single-point login. Do you remember that the first single-point will jump to the authorized server's login page? This function is implemented by OAuth2ClientContextFilter. Let's 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 Uri 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. Redirected access authorization service/oauth/authorize 
            } else {
                if (ex instanceof ServletException) {
                    throw (ServletException) ex;
                }
                if (ex instanceof RuntimeException) {
                    throw (RuntimeException) ex;
                }
                throw new NestedServletException("Unhandled exception", ex);
            }
        }
    }

Look at Debug:

_The whole filter consists of three steps:

  • 1. Record the current Uri to HttpServletRequest
  • 2. Determine whether the current exception UserRedirectRequiredException object is empty
  • 3. Redirected access authorization service/oauth/authorize

#### 3. OAuth2Client Authentication Processing Filter
_OAuth2ClientContextFilter filters accomplish the task of obtaining token information by calling authorization service/oauth/token interface through acquired code code code, and parsing the acquired token information into OAuth2Authentication authentication object. The origins are as follows:

@Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        OAuth2AccessToken accessToken;
        try {
            accessToken = restTemplate.getAccessToken(); //1. Call authorization service to get token 
        } 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. Parse token information as OAuth2Authentication authentication object and return
            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;            
        }

    }

_The whole filter has two functions:

  • RestTemplate. getAccessToken ();//1, call authorization service to get token
  • TokenServices. load Authentication (accessToken. getValue ();//2, parse token information as OAuth2Authentication authentication object and return

    _After completing the above steps, it is a normal process of security authorization and authentication. I will not talk about it here. Some students who are not clear can see the relevant articles I wrote.

#### Authorization Code Access Token Provider
There's one thing I didn't say about OAuth2ClientContextFilter, which is who threw UserRedirectRequiredException. The OAuth2Client Authentication Processing Filter also fails to mention how it determines whether the current / login is a step that requires code code acquisition or a step that requires token acquisition (of course, whether / login has a code parameter or not, which is the main point here). These two points are all designed into the AuthorizationCodeAccessTokenProvider class. When was this class called?
Actually, the OAuth2Client Authentication Processing Filter is hidden in restTemplate.getAccessToken(); here is the accessTokenProvider.obtainAccessToken() that is called inside this method. Let's look at the internal source code of the obtainAccessToken() method of the OAuth2ClientAuthentication Processing Filter:

public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
            throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
            OAuth2AccessDeniedException {

        AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;

        if (request.getAuthorizationCode() == null) {  //1. Judging whether the current parameter contains code code code code 
            if (request.getStateKey() == null) {
                throw getRedirectForAuthorization(resource, request); //2. Throw a UserRedirectRequiredException exception if not included
            }
            obtainAuthorizationCode(resource, request);
        }
        return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
                getHeadersForTokenRequest(request)); // 3. Inclusion calls to get token 

    }

There are three steps in the whole method:

  • 1. Judging whether the current parameter contains code code code code
  • 2. Throw a UserRedirectRequiredException exception if not included
  • 3. Contains continued acquisition of token

Finally, some students may ask why the first client should jump to the authorization service landing page to login, while the second client does not. In fact, the process of the second client is the same, both are authorization code mode, but why does Client 2 not need to login? Actually, it's because of Cookies/Session, because our access to the same two clients is basically in the same browser. Unbelieving students can try two browsers to access two single-point clients.

III. Personal Summary

_Single sign-on is essentially authorization code mode, so it is easy to understand. If you have to give a flow chart, or that authorization code flow chart:

_This paper introduces the JWT-based single sign-on (SSO) development and principle analysis development code can access the code warehouse, project github address: https://github.com/BUG9/sprin...

If you are interested in these, you are welcome to support star, follow, collect and forward!

Posted by nootkan on Mon, 16 Sep 2019 04:13:25 -0700