Spring REST API + OAuth2 + Angular

1. Ikhtisar

Dalam tutorial ini, kita akan mengamankan REST API dengan OAuth2 dan menggunakannya dari klien Angular sederhana.

Aplikasi yang akan kita bangun akan terdiri dari tiga modul terpisah:

  • Server Otorisasi
  • Server Sumber Daya
  • Kode otorisasi UI: aplikasi front-end menggunakan Alur Kode Otorisasi

Kami akan menggunakan tumpukan OAuth di Spring Security 5. Jika Anda ingin menggunakan tumpukan lama OAuth Keamanan Musim Semi, lihat artikel sebelumnya ini: Spring REST API + OAuth2 + Angular (Menggunakan Spring Security OAuth Legacy Stack).

Mari langsung masuk.

2. Server Otorisasi (AS) OAuth2

Sederhananya, Server Otorisasi adalah aplikasi yang mengeluarkan token untuk otorisasi.

Sebelumnya, tumpukan OAuth Keamanan Musim Semi menawarkan kemungkinan untuk menyiapkan Server Otorisasi sebagai Aplikasi Musim Semi. Namun proyek tersebut sudah tidak digunakan lagi, terutama karena OAuth adalah standar terbuka dengan banyak penyedia mapan seperti Okta, Keycloak, dan ForgeRock, untuk beberapa nama.

Dari jumlah tersebut, kami akan menggunakan Keycloak. Ini adalah server Manajemen Identitas dan Akses sumber terbuka yang dikelola oleh Red Hat, dikembangkan di Java, oleh JBoss. Ini mendukung tidak hanya OAuth2 tetapi juga protokol standar lainnya seperti OpenID Connect dan SAML.

Untuk tutorial ini, kami akan menyiapkan server Keycloak tertanam di aplikasi Spring Boot.

3. Server Sumber Daya (RS)

Sekarang mari kita bahas Resource Server; ini pada dasarnya adalah REST API, yang pada akhirnya ingin kami konsumsi.

3.1. Konfigurasi Maven

Pom kami Sumber Daya Server adalah sama seperti sebelumnya Otorisasi Server pom, sans bagian Keycloak dan dengan tambahan semi-boot-starter-OAuth2-sumber daya-server ketergantungan :

 org.springframework.boot     spring-boot-starter-oauth2-resource-server 

3.2. Konfigurasi Keamanan

Karena kami menggunakan Spring Boot, kami dapat menentukan konfigurasi minimal yang diperlukan menggunakan properti Boot.

Kami akan melakukan ini dalam file application.yml :

server: port: 8081 servlet: context-path: /resource-server spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

Di sini, kami menetapkan bahwa kami akan menggunakan token JWT untuk otorisasi.

Properti jwk-set-uri menunjuk ke URI yang berisi kunci publik sehingga Server Sumber Daya kami dapat memverifikasi integritas token.

Properti penerbit-uri mewakili ukuran keamanan tambahan untuk memvalidasi penerbit token (yang merupakan Server Otorisasi). Namun, menambahkan properti ini juga mengamanatkan bahwa Server Otorisasi harus berjalan sebelum kita dapat memulai aplikasi Server Sumber Daya.

Selanjutnya, mari kita siapkan konfigurasi keamanan untuk API untuk mengamankan titik akhir :

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors() .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**") .hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/api/foos") .hasAuthority("SCOPE_write") .anyRequest() .authenticated() .and() .oauth2ResourceServer() .jwt(); } }

Seperti yang bisa kita lihat, untuk metode GET kami, kami hanya mengizinkan permintaan yang memiliki cakupan baca . Untuk metode POST, pemohon harus memiliki otoritas tulis selain membaca . Namun, untuk titik akhir lainnya, permintaan tersebut harus diautentikasi dengan pengguna mana pun.

Selain itu, metode oauth2ResourceServer () menetapkan bahwa ini adalah server sumber daya, dengan token berformat jwt () .

Hal lain yang perlu diperhatikan di sini adalah penggunaan metode cors () untuk mengizinkan header Access-Control pada permintaan. Ini sangat penting karena kita berurusan dengan klien Angular, dan permintaan kita akan datang dari URL asal lain.

3.4. Model dan Repositori

Selanjutnya, mari kita definisikan javax.persistence.Entity untuk model kita, Foo :

@Entity public class Foo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // constructor, getters and setters }

Maka kita membutuhkan repositori Foo s. Kami akan menggunakan Spring's PagingAndSortingRepository :

public interface IFooRepository extends PagingAndSortingRepository { } 

3.4. Layanan dan Implementasi

Setelah itu, kami akan mendefinisikan dan menerapkan layanan sederhana untuk API kami:

public interface IFooService { Optional findById(Long id); Foo save(Foo foo); Iterable findAll(); } @Service public class FooServiceImpl implements IFooService { private IFooRepository fooRepository; public FooServiceImpl(IFooRepository fooRepository) { this.fooRepository = fooRepository; } @Override public Optional findById(Long id) { return fooRepository.findById(id); } @Override public Foo save(Foo foo) { return fooRepository.save(foo); } @Override public Iterable findAll() { return fooRepository.findAll(); } } 

3.5. Pengontrol Sampel

Sekarang mari kita implementasikan pengontrol sederhana yang mengekspos sumber daya Foo kita melalui DTO:

@RestController @RequestMapping(value = "/api/foos") public class FooController { private IFooService fooService; public FooController(IFooService fooService) { this.fooService = fooService; } @CrossOrigin(origins = "//localhost:8089") @GetMapping(value = "/{id}") public FooDto findOne(@PathVariable Long id) { Foo entity = fooService.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); return convertToDto(entity); } @GetMapping public Collection findAll() { Iterable foos = this.fooService.findAll(); List fooDtos = new ArrayList(); foos.forEach(p -> fooDtos.add(convertToDto(p))); return fooDtos; } protected FooDto convertToDto(Foo entity) { FooDto dto = new FooDto(entity.getId(), entity.getName()); return dto; } }

Perhatikan penggunaan @CrossOrigin di atas; ini adalah konfigurasi level pengontrol yang kami perlukan untuk mengizinkan CORS dari Aplikasi Angular kami berjalan di URL yang ditentukan.

Inilah FooDto kami :

public class FooDto { private long id; private String name; }

4. Bagian Depan - Penyiapan

Kita sekarang akan melihat implementasi Angular front-end sederhana untuk klien, yang akan mengakses REST API kita.

Pertama-tama kami akan menggunakan Angular CLI untuk menghasilkan dan mengelola modul front-end kami.

Pertama, kami menginstal node dan npm , karena Angular CLI adalah alat npm.

Kemudian kita perlu menggunakan plugin frontend-maven untuk membangun proyek Angular kita menggunakan Maven:

   com.github.eirslett frontend-maven-plugin 1.3  v6.10.2 3.10.10 src/main/resources    install node and npm  install-node-and-npm    npm install  npm    npm run build  npm   run build      

Dan terakhir, buat Modul baru menggunakan Angular CLI:

ng new oauthApp

Di bagian berikut, kita akan membahas logika aplikasi Angular.

5. Alur Kode Otorisasi Menggunakan Angular

Kami akan menggunakan alur Kode Otorisasi OAuth2 di sini.

Kasus penggunaan kami: Aplikasi klien meminta kode dari Server Otorisasi dan disajikan dengan halaman login. Setelah pengguna memberikan kredensial dan kiriman yang valid, Server Otorisasi memberi kami kodenya. Kemudian klien front-end menggunakannya untuk mendapatkan token akses.

5.1. Komponen Rumah

Mari kita mulai dengan komponen utama kita, HomeComponent , tempat semua tindakan dimulai:

@Component({ selector: 'home-header', providers: [AppService], template: ` Login Welcome !! Logout

` }) export class HomeComponent { public isLoggedIn = false; constructor(private _service: AppService) { } ngOnInit() { this.isLoggedIn = this._service.checkCredentials(); let i = window.location.href.indexOf('code'); if(!this.isLoggedIn && i != -1) { this._service.retrieveToken(window.location.href.substring(i + 5)); } } login() { window.location.href = '//localhost:8083/auth/realms/baeldung/protocol/openid-connect/auth? response_type=code&scope=openid%20write%20read&client_id=' + this._service.clientId + '&redirect_uri='+ this._service.redirectUri; } logout() { this._service.logout(); } }

Pada awalnya, saat pengguna tidak login, hanya tombol login yang muncul. Setelah mengklik tombol ini, pengguna akan diarahkan ke URL otorisasi AS di mana mereka memasukkan nama pengguna dan kata sandi. Setelah login berhasil, pengguna dialihkan kembali dengan kode otorisasi, dan kemudian kami mengambil token akses menggunakan kode ini.

5.2. Layanan Aplikasi

Sekarang mari kita lihat AppService - terletak di app.service.ts - yang berisi logika untuk interaksi server:

  • retveToken () : untuk mendapatkan token akses menggunakan kode otorisasi
  • saveToken () : untuk menyimpan token akses kami dalam cookie menggunakan pustaka ng2-cookie
  • getResource () : untuk mendapatkan objek Foo dari server menggunakan ID-nya
  • checkCredentials () : untuk memeriksa apakah pengguna masuk atau tidak
  • logout () : untuk menghapus cookie token akses dan mengeluarkan pengguna
export class Foo { constructor(public id: number, public name: string) { } } @Injectable() export class AppService { public clientId = 'newClient'; public redirectUri = '//localhost:8089/'; constructor(private _http: HttpClient) { } retrieveToken(code) { let params = new URLSearchParams(); params.append('grant_type','authorization_code'); params.append('client_id', this.clientId); params.append('client_secret', 'newClientSecret'); params.append('redirect_uri', this.redirectUri); params.append('code',code); let headers = new HttpHeaders({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'}); this._http.post('//localhost:8083/auth/realms/baeldung/protocol/openid-connect/token', params.toString(), { headers: headers }) .subscribe( data => this.saveToken(data), err => alert('Invalid Credentials')); } saveToken(token) { var expireDate = new Date().getTime() + (1000 * token.expires_in); Cookie.set("access_token", token.access_token, expireDate); console.log('Obtained Access token'); window.location.href = '//localhost:8089'; } getResource(resourceUrl) : Observable { var headers = new HttpHeaders({ 'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': 'Bearer '+Cookie.get('access_token')}); return this._http.get(resourceUrl, { headers: headers }) .catch((error:any) => Observable.throw(error.json().error || 'Server error')); } checkCredentials() { return Cookie.check('access_token'); } logout() { Cookie.delete('access_token'); window.location.reload(); } }

Dalam metode retveToken , kami menggunakan kredensial klien dan Basic Auth kami untuk mengirim POST ke titik akhir / openid-connect / token untuk mendapatkan token akses. Parameter dikirim dalam format yang dikodekan URL. Setelah kami mendapatkan token akses, kami menyimpannya dalam cookie.

Penyimpanan cookie sangat penting di sini karena kami hanya menggunakan cookie untuk tujuan penyimpanan dan bukan untuk mendorong proses otentikasi secara langsung. Ini membantu melindungi dari serangan dan kerentanan Cross-Site Request Forgery (CSRF).

5.3. Komponen Foo

Terakhir, FooComponent kami menampilkan detail Foo kami:

@Component({ selector: 'foo-details', providers: [AppService], template: `  ID {{foo.id}} Name {{foo.name}} New Foo ` }) export class FooComponent { public foo = new Foo(1,'sample foo'); private foosUrl = '//localhost:8081/resource-server/api/foos/'; constructor(private _service:AppService) {} getFoo() { this._service.getResource(this.foosUrl+this.foo.id) .subscribe( data => this.foo = data, error => this.foo.name = 'Error'); } }

5.5. Komponen Aplikasi

AppComponent sederhana kami untuk bertindak sebagai komponen root:

@Component({ selector: 'app-root', template: ` Spring Security Oauth - Authorization Code ` }) export class AppComponent { } 

Dan AppModule tempat kami membungkus semua komponen, layanan, dan rute kami:

@NgModule({ declarations: [ AppComponent, HomeComponent, FooComponent ], imports: [ BrowserModule, HttpClientModule, RouterModule.forRoot([ { path: '', component: HomeComponent, pathMatch: 'full' }], {onSameUrlNavigation: 'reload'}) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } 

7. Jalankan Front End

1. Untuk menjalankan modul front-end kita, kita perlu membuat aplikasi terlebih dahulu:

mvn clean install

2. Kemudian kita perlu menavigasi ke direktori aplikasi Angular kita:

cd src/main/resources

3. Terakhir, kami akan memulai aplikasi kami:

npm start

Server akan mulai secara default pada port 4200; untuk mengubah port modul apa pun, ubah:

"start": "ng serve"

di package.json; misalnya, untuk menjalankannya di port 8089, tambahkan:

"start": "ng serve --port 8089"

8. Kesimpulan

Di artikel ini, kami mempelajari cara memberi otorisasi aplikasi kami menggunakan OAuth2.

Implementasi lengkap dari tutorial ini dapat ditemukan di proyek GitHub.