Spring REST API + OAuth2 + Angular

1. 개요

이 자습서에서는 OAuth2로 REST API를 보호하고 간단한 Angular 클라이언트에서 사용합니다.

우리가 구축 할 애플리케이션은 세 개의 개별 모듈로 구성됩니다.

  • 인증 서버
  • 리소스 서버
  • UI 인증 코드 : 인증 코드 흐름을 사용하는 프런트 엔드 애플리케이션

Spring Security 5에서 OAuth 스택 을 사용할 것입니다. Spring Security OAuth 레거시 스택을 사용하려면 이전 기사 인 Spring REST API + OAuth2 + Angular (Spring Security OAuth 레거시 스택 사용)를 참조하십시오.

바로 뛰어 들자.

2. OAuth2 권한 부여 서버 (AS)

간단히 말해서 Authorization Server는 인증을 위해 토큰을 발행하는 애플리케이션입니다.

이전에 Spring Security OAuth 스택은 Authorization Server를 Spring 애플리케이션으로 설정할 수있는 가능성을 제공했습니다. 그러나 OAuth는 Okta, Keycloak 및 ForgeRock과 같은 많은 잘 확립 된 제공 업체와 함께 개방형 표준이기 때문에 프로젝트는 더 이상 사용되지 않습니다.

이 중 우리는 Keycloak을 사용할 것입니다. JBoss가 Java로 개발 한 Red Hat에서 관리하는 오픈 소스 ID 및 액세스 관리 서버입니다. OAuth2뿐만 아니라 OpenID Connect 및 SAML과 같은 다른 표준 프로토콜도 지원합니다.

이 튜토리얼에서는 Spring Boot 앱에 내장 된 Keycloak 서버를 설정합니다.

3. 리소스 서버 (RS)

이제 리소스 서버에 대해 살펴 보겠습니다. 이것은 본질적으로 우리가 궁극적으로 사용할 수있는 REST API입니다.

3.1. Maven 구성

우리의 리소스 서버의 pom은 이전 Authorization Server pom과 거의 동일하며 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 세트-URI 우리의 자원 서버 토큰 '무결성을 확인할 수 있도록 공개 키가 포함 된 URI에 속성 점.

발급자 URI 속성 (권한 부여 서버 임) 토큰의 발행을 확인하기위한 부가적인 보안 대책을 나타낸다. 그러나이 속성을 추가하면 Resource Server 응용 프로그램을 시작하기 전에 Authorization 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 메소드의 경우 요청자는 read 외에 쓰기 권한 이 있어야합니다 . 그러나 다른 엔드 포인트의 경우 요청은 모든 사용자로 인증되어야합니다.

또한 oauth2ResourceServer () 이 함께, 자원 서버 방식 지정 있음 - JWT () 포맷 토큰.

여기서 주목해야 할 또 다른 점은 cors () 메서드를 사용 하여 요청에 대한 Access-Control 헤더를 허용한다는 것입니다. Angular 클라이언트를 다루고 있고 요청이 다른 원본 URL에서 올 것이기 때문에 이것은 특히 중요합니다.

3.4. 모델 및 저장소

다음으로 Foo 모델에 대해 javax.persistence.Entity 를 정의 해 보겠습니다 .

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

그런 다음 Foo 저장소가 필요합니다 . Spring의 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. 샘플 컨트롤러

이제 DTO를 통해 Foo 리소스를 노출하는 간단한 컨트롤러를 구현해 보겠습니다 .

@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 사용에 유의하십시오 . 이것은 지정된 URL에서 실행되는 Angular 앱의 CORS를 허용하는 데 필요한 컨트롤러 수준 구성입니다.

FooDto는 다음과 같습니다 .

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

4. 프런트 엔드 — 설정

이제 REST API에 액세스 할 클라이언트를위한 간단한 프런트 엔드 Angular 구현을 살펴 보겠습니다.

먼저 Angular CLI를 사용하여 프런트 엔드 모듈을 생성하고 관리합니다.

먼저 Angular CLI가 npm 도구이므로 node 및 npm을 설치합니다 .

그런 다음 프론트 엔드 -maven-plugin 을 사용하여 Maven을 사용하여 Angular 프로젝트를 빌드해야합니다.

   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      

And finally, generate a new Module using Angular CLI:

ng new oauthApp

In the following section, we will discuss the Angular app logic.

5. Authorization Code Flow Using Angular

We're going to use the OAuth2 Authorization Code flow here.

Our use case: The client app requests a code from the Authorization Server and is presented with a login page. Once a user provides their valid credentials and submits, the Authorization Server gives us the code. Then the front-end client uses it to acquire an access token.

5.1. Home Component

Lets' begin with our main component, the HomeComponent, where all the action starts:

@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

마지막으로, Foo 세부 정보를 표시하는 FooComponent :

@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. 앱 구성 요소

루트 구성 요소 역할을하는 간단한 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 프로젝트에서 찾을 수 있습니다.