Configuration and principle analysis of Spring Security Oauth2 SSO single sign on

Posted by Jay on Mon, 15 Jun 2020 04:15:32 +0200

 

Single sign on (SSO) is a one-time authentication login by users. After a user logs in on the identity authentication server once, he / she can gain access to other associated systems and application software in the single sign on system. At the same time, this implementation does not require the administrator to modify the user's login status or other information, which means that in multiple application systems, the user can access all mutually trusted application systems only by logging in once .

With more and more enterprise systems, such as office automation (OA) system, financial management system, file management system, information query system, etc., the problem of login becomes more and more important. It's not easy to record so many user names and passwords. In order to facilitate memory, many people use the same user name and password in different sites. Although this can reduce the burden, it also reduces the security. Moreover, using different sites also requires multiple logins. For the above reasons, it is very important to provide a smooth login channel for users.

Single sign on (SSO) is a secure communication technology that helps users access multiple sites in the network quickly. Single sign on system is based on a secure communication protocol, which realizes single sign on through the exchange of user identity information among multiple systems. When using single sign on system, users can access multiple systems only by logging in once, without memorizing multiple passwords. Single sign on enables users to access the network quickly, which improves work efficiency and helps improve the security of the system.

OAuth protocol provides a safe, open and simple standard for user resource authorization. The difference with the previous authorization method is that the authorization of OAuth will not make the third party touch the user's account information (such as user name and password), that is, the third party can apply for authorization of the user's resources without using the user's user name and password, so OAuth is secure. OAuth is short for Open Authorization.

 

Although OAuth2 was originally used to allow users to authorize third-party applications to access their resources, it was not used for single sign on, but we can use its features to realize single sign on in disguise, in which the authorization code mode is used, and the token generation uses JWT.

Next, we establish a set of projects, including authorization platform, OA integrated office platform, CRM mobile marketing platform, to simulate the single sign on process, elaborate its configuration, and conduct in-depth analysis for its principle.

 

Authorization platform

 

pom

 

The final pom dependencies are as follows:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <parent>
        <artifactId>spring-security-oauth2-sso-sample</artifactId>
        <groupId>org.xbdframework.sample</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
​
    <groupId>org.xbdframework.sample</groupId>
    <artifactId>sso-auth-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
​
    <name>sso-auth-server</name>
    <description>Demo project for Spring Boot</description>
​
    <properties>
    </properties>
​
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
​
</project>

 

Note that the spring security oauth2 autoconfigure dependency is essential, which is the spring boot project, not the spring cloud project. For spring cloud, just introduce oauth2 starter.

EnableAuthorizationServer

The authorization server is configured as follows:

package org.xbdframework.sample.sso.authserver.confg;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
​
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
​
    @Autowired
    private PasswordEncoder passwordEncoder;
​
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .tokenKeyAccess("isAuthenticated()");
    }
​
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(a1());
    }
​
    @Bean
    public ClientDetailsService a1() throws Exception {
        return new InMemoryClientDetailsServiceBuilder()
                // client oa application
                .withClient("oa")
                .secret(passwordEncoder.encode("oa_secret"))
                .scopes("all")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://localhost:8080/oa/login", "http://www.baidu.com")
                .accessTokenValiditySeconds(7200)
                .autoApprove(true)
​
                .and()
​
                // client crm application
                .withClient("crm")
                .secret(passwordEncoder.encode("crm_secret"))
                .scopes("all")
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://localhost:8090/crm/login")
                .accessTokenValiditySeconds(7200)
                .autoApprove(true)
​
                .and()
                .build();
    }
​
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter())
                .tokenStore(jwtTokenStore());
    }
​
    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
​
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("123456");
        return jwtAccessTokenConverter;
    }
​
}

 

WebSecurityConfiguration

 

Spring Security is configured as follows:

package org.xbdframework.sample.sso.authserver.confg;
​
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
​
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
​
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
​
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());
    }
​
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
    }
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest()
                .authenticated()
                .and().csrf().disable().cors();
    }
​
    @Bean
    @Override
    public UserDetailsService userDetailsServiceBean() {
        Collection<UserDetails> users = buildUsers();
​
        return new InMemoryUserDetailsManager(users);
    }
​
    private Collection<UserDetails> buildUsers() {
        String password = passwordEncoder().encode("123456");
​
        List<UserDetails> users = new ArrayList<>();
​
        UserDetails user_admin = User.withUsername("admin").password(password).authorities("ADMIN", "USER").build();
        UserDetails user_user1 = User.withUsername("user 1").password(password).authorities("USER").build();
​
        users.add(user_admin);
        users.add(user_user1);
​
        return users;
    }
​
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
​
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
​
}

As for the subsequent creation of login page, home page, etc., it is relatively simple and will not be described in detail. Please refer to the project link at the end of the article to view the specific code. After downloading, you can view it by yourself.

 

OA integrated office platform

 

pom

 

The final pom dependencies are as follows:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <parent>
        <artifactId>spring-security-oauth2-sso-sample</artifactId>
        <groupId>org.xbdframework.sample</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
​
    <groupId>org.xbdframework.sample</groupId>
    <artifactId>sso-oa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
​
    <name>sso-oa</name>
    <description>Demo project for Spring Boot</description>
​
    <properties>
    </properties>
​
    <dependencies>
        <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.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
​
</project>

Note that the spring security oauth2 autoconfigure dependency is essential, which is the spring boot project, not the spring cloud project. For spring cloud, just introduce oauth2 starter.

 

WebSecurityConfiguration

 

Spring Security is configured as follows:

package org.xbdframework.sample.sso.oa.config;
​
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
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.WebSecurityConfigurerAdapter;
​
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
​
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.logout()
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }
}

 

In particular, don't forget the @ EnableOAuth2Sso annotation, which is the entry for automatic configuration related to single sign on.

 

CRM mobile marketing platform

 

The related configuration is the same as that of OA platform, and will not be described in detail.

 

Single sign on process

 

After the system is established, we start authorization system (8888), OA platform (8080) and CRM platform (8090). Access to OA platform http://localhost:8080/oa/system/profile. At this time, the browser will be redirected to the authorized system login page and login is required.

At this time, we will check the Network tag and its access path, and we will see that we have jumped to the login page of the commodity system first( http://localhost:8080/oa/login), and then redirect to the authorization link of the authorization system. Because it is not logged in, it is finally redirected to the login interface of the authorization system, as shown in the figure:

Log in with admin/123456 and successfully jump to the profile page of OA platform.

As you can see, the admin user has successfully logged in. At this point, review the Network label again.

After a successful login, the authorization system redirects to the callback address configured by the OA platform( http://localhost:8080/oa/login), at the same time, it carries two parameters, code and state. The most important one is code (the state parameter is set to prevent CSRF attacks, which is not discussed here). According to this code, the client can access the token interface of the authorization system (/ oauth/token) and apply for a token. After successful application, redirect to the callback address configured by OA platform( http://localhost:8080/oa/login).

Then, we click "CRM - mobile marketing platform" (it can also be accessed directly by entering the address in the browser, and the effect is the same. The access address of CRM platform is: http://localhost:8090/crm/sysmtem/profile), at this time, we do not need to log in, we can directly access the page.

Check the Network tab again.

It can be seen that the browser redirects to the login page of the CRM platform, then to the authorization link of the authorization system, and then directly to the login page of the CRM platform. However, this visit does not need to be redirected to the authorization system again for login, but successfully accesses the authorization interface of the authorization system, And carries the code to redirect to the callback path of CRM platform. Then according to this code, the framework accesses the authorized token interface (/ OA uth / token) again, and obtains the token successfully, which can normally access the protected resources.

Why can I get the user information of the first login directly without logging in for the second visit? How can I get it without any confusion?

Thanks to Spring Security. The first Filter of Spring Security is the SecurityContextPersistenceFilter. What's the use? Literally, it is the security context persistence Filter, which stores the authenticated user information. If the user requests subsequent access, it can be taken out and used directly.

The focus is on the loadContext of the HttpSessionSecurityContextRepository class.

Let's see how the readSecurityContextFromSession method gets the SecurityContext.

After the Spring Security is authenticated successfully, it will write some properties to the Session, and the key value is SPRING_SECURITY_CONTEXT. Later, this information can be obtained according to the Session in the Request, including the login user, permissions and other information.

Based on the same browser visiting the same website, its JSESSIONID is fixed. Therefore, when accessing the OA platform, the browser will be redirected to the authorization platform, and a JSESSIONID will be generated to identify the current login user, and then redirected back to the OA platform callback address; when accessing the CRM platform again, it will also be redirected to the authorization platform, at this time, the authorization platform can directly obtain the information of the last login user according to the unique JSESSIONID generated previously. The author of this section also searched for a long time before finding here. It's really not easy!

As shown in the figure, OA platform and CRM platform are the same JSESSIONID when accessing authorization platform.

When users access the server, they will open a session for each user. The browser is based on JSESSIONID to determine which user this session belongs to. JSESSIONID is used to determine which session the current user corresponds to. In other words, the way the server recognizes the session is to tell the server where the client's session is in memory through JSESSIONID.

 

client

 

Let's analyze how the client triggers a series of requests.

As mentioned earlier, an annotation is very important, @ EnableOAuth2Sso. Relying on this annotation, the framework automatically registers the OAuth2ClientAuthenticationProcessingFilter instance. From the name we can see what is the use. Among them, the important logic is as follows:

The second part doesn't say anything. The focus is on the first part, getting token from OAuth2RestTemplate.

As you can see, the framework first obtains the cached token from OAuth2ClientContext, if not, then calls acquireAccessToken method to obtain it. Thrown if a UserRedirectRequiredException exception occurs. Remember that the exception thrown here will trigger a series of subsequent authorization, login and other redirects.

Let's start with the acquireAccessToken method.

Nothing else matters. The point is accessTokenProvider.obtainAccessToken In this sentence, the accesstokenprovider is nothing else. It is the AuthorizationCodeAccessTokenProvider. Second, once the token is obtained, it is cached in OAuth2ClientContext. Therefore, subsequent requests can get token directly from OAuth2ClientContext, which is the reason.

When I first visited the OA platform, there was no code or state because I did not log in or apply for authorization. Therefore, the framework will generate a UserRedirectRequiredException and return it, which will be caught and thrown by the getAccessToken method of OAuth2RestTemplate.

So, what does the exception framework do to trigger redirection?

The answer is OAuth2ClientContextFilter. In the subsequent filter process, redirection will be triggered. The logic is as follows:

After the redirection returns the callback url, it's / login just like the profile page to the client application's own login page, but it's intercepted by OAuth2ClientAuthenticationProcessingFilter. The intercepting path is / login. The difference is that the former is the subsequent operation of the latter after a series of operations, that is, to visit the profile page and jump to the client application's own login page, which is intercepted by OAuth2ClientAuthenticationProcessingFilter, and then a UserRedirectRequiredException exception occurs, which is redirected to the authorized service to apply for authorization, and then redirected to the login page after the application is successful, and then successfully obtained according to the code Get the token.

Next, we will explain the AccessTokenRequest object and OAuth2ClientContext object. The declarations of these two bean s are as follows:

As you can see, the AccessTokenRequest object scope is request, which is valid for each HTTP request. That is to say, in an HTTP request, each Bean definition corresponds to an instance. The scope of OAuth2ClientContext object is session, which is valid for each HTTP Session, that is, in an HTTP Session, each Bean definition corresponds to an instance. In this way, different requests and different users are distinguished.

 

UML sequence diagram

 

The sequence diagram of the whole authorization process is as follows:

 

Source code

 

Attached source code for reference, welcome star, fork!

 

github

 

https://github.com/liuminglei/spring-security-oauth2-sso-sample.git

 

gitee

 

https://gitee.com/xbd521/spring-security-oauth2-sso-sample.git

 

 

 

Reply to the following keywords for more resources

 

Spring cloud's way to advance | JAVA foundation | microservice | JAVA WEB | JAVA advanced | JAVA interview | MK elaboration

 

 

The Milky Way architect has opened the personal WeChat official account, sharing the experience of work and life, filling the pit guide, and understanding the technology. It will be updated earlier than blogs, and welcome subscriptions.

 

Topics: Spring Maven Session Java