OAuth2 Authorization Server


基于Spring Security 5 的 Authorization Server的写法

先看演示

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
 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">
    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.6.5
         
    
    com.cjs.example.oauth2
    cjs-authorization-server
    0.0.1-SNAPSHOT
    cjs-authorization-server
    
        1.8
        cjssso
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.springframework.security
            spring-security-oauth2-authorization-server
            0.2.3
        
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        
        
            org.thymeleaf.extras
            thymeleaf-extras-springsecurity5
        
        
        
            org.webjars
            webjars-locator-core
            0.50
        
        
            org.webjars
            bootstrap
            5.1.3
        
        
            org.webjars
            jquery
            3.6.0
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.security
            spring-security-test
            test
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
            
            
                com.spotify
                docker-maven-plugin
                1.2.2
                
                    ${docker.image.prefix}/${project.artifactId}
                    src/main/docker
                    
                        
                            /
                            ${project.build.directory}
                            ${project.build.finalName}.jar
                        
                    
                
            
        
    

application.yml


server:
  port: 9000
#  servlet:
#    context-path: /uaa
logging:
  level:
    root: debug

Security配置


package com.cjs.example.oauth2.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
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.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
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.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

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

/**
 * @Author ChengJianSheng
 * @Date 2022/3/29
 */
@Configuration
public class OAuth2AuthorizationServerSecurityConfiguration {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.formLogin(Customizer.withDefaults()).build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize
                .antMatchers("/webjars/**", "/login", "/login-error").permitAll()
                .anyRequest().authenticated())
                .formLogin().loginPage("/login").failureUrl("/login-error");

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client1 = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client-1")
                .clientSecret("$2a$10$zNvOs9Ex0cD794OyWwDIXutF7F4hHYjLuEwI.U30p3nNIyumfON42")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8081/message/login/oauth2/code/mycustom")
                .redirectUri("http://127.0.0.1:8081/message/authorized")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
                .build();
        RegisteredClient client2 = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client-2")
                .clientSecret("$2a$10$csxC.p5gNQbnpv8Mt5dqPevS66QL2USHERtQ8hEA1TWETIk1Zo3SS")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("http://www.baidu.com")
                .scope("message:read")
                .build();

        return new InMemoryRegisteredClientRepository(client1, client2);
    }

    @Bean
    public JWKSource jwkSource(KeyPair keyPair) {
        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 new ImmutableJWKSet<>(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(KeyPair keyPair) {
        return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
    }

    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder().issuer("http://localhost:9000").build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.withUsername("zhangsan")
                .password("$2a$10$5nlwTDZ9LNbz7UMKcseY3u6YqbkGrMe2tQirfrwrCJAKrDAM78bUa")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(userDetails);
    }

    public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().encode("123456"));
    }

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

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }
}

自定义登录页面


 xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">

    </span>Spring Security OAuth 2.0 Sample<span style="color: rgba(249, 38, 114, 1)">
     charset="utf-8" />
     http-equiv="X-UA-Compatible" content="IE=edge" />
     name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />


 class="container">
    

Login

th:if="${loginError}" style="font-weight:bold;color:red;">Wrong username or password

th:action="@{/login}" method="post"> class="form-row"> class="form-group"> for="username">Username type="text" id="username" name="username" placeholder="请输入用户名" autofocus="autofocus" class="form-control"> class="form-text text-muted">user1 / password
class="form-row"> class="form-group"> for="password">Password type="password" id="password" name="password" placeholder="请输入密码" class="form-control">
class="form-row"> class="form-group"> for="verificationCode">验证码 type="text" id="verificationCode" name="verificationCode" placeholder="请输入验证码" class="form-control">
type="submit" class="btn btn-primary">登录

默认的Controller


package com.cjs.example.oauth2.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @Author ChengJianSheng
 * @Date 2022/4/2
 */
@Controller
public class DefaultController {

	@GetMapping("/")
	public String root() {
		return "redirect:/index";
	}

	@GetMapping("/index")
	public String index() {
		return "index";
	}

	@GetMapping("/login")
	public String login() {
		return "login";
	}

	@GetMapping("/login-error")
	public String loginError(Model model) {
		model.addAttribute("loginError", true);
		return login();
	}
}

获取token


GET http://localhost:9000/oauth2/authorize?response_type=code&client_id=client-2&redirect_uri=http://www.baidu.com

POST /oauth2/token HTTP/1.1
Host: localhost:9000
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=xxx&redirect_uri=http://www.baidu.com

抓包

项目结构

参考

https://github.com/jgrandja/spring-security-oauth-5-2-migrate/tree/main/client-app