10

1. Background

In Spring Security 5 authorization server configuration is no longer provided, but the authorization server is still used in our usual development process. However, Spring officially provides a community-driven authorization service spring-authorization-server led by Spring. It has reached version 0.1.2. However, this project is still an experimental project and cannot be used in a production environment. Use the project here to build A simple authorization server.

2. Pre-knowledge

1. Understand the oauth2 protocol and process. can refer to this article by
2. Concepts of JWT, JWS, JWK

JWT: refers to JSON Web Token, which is composed of header.payload.signture. A JWT without a signature is insecure, and a JWT with a signature cannot be tampered with.
JWS: refers to the signed JWT, that is, the signed JWT.
JWK: involves signature, it involves the signature algorithm, symmetric encryption or asymmetric encryption, then the encryption key or public-private key pair is required. Here we refer to the JWT key or public-private key pair as JSON WEB KEY, which is JWK.

3. Demand

1. Complete the authorization code ( authorization-code ) process.

The safest process requires the participation of users.

2. Complete the client ( client credentials ) process.

Without the participation of users, it can generally be used for access between internal systems, or there is no need for user participation between systems.

3. The simplified mode has been deprecated in the new spring-authorization-server project.
4. Refresh the token.
5. Withdraw the token.
6. View the issued token information.
7. View JWK information.
8. Personalize the JWT token, that is, add additional information to the JWT token.

completed case:
Zhang San used the QQ login method to log in to the CSDN website.
After logging in, CSDN can get the token personal information of Zhang San on the QQ resource server with the token.

role analysis
Zhang Three: The user is the resource owner
CSDN: client
QQ: authorization server
personal information: the user's resources, stored in the resource server

Four, core code writing

1. Introduce authorization server dependency

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

2. Create an authorization server user

Zhang San used the QQ login method to log in to the CSDN website.

Zhang San is completed here. This Zhang San is the user of the authorization server, and here is the user of the QQ server.

@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() {
        return new BCryptPasswordEncoder();
    }

    // 此处创建用户,张三。
    @Bean
    UserDetailsService users() {
        UserDetails user = User.builder()
                .username("zhangsan")
                .password(passwordEncoder().encode("zhangsan123"))
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}

3. Create an authorization server and client

Zhang San used the QQ login method to log in to the CSDN website.

The creation of the QQ authorization server and client CSDN is completed 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;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
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;

/**
 * 认证服务器配置
 *
 * @author huan.fu 2021/7/12 - 下午2:08
 */
@Configuration
public class AuthorizationConfig {

    @Autowired
    private PasswordEncoder passwordEncoder;
    
    /**
     * 个性化 JWT token
     */
    class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

        @Override
        public void customize(JwtEncodingContext context) {
            // 添加一个自定义头
            context.getHeaders().header("client-id", context.getRegisteredClient().getClientId());
        }
    }

    /**
     * 定义 Spring Security 的拦截器链
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 设置jwt token个性化
        http.setSharedObject(OAuth2TokenCustomizer.class, new CustomOAuth2TokenCustomizer());
        // 授权服务器配置
        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();
    }

    /**
     * 创建客户端信息,可以保存在内存和数据库,此处保存在数据库中
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端id 需要唯一
                .clientId("csdn")
                // 客户端密码
                .clientSecret(passwordEncoder.encode("csdn123"))
                // 可以基于 basic 的方式和授权服务器进行认证
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                // 授权码
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                // 刷新token
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 客户端模式
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 密码模式
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                // 简化模式,已过时,不推荐
                .authorizationGrantType(AuthorizationGrantType.IMPLICIT)
                // 重定向url
                .redirectUri("https://www.baidu.com")
                // 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等
                .scope("user.userInfo")
                .scope("user.photos")
                .clientSettings(clientSettings -> {
                    // 是否需要用户确认一下客户端需要获取用户的哪些权限
                    // 比如:客户端需要获取用户的 用户信息、用户照片 但是此处用户可以控制只给客户端授权获取 用户信息。
                    clientSettings.requireUserConsent(true);
                })
                .tokenSettings(tokenSettings -> {
                    // accessToken 的有效期
                    tokenSettings.accessTokenTimeToLive(Duration.ofHours(1));
                    // refreshToken 的有效期
                    tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3));
                    // 是否可重用刷新令牌
                    tokenSettings.reuseRefreshTokens(true);
                })
                .build();

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

        return jdbcRegisteredClientRepository;
    }

    /**
     * 保存授权信息,授权服务器给我们颁发来token,那我们肯定需要保存吧,由这个服务来保存
     */
    @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;
    }

    /**
     * 如果是授权码的流程,可能客户端申请了多个权限,比如:获取用户信息,修改用户信息,此Service处理的是用户给这个客户端哪些权限,比如只给获取用户信息的权限
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 对JWT进行签名的 加解密密钥
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() 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 解码
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 配置一些断点的路径,比如:获取token、授权端点 等
     */
    @Bean
    public ProviderSettings providerSettings() {
        return new ProviderSettings()
                // 配置获取token的端点路径
                .tokenEndpoint("/oauth2/token")
                // 发布者的url地址,一般是本系统访问的根路径
                // 此处的 qq.com 需要修改我们系统的 host 文件
                .issuer("http://qq.com:8080");
    }
}

Note⚠️:
1. It is necessary to map qq.com with 127.0.0.1 in the host file of the system.
2. Because the client information and authorization information (token information, etc.) are stored in the database, the table needs to be built.
客户端和授权信息表
3. For more information, see the comments of the code above

Five, test

As can be seen from the code above:

Resource owner: Zhang San User name and password are: zhangsan/zhangsan123
client information: CSDN clientId and clientSecret: csdn/csdn123
Authorization server address: qq.com
clientSecret cannot be leaked to the client and must be stored on the server.

1. Authorization code process

1. Obtain the authorization code

http://qq.com:8080/oauth2/authorize?client_id=csdn&response_type=code&redirect_uri=https://www.baidu.com&scope=user.userInfo user.userInfo

client_id=csdn: indicates who the client is
response_type=code: indicates that the authorization code is returned
scope=user.userInfo user.userInfo: Get multiple permissions separated by spaces
redirect_uri= after the user agrees or rejects

2. Obtain the token according to the authorization code

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

Authorization: Base64 value carrying specific clientId and clientSecret
grant_type=authorization_code means that the method used is authorization code
code=xxx: the authorization code obtained in the previous step

3. Process demonstration

在这里插入图片描述

2. Obtain token according to 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'

在这里插入图片描述

3. Client mode

In this mode, there is no user's participation, only the participation between the client and the authorization server.

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

客户端模式

4. Withdraw token

curl -i -X POST \
 'http://qq.com:8080/oauth2/revoke?token=令牌'

5. View token information

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

token详情

6. View JWK information

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

JWK信息

Six, complete code

https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server

Seven, reference address

1、https://github.com/spring-projects-experimental/spring-authorization-server


huan1993
218 声望34 粉丝

java工程师