Spring REST API + OAuth2 + Angular

1. Gambaran keseluruhan

Dalam tutorial ini, kami akan mendapatkan REST API dengan OAuth2 dan menggunakannya dari klien Angular yang mudah.

Aplikasi yang akan kami bina akan terdiri daripada tiga modul berasingan:

  • Pelayan Kebenaran
  • Pelayan Sumber
  • Kod kebenaran UI: aplikasi front-end menggunakan Aliran Kod Kebenaran

Kami akan menggunakan timbunan OAuth di Spring Security 5. Jika anda ingin menggunakan tumpukan legasi Spring Security OAuth, lihat artikel sebelumnya: Spring REST API + OAuth2 + Angular (Menggunakan Spring Security OAuth Legacy Stack).

Mari melompat masuk.

2. Pelayan Pengesahan OAuth2 (AS)

Ringkasnya, Server Pengesahan adalah aplikasi yang mengeluarkan token untuk mendapatkan kebenaran.

Sebelumnya, tumpukan Spring Security OAuth menawarkan kemungkinan menyiapkan Server Pengesahan sebagai Aplikasi Musim Semi. Tetapi projek itu sudah tidak digunakan lagi, terutamanya kerana OAuth adalah standard terbuka dengan banyak penyedia mapan seperti Okta, Keycloak, dan ForgeRock, untuk beberapa nama.

Daripada jumlah tersebut, kami akan menggunakan Keycloak. Ini adalah pelayan Identiti dan Pengurusan Sumber terbuka yang ditadbir oleh Red Hat, yang dikembangkan di Java, oleh JBoss. Ia menyokong bukan sahaja OAuth2 tetapi juga protokol standard lain seperti OpenID Connect dan SAML.

Untuk tutorial ini, kami akan menyediakan pelayan Keycloak tertanam dalam aplikasi Spring Boot.

3. Pelayan Sumber (RS)

Sekarang mari kita bincangkan Pelayan Sumber; ini pada dasarnya adalah API REST, yang akhirnya kita mahu dapat dimakan.

3.1. Konfigurasi Maven

Pom kami Resource Server adalah lebih kurang sama seperti pom Kebenaran Server sebelumnya, sans sebahagian Keycloak dan dengan tambahan bunga-boot-starter-OAuth2-sumber pelayan pergantungan :

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

3.2. Konfigurasi Keselamatan

Oleh kerana kita menggunakan Spring Boot, kita dapat menentukan konfigurasi minimum yang diperlukan menggunakan sifat Boot.

Kami akan melakukan ini dalam fail 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 menyatakan bahawa kami akan menggunakan token JWT untuk kebenaran.

The jwk-set-uri mata harta kepada URI yang mengandungi kunci awam supaya Server Sumber kami boleh mengesahkan integriti token '.

Harta penerbit-uri mewakili langkah keselamatan tambahan untuk mengesahkan penerbit token (yang merupakan Pelayan Pengesahan). Namun, menambahkan harta ini juga mewajibkan bahawa Server Pengesahan harus dijalankan sebelum kita dapat memulakan aplikasi Pelayan Sumber.

Seterusnya, mari sediakan konfigurasi keselamatan 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 dapat kita lihat, untuk kaedah GET, kita hanya membenarkan permintaan yang mempunyai ruang lingkup membaca . Untuk kaedah POST, pemohon perlu mempunyai kuasa menulis selain membaca . Namun, untuk titik akhir yang lain, permintaan tersebut harus disahkan dengan pengguna mana pun.

Juga, kaedah oauth2ResourceServer () menetapkan bahawa ini adalah pelayan sumber, dengan token berformat jwt () .

Satu lagi perkara yang perlu diperhatikan di sini adalah penggunaan kaedah kaedah () untuk membolehkan tajuk Access-Control pada permintaan. Ini sangat penting kerana kita berurusan dengan pelanggan Angular, dan permintaan kita akan datang dari URL asal yang lain.

3.4. Model dan Repositori

Seterusnya, mari kita tentukan javax.persistence.Entity untuk model kami, Foo :

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

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

public interface IFooRepository extends PagingAndSortingRepository { } 

3.4. Perkhidmatan dan Pelaksanaan

Selepas itu, kami akan menentukan dan melaksanakan perkhidmatan mudah 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. Pengawal Sampel

Sekarang mari kita laksanakan pengawal mudah yang memperlihatkan sumber 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 peringkat pengawal yang kita perlukan untuk membenarkan CORS dari Aplikasi Sudut kita berjalan pada URL yang ditentukan.

Inilah FooDto kami :

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

4. Bahagian Depan - Persediaan

Kami sekarang akan melihat pelaksanaan sudut depan yang mudah untuk pelanggan, yang akan mengakses REST API kami.

Kami akan menggunakan Angular CLI terlebih dahulu untuk menghasilkan dan mengurus modul front-end kami.

Pertama, kami memasang node dan npm , kerana Angular CLI adalah alat npm.

Maka kita perlu menggunakan plugin frontend-maven untuk membina projek 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 akhirnya, buat Modul baru menggunakan Angular CLI:

ng new oauthApp

Di bahagian berikut, kita akan membincangkan logik aplikasi Sudut.

5. Aliran Kod Kebenaran Menggunakan Sudut

Kami akan menggunakan aliran Kod Pengesahan OAuth2 di sini.

Kes penggunaan kami: Aplikasi klien meminta kod dari Pelayan Pengesahan dan disertakan dengan halaman masuk. Setelah pengguna memberikan bukti kelayakan mereka dan mengemukakan, Pelayan Pengesahan memberi kami kod. Kemudian pelanggan front-end menggunakannya untuk memperoleh token akses.

5.1. Komponen Rumah

Mari bermula dengan komponen utama kami, HomeComponent , di mana semua tindakan bermula:

@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 mulanya, apabila pengguna tidak log masuk, hanya butang log masuk yang muncul. Setelah mengklik butang ini, pengguna dilayari ke URL kebenaran AS di mana mereka memasukkan nama pengguna dan kata laluan. Setelah berjaya masuk, pengguna diarahkan kembali dengan kod kebenaran, dan kemudian kami mengambil token akses menggunakan kod ini.

5.2. Perkhidmatan Aplikasi

Sekarang mari kita lihat AppService - terletak di app.service.ts - yang mengandungi logik untuk interaksi pelayan:

  • retrieveToken () : untuk mendapatkan token akses menggunakan kod kebenaran
  • saveToken () : untuk menyimpan token akses kami dalam kuki menggunakan pustaka ng2-cookie
  • getResource () : untuk mendapatkan objek Foo dari pelayan menggunakan IDnya
  • checkCredentials () : untuk memeriksa sama ada pengguna log masuk atau tidak
  • logout () : untuk menghapus kuki token akses dan log keluar 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 kaedah retrieveToken , kami menggunakan bukti kelayakan pelanggan dan Basic Auth untuk menghantar POST ke titik akhir / openid-connect / token untuk mendapatkan token akses. Parameter dikirim dalam format yang dikodkan URL. Setelah memperoleh token akses, kami menyimpannya dalam kuki.

Penyimpanan kuki sangat penting di sini kerana kami hanya menggunakan kuki untuk tujuan penyimpanan dan tidak mendorong proses pengesahan secara langsung. Ini membantu melindungi daripada serangan dan kerentanan Permintaan Lintas Tapak (CSRF).

5.3. Komponen Foo

Akhirnya, FooComponent kami untuk memaparkan perincian 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 ringkas kami untuk bertindak sebagai komponen akar:

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

Dan AppModule di mana kami membungkus semua komponen, perkhidmatan dan laluan 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 Bahagian Depan

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

mvn clean install

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

cd src/main/resources

3. Akhirnya, kami akan memulakan aplikasi kami:

npm start

Pelayan akan bermula secara lalai pada port 4200; untuk menukar port modul mana pun, ubah:

"start": "ng serve"

dalam pakej.json; sebagai contoh, untuk membuatnya berjalan di port 8089, tambahkan:

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

8. Kesimpulannya

Dalam artikel ini, kami belajar bagaimana mengizinkan aplikasi kami menggunakan OAuth2.

Pelaksanaan penuh tutorial ini boleh didapati dalam projek GitHub.