Proljetni REST API + OAuth2 + kutni

1. Pregled

U ovom vodiču, osigurat ćemo REST API s OAuth2 i konzumirati ga od jednostavnog Angular klijenta.

Aplikacija koju ćemo izraditi sastojat će se od tri zasebna modula:

  • Autorizacijski poslužitelj
  • Resursni poslužitelj
  • UI autorizacijski kôd: prednja aplikacija koja koristi tijek autorizacijskog koda

Koristit ćemo OAuth stog u Spring Security 5. Ako želite koristiti naslijeđeni naslon Spring Security OAuth, pogledajte ovaj prethodni članak: Spring REST API + OAuth2 + Angular (Korištenje naslijeđenog naslaga Spring OAuth).

Uskočimo odmah.

2. OAuth2 poslužitelj za autorizaciju (AS)

Jednostavno rečeno, Autorizacijski poslužitelj je aplikacija koja izdaje tokene za autorizaciju.

Prije toga, Spring OAuth stog pružao je mogućnost postavljanja Autorizacijskog poslužitelja kao proljetne aplikacije. No projekt je zastario, uglavnom zato što je OAuth otvoreni standard s mnogim dobro uspostavljenim dobavljačima kao što su Okta, Keycloak i ForgeRock, da nabrojimo samo neke.

Od njih ćemo koristiti Keycloak. Riječ je o poslužitelju za upravljanje identitetom i pristupom otvorenog koda kojim upravlja Red Hat, a razvio ga je na Javi JBoss. Podržava ne samo OAuth2 već i druge standardne protokole poput OpenID Connect i SAML.

Za ovaj vodič, postavit ćemo ugrađeni poslužitelj Keycloak u aplikaciju Spring Boot.

3. Resursni poslužitelj (RS)

Sada razgovarajmo o poslužitelju resursa; ovo je u osnovi REST API, koji u konačnici želimo biti u mogućnosti konzumirati.

3.1. Maven konfiguracija

Pom Pom našeg poslužitelja resursa gotovo je isti kao i prethodni pom Server za autorizaciju, bez dijela Keycloak-a i sa dodatni spring-boot-starter-oauth2-resource-server ovisnost:

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

3.2. Konfiguracija sigurnosti

Budući da koristimo Spring Boot, možemo definirati minimalnu potrebnu konfiguraciju pomoću svojstava Boot.

To ćemo učiniti za primjena.iml datoteka:

poslužitelj: 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

Ovdje smo naveli da ćemo za autorizaciju koristiti JWT tokene.

The jwk-set-uri svojstvo pokazuje na URI koji sadrži javni ključ tako da naš poslužitelj resursa može provjeriti cjelovitost tokena.

The izdavatelj-uri svojstvo predstavlja dodatnu sigurnosnu mjeru za provjeru valjanosti izdavatelja tokena (a to je Autorizacijski poslužitelj). Međutim, dodavanje ovog svojstva također nalaže da se Autorizacijski poslužitelj treba izvoditi prije nego što možemo pokrenuti aplikaciju Resursni poslužitelj.

Dalje, postavimo a sigurnosna konfiguracija za API za zaštitu krajnjih točaka:

@Configuration javna klasa SecurityConfig proširuje WebSecurityConfigurerAdapter {@Override zaštićena void konfiguracija (HttpSecurity http) baca izuzetak {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 (); }}

Kao što vidimo, za naše GET metode dopuštamo samo zahtjeve koji jesu čitati opseg. Za POST metodu podnositelj zahtjeva mora imati a pisati autoritet uz čitati. Međutim, za bilo koju drugu krajnju točku zahtjev treba samo provjeriti autentičnost kod bilo kojeg korisnika.

Također, the oauth2ResourceServer () metoda određuje da je ovo poslužitelj resursa s jwt () -formatirani tokeni.

Još jedna stvar koju ovdje treba primijetiti je uporaba metode cors () da biste omogućili zaglavlja za kontrolu pristupa na zahtjevima. To je posebno važno jer imamo posla s Angular klijentom, a naši će zahtjevi dolaziti s drugog izvornog URL-a.

3.4. Model i spremište

Dalje, definirajmo a javax.postojanost.Entitet za naš model, Foo:

@Entity javna klasa Foo {@Id @GeneratedValue (strategija = GenerationType.IDENTITY) private Long id; privatni naziv niza; // konstruktor, getteri i postavljači}

Tada nam treba spremište od Foos. Koristit ćemo Spring's PagingAndSortingRepository:

javno sučelje IFooRepository proširuje PagingAndSortingRepository {} 

3.4. Usluga i provedba

Nakon toga definirat ćemo i implementirati jednostavnu uslugu za naš API:

javno sučelje IFooService {Neobvezno findById (dugi id); Foo save (Foo foo); Iterable findAll (); } @Service javna klasa FooServiceImpl implementira IFooService {private IFooRepository fooRepository; javni FooServiceImpl (IFooRepository fooRepository) {this.fooRepository = fooRepository; } @Override public Neobvezno 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. Uzorak kontrolera

Sada ćemo implementirati jednostavan kontroler koji izlaže naš Foo resurs putem DTO-a:

@RestController @RequestMapping (value = "/ api / foos") javna klasa FooController {private IFooService fooService; javni FooController (IFooService fooService) {this.fooService = fooService; } @CrossOrigin (poreklo = "// localhost: 8089") @GetMapping (value = "/ {id}") javno FooDto findOne (@PathVariable Long id) {Foo entity = fooService.findById (id) .orElseThrow (() -> novi ResponseStatusException (HttpStatus.NOT_FOUND)); vratiti convertToDto (entitet); } @GetMapping javna zbirka findAll () {Iterable foos = this.fooService.findAll (); Popis fooDtos = novi ArrayList (); foos.forEach (p -> fooDtos.add (convertToDto (p))); return fooDtos; } zaštićen FooDto convertToDto (Foo entitet) {FooDto dto = novi FooDto (entity.getId (), entity.getName ()); vratiti dto; }}

Primijetite upotrebu @CrossOrigin iznad; ovo je konfiguracija na razini kontrolera koja nam treba omogućiti CORS iz naše kutne aplikacije koja se izvodi na navedenom URL-u.

Evo našeg FooDto:

javna klasa FooDto {private long id; privatni naziv niza; }

4. Prednji kraj - Postavljanje

Sada ćemo pogledati jednostavnu front-end Angular implementaciju za klijenta, koja će pristupiti našem REST API-ju.

Prvo ćemo koristiti Angular CLI za generiranje i upravljanje našim front-end modulima.

Prvo instaliramo node i npm, jer je Angular CLI npm alat.

Tada moramo koristiti frontend-maven-plugin za izgradnju našeg Angular projekta koristeći Maven:

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

I konačno, generirati novi modul koristeći Angular CLI:

novi oauthApp

U sljedećem ćemo odjeljku razgovarati o logici aplikacije Angular.

5. Protok autorizacijskog koda pomoću kutnog

Ovdje ćemo koristiti tok OAuth2 autorizacijskog koda.

Naš slučaj upotrebe: Klijentska aplikacija zahtijeva kôd od Autorizacijskog poslužitelja i prikazuje se sa stranicom za prijavu. Jednom kada korisnik pruži svoje valjane vjerodajnice i pošalje ih, Autorizacijski poslužitelj daje nam kôd. Tada ga front-end klijent koristi za dobivanje pristupnog tokena.

5.1. Kućna komponenta

Počnimo s našom glavnom komponentom, HomeComponent, gdje započinje sva radnja:

@Component ({selector: 'home-header', dobavljači: [AppService], predložak: `Prijava Dobrodošli !! Odjava

`}) klasa izvoza HomeComponent {public isLoggedIn = false; konstruktor (privatna _service: AppService) {} ngOnInit () {this.isLoggedIn = this._service.checkCredentials (); neka i = window.location.href.indexOf ('kod'); 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; } odjava () {this._service.logout (); }}

Na početku, kada korisnik nije prijavljen, pojavljuje se samo gumb za prijavu. Nakon klika na ovaj gumb, korisnik se preusmjerava na autorizacijski URL AS-a, gdje unosi korisničko ime i lozinku. Nakon uspješne prijave, korisnik se preusmjerava natrag s autorizacijskim kodom, a zatim pomoću ovog koda dohvaćamo pristupni token.

5.2. Usluga aplikacija

Pogledajmo sada AppService - nalazi se na app.service.ts - koji sadrži logiku za poslužiteljske interakcije:

  • retrieveToken (): za dobivanje pristupnog tokena pomoću autorizacijskog koda
  • saveToken (): za spremanje našeg pristupnog tokena u kolačić pomoću biblioteke ng2-cookies
  • getResource (): za dobivanje Foo objekta s poslužitelja koristeći njegov ID
  • checkCredentials (): za provjeru je li korisnik prijavljen ili nije
  • Odjavite se(): za brisanje kolačića s pristupnim tokenom i odjava korisnika
klasa izvoza Foo {konstruktor (javni id: broj, javno ime: niz) {}} @Injectable () klasa izvoza AppService {public clientId = 'newClient'; javni redirectUri = '// localhost: 8089 /'; konstruktor (privatni _http: HttpClient) {} retrieveToken (kod) {let params = novi URLSearchParams (); params.append ('vrsta_dodate', 'autorizacijski kod'); params.append ('client_id', this.clientId); params.append ('client_secret', 'newClientSecret'); params.append ('redirect_uri', this.redirectUri); params.append ('kod', kod); neka zaglavlja = novi 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 ( podaci), pogreška => upozorenje ('Nevaljane vjerodajnice')); } saveToken (token) {var expireDate = novi datum (). getTime () + (1000 * token.expires_in); Cookie.set ("access_token", token.access_token, expireDate); console.log ('Dobiveni token pristupa'); 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 ('pristupni_token')}); vrati ovo._http.get (resourceUrl, {headers: headers}) .catch ((error: any) => Observable.throw (error.json (). error || 'Greška poslužitelja')); } checkCredentials () {return Cookie.check ('access_token'); } odjava () {Cookie.delete ('access_token'); window.location.reload (); }}

U retrieveToken metodu koristimo vjerodajnice klijenta i Basic Auth za slanje a OBJAVI prema / openid-connect / token krajnja točka za dobivanje pristupnog tokena. Parametri se šalju u formatu kodiranom URL-om. Nakon što dobijemo pristupni token, pohranjujemo u kolačić.

Pohrana kolačića ovdje je posebno važna jer kolačić koristimo samo za pohranu, a ne za izravno pokretanje postupka provjere autentičnosti. To pomaže u zaštiti od napada i ranjivosti krivotvorenja zahtjeva za više web lokacija (CSRF).

5.3. Foo komponenta

Napokon, naša FooComponent za prikaz naših podataka o Foou:

@Component ({selector: 'foo-details', dobavljači: [AppService], predložak: `ID {{foo.id}} Ime {{foo.name}} New Foo`}) klasa izvoza FooComponent {public foo = new Foo (1, 'uzorak foo'); private foosUrl = '// localhost: 8081 / resource-server / api / foos /'; konstruktor (private _service: AppService) {} getFoo () {this._service.getResource (this.foosUrl + this.foo.id) .subscribe (data => this.foo = data, error => this.foo.name = 'Pogreška'); }}

5.5. Komponenta aplikacije

Naše jednostavno AppComponent da djeluje kao korijenska komponenta:

@Component ({selector: 'app-root', predložak: `Spring Security Oauth - Autorization Code`}) klasa izvoza AppComponent {} 

I AppModule gdje umotavamo sve svoje komponente, usluge i rute:

@NgModule ({deklaracije: [AppComponent, HomeComponent, FooComponent], uvozi: [BrowserModule, HttpClientModule, RouterModule.forRoot ([{path: '', komponenta: HomeComponent, pathMatch: 'full'}], {onSameUrlNavigation: 'reload:' reload: 'reload:' reload: 'reload:' reload: 'reload' 'reload' 'reload' 'reload' ' })], dobavljači: [], bootstrap: [AppComponent]}) klasa izvoza AppModule {} 

7. Pokrenite prednji kraj

1. Da bismo pokrenuli bilo koji od naših front-end modula, prvo moramo izraditi aplikaciju:

mvn čista instalacija

2. Zatim moramo prijeći na naš direktorij aplikacija Angular:

cd src / main / resources

3. Napokon ćemo pokrenuti našu aplikaciju:

npm start

Poslužitelj će se prema zadanim postavkama pokrenuti na priključku 4200; da biste promijenili port bilo kojeg modula, promijenite:

"start": "posluživanje"

u package.json; na primjer, da bi se pokrenuo na portu 8089, dodajte:

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

8. Zaključak

U ovom smo članku naučili kako autorizirati našu aplikaciju pomoću OAuth2.

Potpuna implementacija ovog vodiča može se naći u projektu GitHub.