SpringRESTAPI+OAuth2+Angular
1. 概要
このチュートリアルでは、OAuth2でRESTAPIを保護し、単純なAngularクライアントから使用します。
構築するアプリケーションは、次の3つの個別のモジュールで構成されます。
- 承認サーバー
- リソースサーバー
- UI認証コード:認証コードフローを使用するフロントエンドアプリケーション
SpringSecurity5でOAuthスタックを使用します。SpringSecurity OAuthレガシースタックを使用する場合は、次の前の記事を参照してください: Spring REST API + OAuth2 + Angular( Spring Security OAuth Legacy Stack)。
すぐに飛び込みましょう。
2. OAuth2認証サーバー(AS)
簡単に言えば、承認サーバーは承認のためにトークンを発行するアプリケーションです。
以前は、Spring Security OAuthスタックは、承認サーバーをSpringアプリケーションとしてセットアップする可能性を提供していました。 しかし、このプロジェクトは廃止されました。これは主に、OAuthが、Okta、Keycloak、ForgeRockなどの多くの定評のあるプロバイダーによるオープンスタンダードであるためです。
このうち、Keycloakを使用します。 これは、JBossによってJavaで開発された、RedHatによって管理されるオープンソースのIdentityandAccessManagementサーバーです。 OAuth2だけでなく、OpenIDConnectやSAMLなどの他の標準プロトコルもサポートします。
このチュートリアルでは、Spring Bootアプリに組み込みKeycloakサーバーをセットアップします。
3. リソースサーバー(RS)
次に、リソースサーバーについて説明します。 これは本質的にRESTAPIであり、最終的には使用できるようにしたいと考えています。
3.1. Maven構成
リソースサーバーの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. セキュリティ構成
Spring 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-set-uri プロパティは、公開鍵を含むURIを指しているため、リソースサーバーはトークンの整合性を検証できます。
issuer-uri プロパティは、トークンの発行者(承認サーバー)を検証するための追加のセキュリティ対策を表します。 ただし、このプロパティを追加すると、ResourceServerアプリケーションを起動する前にAuthorizationServerを実行する必要があります。
次に、エンドポイントを保護するための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メソッドでは、readスコープを持つリクエストのみを許可します。 POST方式の場合、リクエスターは読み取りに加えて書き込み権限を持っている必要があります。 ただし、他のエンドポイントの場合、リクエストは任意のユーザーで認証される必要があります。
また、 oauth2ResourceServer()メソッドは、これが jwt()-形式のトークンを持つリソースサーバーであることを指定します。
ここで注意すべきもう1つのポイントは、メソッド 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. フロントエンド—セットアップ
次に、RESTAPIにアクセスするクライアント用の単純なフロントエンドAngular実装を見ていきます。
最初にAngularCLI を使用して、フロントエンドモジュールを生成および管理します。
まず、Angular CLIはnpmツールであるため、ノードとnpmをインストールします。
次に、 frontend-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>
そして最後に、はAngularCLIを使用して新しいモジュールを生成します:
ng new oauthApp
次のセクションでは、Angularアプリのロジックについて説明します。
5. Angularを使用した認証コードフロー
ここでは、OAuth2認証コードフローを使用します。
ユースケース:クライアントアプリが認証サーバーにコードを要求し、ログインページが表示されます。 ユーザーが有効な資格情報を提供して送信すると、認証サーバーからコードが提供されます。次に、フロントエンドクライアントはそれを使用してアクセストークンを取得します。
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ライブラリを使用してアクセストークンをcookieに保存します
- getResource():IDを使用してサーバーからFooオブジェクトを取得します
- checkCredentials():ユーザーがログインしているかどうかを確認します
- logout():アクセストークンCookieを削除し、ユーザーをログアウトします
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を/openid-connect /tokenエンドポイントに送信してアクセストークンを取得します。 パラメータはURLエンコードされた形式で送信されます。 アクセストークンを取得したら、Cookieに保存します。
ここではCookieの保存が特に重要です。これは、Cookieを保存目的でのみ使用しており、認証プロセスを直接実行するためではないためです。 これは、クロスサイトリクエストフォージェリ(CSRF)攻撃と脆弱性からの保護に役立ちます。
5.3. Fooコンポーネント
最後に、 FooComponent を使用して、Fooの詳細を表示します。
@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プロジェクトにあります。