The background,

In Spring Security 5, authorization server configuration is no longer provided, but we usually use authorization server in the development process. However, Spring officially provides a spring-led, community-driven authorization service, Spring-authorization-Server, which is currently in version 0.1.2. However, this project is still an experimental project and cannot be used in production environments. Set up a simple authorization server using the project here.

Second, pre-knowledge

1. Understand oAUTH2 protocol and process. 2. Concepts of JWT, JWS and JWK

JWT: indicates a JSON Web Token, consisting of header. paypay. signture. A JWT without a signature is insecure, and a JWT with a signature cannot be tampered with. JWS: A signed JWT, that is, a JWT with a signature. JWK: Since it involves signature, it involves signature algorithm, symmetric encryption or asymmetric encryption, so you need encryption key or public and private key pair. Here we refer to the JWT KEY or public/private KEY pair as a JSON WEB KEY, or JWK.

3, requirements,

1. Complete the authorization-code process.

The most secure process requires user participation.

2. Complete the client credentials process.

Without user participation, it can be used for access between internal systems, or no user participation is required between systems.

3. The simplified pattern has been deprecated in the new Spring-authorization-server project. Refresh the token. 5. Revoke the token. 6. View the issued token information. 7. View JWK information. 8. Personalize the JWT token by adding additional information to the JWT token.

Completed case: Zhang SAN logs in CSDN website through QQ. After login, CSDN can obtain the token issued by QQ, and CSDN website can obtain the personal information of Zhang SAN on the QQ resource server with the token.

Role Analysis Zhang SAN: The user is the resource owner. CSDN: client QQ: authorization server Personal information: the user’s resources are saved in the resource server

4. Core code writing

1. Introduce authorization server dependencies

<dependency>
    <groupId>org.springframework.security.experimental</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.1.2</version>
</dependency>
Copy the code

2. Create an authorization server user

Zhang SAN logs in CSDN website through QQ.

The user Zhang SAN is created here. This user Zhang SAN is the user of the authorization server, which is the user of the QQ server here.

@EnableWebSecurity
public class DefaultSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin();
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    // Create a user here.
    @Bean
    UserDetailsService users(a) {
        UserDetails user = User.builder()
                .username("zhangsan")
                .password(passwordEncoder().encode("zhangsan123"))
                .roles("USER")
                .build();
        return newInMemoryUserDetailsManager(user); }}Copy the code

3. Create an authorization server and client

Zhang SAN logs in CSDN website through QQ.

The QQ authorization server and client CSDN are created here.

package com.huan.study.authorization.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
importorg.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConf igurer;import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;

/** * Authentication server configuration **@authorHuan.fu 2021/7/12-2:08 PM */
@Configuration
public class AuthorizationConfig {

    @Autowired
    privatePasswordEncoder passwordEncoder; ` ` `/** * Personalize JWT token */
    class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

        @Override
        public void customize(JwtEncodingContext context) {
            // Add a custom header
            context.getHeaders().header("client-id", context.getRegisteredClient().getClientId()); }} ` ` `/** * Defines the interceptor chain for Spring Security */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
   
        // Set JWT token personalization
        http.setSharedObject(OAuth2TokenCustomizer.class, new CustomOAuth2TokenCustomizer());

        // Authorization server configuration
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        return http
                .requestMatcher(endpointsMatcher)
                .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer)
                .and()
                .formLogin()
                .and()
                .build();
    }

    /** * Create client information, can be saved in memory and database, save in the database here */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // The client ID must be unique
                .clientId("csdn")
                // Client password
                .clientSecret(passwordEncoder.encode("csdn123"))
                // Can be based on basic mode and authorization server authentication
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                / / authorization code
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                / / refresh token
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // Client mode
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // Password mode
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                // Simplified mode, obsolete, not recommended
                .authorizationGrantType(AuthorizationGrantType.IMPLICIT)
                // Redirect the URL
                .redirectUri("https://www.baidu.com")
                // The client application scope, also can understand the client application to access the user's information, such as: get user information, get user photos, etc
                .scope("user.userInfo")
                .scope("user.photos")
                .clientSettings(clientSettings -> {
                    // Does the user need to confirm which permissions the client needs to obtain from the user
                    // For example, the client needs to obtain user information and user photos, but the user can control to authorize the client only to obtain user information.
                    clientSettings.requireUserConsent(true);
                })
                .tokenSettings(tokenSettings -> {
                    // accessToken valid date
                    tokenSettings.accessTokenTimeToLive(Duration.ofHours(1));
                    // The validity period of refreshToken
                    tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3));
                    // Whether the refresh token is reusable
                    tokenSettings.reuseRefreshTokens(true);
                })
                .build();

        JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        if (null == jdbcRegisteredClientRepository.findByClientId("csdn")) {
            jdbcRegisteredClientRepository.save(registeredClient);
        }

        return jdbcRegisteredClientRepository;
    }

    /** * save the authorization information, the authorization server issued to us the token, then we definitely need to save, by this service to save */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);

        class CustomOAuth2AuthorizationRowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
            public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
                super(registeredClientRepository);
                getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
                this.setLobHandler(new DefaultLobHandler());
            }
        }

        CustomOAuth2AuthorizationRowMapper oAuth2AuthorizationRowMapper =
                new CustomOAuth2AuthorizationRowMapper(registeredClientRepository);

        authorizationService.setAuthorizationRowMapper(oAuth2AuthorizationRowMapper);
        return authorizationService;
    }

    /** * If it is an authorization code process, the client may apply for multiple permissions, such as: obtain user information, modify user information, this Service handles the user to the client which permissions, such as only obtain user information */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /** * Encryption key to sign JWT */
    @Bean
    public JWKSource<SecurityContext> jwkSource(a) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    /** * JWT decode */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /** * Configure the path of some breakpoints, such as: obtain token, authorize endpoint, etc. */
    @Bean
    public ProviderSettings providerSettings(a) {
        return new ProviderSettings()
                // Configure the endpoint path to obtain the token
                .tokenEndpoint("/oauth2/token")
                // The url of the publisher, usually the root path for the system to access
                // qq.com needs to modify the host file of our system
                .issuer("http://qq.com:8080"); }}Copy the code

Note ⚠ ️ :1. Map QQ.com to 127.0.0.1 in the host file of the system. 2. Since client information and authorization information (token information, etc.) are stored in the database, the table needs to be built well.3. For details, see the comments for the code above

Five, the test

You can see from the code above:

Resource owner: Zhangsan User name and password: zhangsan/zhangsan123 Client information: CSDN clientId and clientSecret: CSDN/CSdn123 Authorization server address: The value of QQ.com clientSecret cannot be leaked to the client and must be saved on the server.

1. Authorization code process

1. Obtain the authorization code

Qq.com: 8080 / oauth2 / auth… user.userInfo

Response_type =code: return authorization code scope= user.userinfo user.userinfo: Multiple permissions are obtained separated by Spaces. Redirect_uri =www.baidu.com: indicates a forward request that is approved or rejected by the user

2. Obtain the token based on the authorization code

curl -i -X POST \ -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ 'http://qq.com:8080/oauth2/token?grant_type=authorization_code&code=tDrZ-LcQDG0julJBcGY5mjtXpE04mpmXjWr9vr0-rQFP7UuNFIP6 kFArcYwYo4U-iZXFiDcK4p0wihS_iUv4CBnlYRt79QDoBBXMmQBBBm9jCblEJFHZS-WalCoob6aQ&redirect_uri=https%3A%2F%2Fwww.baidu.com'Copy the code

Authorization: Base64 value of clientId and clientSecret Grant_type =authorization_code Indicates the Authorization code. Code = XXX: indicates the Authorization code obtained in the previous step

3. Process demonstration

2. Obtain the token based on the refresh token

curl -i -X POST \ -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ 'http://qq.com:8080/oauth2/token?grant_type=refresh_token&refresh_token=Wpu3ruj8FhI-T1pFmnRKfadOrhsHiH1JLkVg2CCFFYd7bYPN -jICwNtPgZIXi3jcWqR6FOOBYWo56W44B5vm374nvM8FcMzTZaywu-pz3EcHvFdFmLJrqAixtTQZvMzx'Copy the code

3. Client mode

In this mode, only the client and the authorization server participate in this mode.

curl -i -X POST \
   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
 'http://qq.com:8080/oauth2/token?grant_type=client_credentials'
Copy the code

4. Revoke the token

The curl - I - X POST \ 'token' http://qq.com:8080/oauth2/revoke?token=Copy the code

5. View the token information

curl -i -X POST \
   -H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
 'http://qq.com:8080/oauth2/introspect?token=XXX'
Copy the code

6. View JWK information

curl -i -X GET \
 'http://qq.com:8080/oauth2/jwks'
Copy the code

Vi. Complete code

Gitee.com/huan1993/sp…

7. Reference address

1, github.com/spring-proj…