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 구성

우리의 Resource Server의 pom은 이전 Authorization Server pom과 거의 동일하며 Keycloak 부분이 없으며 추가 spring-boot-starter-oauth2-resource-server 의존성이 있습니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

3.2. Security 구성

Spring Boot를 사용하고 있으므로 Boot 속성을 사용하여 필요한 최소 구성을 정의 할 수 있습니다.

application.yml 파일 에서이 작업을 수행 합니다.

server: 
  port: 8081
  servlet: 
    context-path: /resource-server

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung
          jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

여기서는 인증에 JWT 토큰을 사용하도록 지정했습니다.

jwk 세트-URI 우리의 자원 서버 토큰 '무결성을 확인할 수 있도록 공개 키가 포함 된 URI에 속성 점. 

발급자 URI 속성 (권한 부여 서버 임) 토큰의 발행을 확인하기위한 부가적인 Security 대책을 나타낸다. 그러나이 속성을 추가하면 Resource Server 응용 프로그램을 시작하기 전에 Authorization Server를 실행해야합니다.

다음으로 엔드 포인트를 보호하기 위해 API에 대한 Security 구성을 설정해 보겠습니다 .

@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<Foo, Long> {
}

3.4. 서비스 및 구현

그런 다음 API에 대한 간단한 서비스를 정의하고 구현합니다.

public interface IFooService {
    Optional<Foo> findById(Long id);

    Foo save(Foo foo);
    
    Iterable<Foo> findAll();

}

@Service
public class FooServiceImpl implements IFooService {

    private IFooRepository fooRepository;

    public FooServiceImpl(IFooRepository fooRepository) {
        this.fooRepository = fooRepository;
    }

    @Override
    public Optional<Foo> findById(Long id) {
        return fooRepository.findById(id);
    }

    @Override
    public Foo save(Foo foo) {
        return fooRepository.save(foo);
    }

    @Override
    public Iterable<Foo> 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 = "http://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<FooDto> findAll() {
        Iterable<Foo> foos = this.fooService.findAll();
        List<FooDto> 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 프로젝트를 빌드해야합니다.

<build>
    <plugins>
        <plugin>
            <groupId>com.github.eirslett</groupId>
            <artifactId>frontend-maven-plugin</artifactId>
            <version>1.3</version>
            <configuration>
                <nodeVersion>v6.10.2</nodeVersion>
                <npmVersion>3.10.10</npmVersion>
                <workingDirectory>src/main/resources</workingDirectory>
            </configuration>
            <executions>
                <execution>
                    <id>install node and npm</id>
                    <goals>
                        <goal>install-node-and-npm</goal>
                    </goals>
                </execution>
                <execution>
                    <id>npm install</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                </execution>
                <execution>
                    <id>npm run build</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <arguments>run build</arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

마지막으로 Angular CLI를 사용하여 새 모듈을 생성합니다.

ng new oauthApp

다음 섹션에서는 Angular 앱 논리에 대해 설명합니다.

5. Angular를 사용한 인증 코드 흐름

여기서는 OAuth2 인증 코드 흐름을 사용할 것입니다.

사용 사례 : 클라이언트 앱이 인증 서버에서 코드를 요청하고 로그인 페이지가 표시됩니다. 사용자가 유효한 자격 증명을 제공하고 제출하면 Authorization Server가 코드를 제공합니다. 그런 다음 프런트 엔드 클라이언트는이를 사용하여 액세스 토큰을 얻습니다.

5.1. 홈 구성 요소

모든 작업이 시작되는 주요 구성 요소 인 HomeComponent 부터 시작 하겠습니다 .

@Component({
  selector: 'home-header',
  providers: [AppService],
  template: `<div class="container" >
    <button *ngIf="!isLoggedIn" class="btn btn-primary" (click)="login()" type="submit">
      Login</button>
    <div *ngIf="isLoggedIn" class="content">
      <span>Welcome !!</span>
      <a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>
      <br/>
      <foo-details></foo-details>
    </div>
  </div>`
})
 
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 = 
      'http://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();
  }
}

처음에는 사용자가 로그인하지 않은 경우 로그인 버튼 만 나타납니다. 이 버튼을 클릭하면 사용자는 사용자 이름과 비밀번호를 입력하는 AS의 인증 URL로 이동합니다. 로그인에 성공하면 사용자는 인증 코드로 다시 리디렉션되고이 코드를 사용하여 액세스 토큰을 검색합니다.

5.2. 앱 서비스

이제 서버 상호 작용에 대한 논리가 포함 된 AppService ( app.service.ts 에 있음)를 살펴 보겠습니다 .

  • retrieveToken () : 인증 코드를 사용하여 액세스 토큰을 얻으려면
  • saveToken () : ng2-cookies 라이브러리를 사용하여 쿠키에 액세스 토큰을 저장합니다.
  • getResource () : ID를 사용하여 서버에서 Foo 객체를 가져옵니다.
  • checkCredentials () : 사용자 로그인 여부 확인
  • logout () : 액세스 토큰 쿠키 삭제 및 사용자 로그 아웃
export class Foo {
  constructor(public id: number, public name: string) { }
} 

@Injectable()
export class AppService {
  public clientId = 'newClient';
  public redirectUri = 'http://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('http://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 = 'http://localhost:8089';
  }

  getResource(resourceUrl) : Observable<any> {
    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();
  }
}

에서 retrieveToken의 방법, 우리는 보내 우리의 클라이언트 자격 증명 및 기본 인증을 사용하여 POST를 받는 사람 / 오픈 ID-연결 / 토큰 액세스 토큰을 얻기 위해 엔드 포인트. 매개 변수는 URL 인코딩 형식으로 전송됩니다. 액세스 토큰을 얻은 후 쿠키에 저장합니다.

쿠키 저장은 여기에서 특히 중요합니다. 우리는 저장 목적으로 만 쿠키를 사용하고 인증 프로세스를 직접 구동하지 않기 때문입니다. 이는 CSRF (Cross-Site Request Forgery) 공격 및 취약성으로부터 보호하는 데 도움이됩니다.

5.3. 푸 구성 요소

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

@Component({
  selector: 'foo-details',
  providers: [AppService],  
  template: `<div class="container">
    <h1 class="col-sm-12">Foo Details</h1>
    <div class="col-sm-12">
        <label class="col-sm-3">ID</label> <span>{{foo.id}}</span>
    </div>
    <div class="col-sm-12">
        <label class="col-sm-3">Name</label> <span>{{foo.name}}</span>
    </div>
    <div class="col-sm-12">
        <button class="btn btn-primary" (click)="getFoo()" type="submit">New Foo</button>        
    </div>
  </div>`
})

export class FooComponent {
  public foo = new Foo(1,'sample foo');
  private foosUrl = 'http://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: `<nav class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <a class="navbar-brand" href="/">Spring Security Oauth - Authorization Code</a>
      </div>
    </div>
  </nav>
  <router-outlet></router-outlet>`
})

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 프로젝트 에서 찾을 수 있습니다 .