Keamanan Musim Semi - Contoh Aplikasi Web Otorisasi O2uth Melalui BitBucket

Pada artikel ini, kami akan mempertimbangkan cara untuk mengotorisasi pengguna dalam aplikasi web pada Spring Boot menggunakan protokol OAuth2 menggunakan server otorisasi eksternal (menggunakan contoh Bitbucket).

Apa yang ingin kita dapatkan


Misalkan kita sedang mengembangkan aplikasi web aman yang memiliki akses ke sumber daya pengguna pada server eksternal, misalnya, sistem integrasi berkelanjutan, dan Bitbucket bertindak sebagai server eksternal, dan kami tidak ingin menyimpan nama pengguna dan kata sandi pengguna dari sistem eksternal. Artinya, kita perlu memberi otorisasi kepada pengguna melalui Bitbucket untuk mendapatkan akses ke akun dan sumber dayanya, juga memeriksa bahwa dia adalah pengguna aplikasi kita dan membuatnya agar pengguna tidak mengungkapkan kepada kami kredensial dari Bitbucket kepada kami. Saya ingin tahu bagaimana melakukan ini - selamat datang di kucing.

OAuth2


OAuth2 adalah protokol otorisasi yang memungkinkan Anda untuk memberikan kepada pihak ketiga akses terbatas ke sumber daya yang dilindungi pengguna tanpa harus memberinya (nama pihak ketiga) nama pengguna dan kata sandi.

OAuth2 mendefinisikan 4 peran:

  • Pemilik sumber daya
  • Server sumber daya
  • Server otorisasi
  • Klien (aplikasi)

Pemilik sumber daya adalah pengguna yang menggunakan aplikasi klien dan memungkinkannya mengakses akunnya yang dihosting di server sumber daya. Akses aplikasi ke akun dibatasi oleh izin yang diberikan.

The sumber daya server host mengamankan akun pengguna.
Server otorisasi mengotentikasi pemilik sumber daya dan mengeluarkan token akses. Server otorisasi dapat sekaligus menjadi server sumber daya.

Aplikasi klien adalah aplikasi yang ingin mengakses akun dan sumber daya pengguna.

Dalam kasus kami, aplikasi klien adalah aplikasi yang kami kembangkan, dan Bitbucket akan menjadi server otorisasi dan server sumber daya.

OAuth2 mendukung empat jenis otorisasi: Kode Otorisasi, Implisit, Kredensial Kata Sandi Pemilik Sumber Daya, dan Kredensial Klien. Kami tidak akan mempertimbangkan semuanya, kami tertarik pada jenis Kode Otorisasi. Jenis Kode Otorisasi dioptimalkan untuk aplikasi server di mana kode sumber tidak tersedia untuk umum dan kode Rahasia Klien dapat dirahasiakan. Jenis ini berfungsi atas dasar pengalihan, yaitu, pengguna akan diarahkan ke server otorisasi untuk mengonfirmasi identitasnya dan memungkinkan aplikasi untuk menggunakan akunnya.

Proses otorisasi melalui Kode Otorisasi terdiri dari urutan dua permintaan:

  • Permintaan Otorisasi
  • Akses Permintaan Token

Permintaan otorisasi digunakan untuk mengonfirmasi identitas pengguna, serta meminta otorisasi aplikasi kami dari pengguna. Permintaan ini adalah permintaan GET dengan parameter berikut:

  • response_type - nilainya harus sama dengan kode
  • client_id - nilai yang diperoleh saat mendaftarkan klien dengan penyedia OAuth2
  • redirect_uri - URL tempat pengguna akan dialihkan setelah otorisasi
  • scope - parameter opsional yang menunjukkan level akses yang diminta
  • state - string yang dihasilkan secara acak untuk memverifikasi respons

Contoh permintaan:

GET https://server.example.com/authorize?response_type=code&client_id=CLIENT_ID&state=xyz&redirect_uri=REDIRECT_URI

Jika pengguna mengonfirmasi identitasnya dan memungkinkan aplikasi mengakses sumber dayanya, agen pengguna akan dialihkan ke URL panggilan balik yang ditentukan saat pendaftaran klien dengan kode parameter tambahan yang berisi kode otorisasi dan parameter status dengan nilai yang diteruskan dalam permintaan.

Contoh panggilan balik:

GET https://client.example.com/cb?code=AUTH_CODE_HERE&state=xyz

Permintaan kode akses digunakan untuk menukar kode otorisasi yang diterima dengan kode akses ke sumber daya pengguna. Permintaan ini adalah permintaan POST dengan parameter berikut:

  • grant_type - nilai harus berupa authorization_code
  • kode - kode otorisasi yang diperoleh pada langkah sebelumnya
  • redirect_uri - harus cocok dengan URL yang ditentukan pada langkah sebelumnya
  • client_id - nilai yang diperoleh saat mendaftarkan klien dengan penyedia OAuth2
  • client_secret - nilai yang diperoleh saat mendaftarkan klien dengan penyedia OAuth2

Contoh permintaan:

POST https://server.example.com/token
    grant_type=authorization_code&
    code=AUTH_CODE&
    redirect_uri=REDIRECT_URI&
    client_id=CLIENT_ID&
    client_secret=CLIENT_SECRET

Respons server berisi kode akses dan masa pakainya:

{
    "access_token": "2YotnFZFEjr1zCsicMWpAA",
    "expires_in": 3600,
    "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}

Seluruh proses ini sudah terotomatisasi di Spring Security dan kami tidak perlu khawatir dengan implementasinya.

Registrasi pelanggan


Pertama-tama, kami akan mendaftarkan aplikasi kami sebagai klien di Bitbucket untuk mendapatkan kunci (Kunci) dan kode akses (Rahasia Klien).



Masukkan nama klien dan URL panggilan balik. Kemudian kami mencatat apa yang akan tersedia untuk klien ini.



Nilai Key dan ClientSecret yang diperoleh disimpan di application.properties:

client_id=ZJsdANfWkJ7jcktw2x
client_secret=28uUrJ9m43svbkcnXVNj8qeBjFtd8jaD

Konfigurasikan Keamanan Musim Semi


Selanjutnya, mari kita mengatur Spring Security. Agar OAuth2 berfungsi, Anda harus membuat objek ClientRegistration. ClientRegistration menyimpan informasi tentang klien yang terdaftar pada penyedia OAuth2. Di sini kita membutuhkan client_id dan client_secret yang diperoleh pada langkah sebelumnya. Karena ada beberapa objek ClientRegistration tersebut secara umum, Spring Security menggunakan objek ClientRegistrationRepository untuk menyimpan dan mengaksesnya. Mari kita ciptakan juga. Kami juga menunjukkan bahwa hanya pengguna yang sah yang dapat memanggil permintaan apa pun dan mendefinisikan kembali UserService dengan implementasinya.

SecurityConfig.java
@Configuration
public class SecurityConfig {

    @Value("${client_id}")
    private String clientId;

    @Value("${client_secret}")
    private String clientSecret;

    @Bean
    public ClientRegistration clientRegistration() {
        return ClientRegistration
                .withRegistrationId("bitbucket")
                .clientId(clientId)
                .clientSecret(clientSecret)
                .userNameAttributeName("username")
                .clientAuthenticationMethod(BASIC)
                .authorizationGrantType(AUTHORIZATION_CODE)
                .userInfoUri("https://api.bitbucket.org/2.0/user")
                .tokenUri("https://bitbucket.org/site/oauth2/access_token")
                .authorizationUri("https://bitbucket.org/site/oauth2/authorize")
                .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
                .build();
    }

    @Bean
    @Autowired
    public MyOAuth2UserService oAuth2userService(UserService userService) {
        return new MyOAuth2UserService(userService);
    }

    @Bean
    @Autowired
    public ClientRegistrationRepository clientRegistrationRepository(List<ClientRegistration> registrations) {
        return new InMemoryClientRegistrationRepository(registrations);
    }

    @Configuration
    @EnableWebSecurity
    public static class AuthConfig extends WebSecurityConfigurerAdapter {

        private final MyOAuth2UserService userService;

        @Autowired
        public AuthConfig(MyOAuth2UserService userService) {
            this.userService = userService;
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests(authorizeRequests -> authorizeRequests
                            .anyRequest().authenticated()
                    )
                    .oauth2Login(oauth2Login -> oauth2Login
                            .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(userService))
                    );
        }
    }
}


Kustomisasi Layanan Pengguna


Spring Security tidak hanya sepenuhnya mengimplementasikan proses otorisasi, tetapi juga menyediakan kemampuan untuk menyesuaikannya. Misalnya, kemungkinan mengkustomisasi permintaan otorisasi , meminta kode akses , serta kemungkinan posting kustom memproses respons terhadap permintaan kode akses . Setelah otorisasi berhasil, Spring Security menggunakan UserInfo Endpoint untuk mengambil atribut pengguna dari server otorisasi. Secara khusus, implementasi antarmuka OAuth2UserService digunakan untuk ini.

Kami akan membuat implementasi layanan kami sendiri, sehingga setelah otorisasi pengguna di server otorisasi, kami juga memeriksa apakah dia adalah pengguna aplikasi kami, atau mendaftar jika pendaftaran terbuka untuk semua orang. Secara default, Spring Security menggunakan implementasi DefaultOAuth2UserService. Dia akan membentuk dasar dari implementasi kami.

MyOAuth2UserService.java
public class MyOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";

    private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";

    private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";

    private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<Map<String, Object>>() {
    };

    private final UserService userService;

    private final RestOperations restOperations;

    private final Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();

    public MyOAuth2UserService(UserService userService) {
        this.userService = requireNonNull(userService);
        this.restOperations = createRestTemplate();
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        checkNotNull(userRequest, "userRequest cannot be null");
        if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
            OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE, "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
        if (!StringUtils.hasText(userNameAttributeName)) {
            OAuth2Error oauth2Error = new OAuth2Error(
                    MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
                    "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " + registrationId, null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }

        ResponseEntity<Map<String, Object>> response;
        try {
            // OAuth2UserRequestEntityConverter cannot return null values.
            //noinspection ConstantConditions
            response = this.restOperations.exchange(requestEntityConverter.convert(userRequest), PARAMETERIZED_RESPONSE_TYPE);
        } catch (OAuth2AuthorizationException ex) {
            OAuth2Error oauth2Error = ex.getError();
            StringBuilder errorDetails = new StringBuilder();
            errorDetails.append("Error details: [");
            errorDetails.append("UserInfo Uri: ").append(
                    userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
            errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode());
            if (oauth2Error.getDescription() != null) {
                errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
            }
            errorDetails.append("]");
            oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
        } catch (RestClientException ex) {
            OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
        }

        Map<String, Object> userAttributes = emptyIfNull(response.getBody());

        Set<GrantedAuthority> authorities = new LinkedHashSet<>();
        authorities.add(new OAuth2UserAuthority(userAttributes));

        for (String authority : userRequest.getAccessToken().getScopes()) {
            authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
        }

        //     ,   
        //          ,
        //    
        User user = findOrCreate(userAttributes);
        userAttributes.put(MyOAuth2User.ID_ATTR, user.getId());
        return new MyOAuth2User(userNameAttributeName, userAttributes, authorities);
    }

    private User findOrCreate(Map<String, Object> userAttributes) {
        String login = (String) userAttributes.get("username");
        String username = (String) userAttributes.get("display_name");

        Optional<User> userOpt = userService.findByLogin(login);
        if (!userOpt.isPresent()) {
            User user = new User();
            user.setLogin(login);
            user.setName(username);
            return userService.create(user);
        }
        return userOpt.get();
    }

    private RestTemplate createRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        return restTemplate;
    }
}


Tes titik akhir


Saatnya membuat titik akhir untuk menguji kesehatan dari apa yang baru saja kita lakukan. Titik akhir kami akan terdiri dari hanya satu permintaan, yang akan menyambut pengguna saat ini.

WelcomeEndpoint.java
@Path("/")
public class WelcomeEndpoint {

    @Autowired
    private UserService userService;

    @GET
    public String welcome() {
        User currentUser = getCurrentUser();
        return String.format("Welcome, %s! (user id: %s, user login: %s)",
                currentUser.getName(), currentUser.getId(), currentUser.getLogin());
    }

    public User getCurrentUser() {
        return userService.findByLogin(getAuthenticatedUser().getName()).orElseThrow(() -> new RuntimeException("No user logged in."));
    }

    private Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

    private MyOAuth2User getAuthenticatedUser() {
        return (MyOAuth2User) getAuthentication().getPrincipal();
    }
}


Cek kesehatan


Kami meluncurkan aplikasi dan pergi ke alamat http: // localhost: 8080 dan melihat bahwa kami dialihkan ke situs web Bitbucket untuk mengonfirmasi akun kami. Masukkan nama pengguna dan kata sandi.



Sekarang kita perlu mengizinkan aplikasi kita mengakses akun dan sumber daya kita.



Salam pengguna mengandung atribut dari server otorisasi dan ID pengguna dalam aplikasi kami.



Sumber


Kode sumber lengkap untuk aplikasi ini ada di Github .

Referensi



All Articles