Spring REST API + OAuth2 + Angular

1. Общ преглед

В този урок ще осигурим REST API с OAuth2 и ще го използваме от прост Angular клиент.

Приложението, което ще разработим, ще се състои от три отделни модула:

  • Сървър за оторизация
  • Ресурсен сървър
  • Код за оторизация на потребителския интерфейс: приложение от предния край, използващо потока на кода за разрешаване

Ще използваме стека OAuth в Spring Security 5. Ако искате да използвате наследствения стек Spring Security OAuth, разгледайте тази предишна статия: Spring REST API + OAuth2 + Angular (Използване на Spring OAuth Legacy Stack).

Да скочим направо.

2. Сървърът за упълномощаване OAuth2 (AS)

Най-просто казано, Authorization Server е приложение, което издава токени за оторизация.

Преди това стекът Spring OAuth предлагаше възможност за настройка на сървър за оторизация като пролетно приложение. Но проектът е остарял, главно защото OAuth е отворен стандарт с много утвърдени доставчици като Okta, Keycloak и ForgeRock, за да назовем само няколко.

От тях ще използваме Keycloak. Това е сървър за управление на идентичност и достъп с отворен код, администриран от Red Hat, разработен в Java, от JBoss. Той поддържа не само OAuth2, но и други стандартни протоколи като OpenID Connect и SAML.

За този урок ще настроим вграден сървър Keycloak в приложение Spring Spring.

3. Ресурсният сървър (RS)

Сега нека обсъдим Resource Server; това е по същество REST API, който в крайна сметка искаме да можем да консумираме.

3.1. Maven конфигурация

Помът на нашия Resource Server е почти същият като предишния pom Server на Authorization Server, без частта Keycloak и с допълнителна зависимост spring-boot-starter-oauth2-resource-server :

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

3.2. Конфигурация на защитата

Тъй като използваме Spring Boot, можем да определим минималната необходима конфигурация, като използваме свойствата на Boot.

Ще направим това във файл 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

Тук посочихме, че ще използваме JWT маркери за оторизация.

Свойството jwk-set-uri сочи към URI, съдържащ публичния ключ, така че нашият Resource Server да може да провери целостта на маркерите .

Най- емитент-URI Имотът представлява допълнителна мярка за сигурност за валидиране на издателя на жетоните (което е оторизация на сървъра). Въпреки това, добавянето на това свойство също налага, че сървърът за оторизация трябва да работи, преди да можем да стартираме приложението Resource Server.

След това нека настроим конфигурация на защитата за API за защита на крайните точки :

@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(); } }

Както виждаме, за нашите GET методи разрешаваме само заявки с обхват на четене . За метода POST заявителят трябва да има право на запис в допълнение към четенето . За всяка друга крайна точка обаче заявката трябва просто да бъде удостоверена с всеки потребител.

Също така, на oauth2ResourceServer () метод определя, че това е сървър на ресурсите, с JWT () - форматирани жетони.

Друг момент, който трябва да отбележим тук, е използването на метод cors (), който позволява заглавките за контрол на достъпа в заявките. Това е особено важно, тъй като имаме работа с клиент на Angular и исканията ни ще идват от друг URL на източника.

3.4. Моделът и хранилището

След това нека дефинираме javax.persistence.Entity за нашия модел, Foo :

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

Тогава се нуждаем от хранилище на Foo s. Ще използваме Spring's PagingAndSortingRepository :

public interface IFooRepository extends PagingAndSortingRepository { } 

3.4. Услугата и изпълнението

След това ще дефинираме и внедрим проста услуга за нашия API:

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. Примерен контролер

Сега нека внедрим прост контролер, излагащ нашия Foo ресурс чрез 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; } }

Забележете използването на @CrossOrigin по-горе; това е конфигурацията на ниво контролер, която трябва да разрешим CORS от нашето Angular App, работещо на посочения URL адрес.

Ето нашето FooDto :

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

4. Преден край - Настройка

Сега ще разгледаме проста внедрена Angular реализация за клиента, която ще има достъп до нашия REST API.

Първо ще използваме Angular CLI за генериране и управление на нашите предни модули.

Първо, ние инсталираме node и npm , тъй като Angular CLI е npm инструмент.

След това трябва да използваме приставката frontend-maven, за да изградим нашия Angular проект с помощта на 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      

И накрая, генерирайте нов модул, използвайки Angular CLI:

ng new oauthApp

В следващия раздел ще обсъдим логиката на приложението Angular.

5. Поток на разрешителния код с използване на Angular

Тук ще използваме потока на OAuth2 Authorization Code.

Нашият случай на употреба: Клиентското приложение иска код от сървъра за оторизация и се представя със страница за вход. След като потребителят предостави валидните си идентификационни данни и ги изпрати, Authorization Server ни дава кода. Тогава клиентският клиент го използва, за да придобие токен за достъп.

5.1. Начало Компонент

Нека започнем с нашия основен компонент, HomeComponent , където започва цялото действие:

@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(); } }

In the beginning, when the user is not logged in, only the login button appears. Upon clicking this button, the user is navigated to the AS's authorization URL where they key in username and password. After a successful login, the user is redirected back with the authorization code, and then we retrieve the access token using this code.

5.2. App Service

Now let's look at AppService — located at app.service.ts — which contains the logic for server interactions:

  • retrieveToken(): to obtain access token using authorization code
  • saveToken(): to save our access token in a cookie using ng2-cookies library
  • getResource(): to get a Foo object from server using its ID
  • checkCredentials(): to check if user is logged in or not
  • logout(): to delete access token cookie and log the user out
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(); } }

In the retrieveToken method, we use our client credentials and Basic Auth to send a POST to the /openid-connect/token endpoint to get the access token. The parameters are being sent in a URL-encoded format. After we obtain the access token, we store it in a cookie.

The cookie storage is especially important here because we're only using the cookie for storage purposes and not to drive the authentication process directly. This helps protect against Cross-Site Request Forgery (CSRF) attacks and vulnerabilities.

5.3. Foo Component

И накрая, нашият FooComponent за показване на нашите подробности за Foo:

@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. App Component

Нашият прост AppComponent да действа като основен компонент:

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

И AppModule, където обгръщаме всички наши компоненти, услуги и маршрути:

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

7. Пуснете предния край

1. За да стартираме някой от нашите предни модули, първо трябва да изградим приложението:

mvn clean install

2. След това трябва да отидем до нашата директория с приложения Angular:

cd src/main/resources

3. Накрая ще стартираме нашето приложение:

npm start

Сървърът ще се стартира по подразбиране на порт 4200; за да смените порта на всеки модул, променете:

"start": "ng serve"

в package.json; например, за да се изпълни на порт 8089, добавете:

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

8. Заключение

В тази статия научихме как да упълномощаваме нашето приложение с помощта на OAuth2.

Пълното изпълнение на този урок може да бъде намерено в проекта GitHub.