Spring REST API OAuth2 Angular

1. 概要

このチュートリアルでは、OAuthを使用してREST APIを保護し、単純なAngularクライアントから使用します。
作成するアプリケーションは、4つの個別のモジュールで構成されます。
  • 認可サーバー

  • リソースサーバー

  • 暗黙的なUI –暗黙的なフローを使用するフロントエンドアプリ

  • UIパスワード-パスワードフローを使用するフロントエンドアプリ

参考文献:

Spring Security OAuthでJWTを使用する

Spring Security OAuthで対称署名と非対称署名の両方でJSON Webトークンを使用するためのガイド。
link:/spring-security-oauth-jwt [続きを読む]↠’

OAuth2.0および動的クライアント登録

Spring SecurityとOAuth2でクライアントを動的に定義する方法を学びます。
link:/spring-security-oauth-dynamic-client-registration [詳細]↠’
 
始める前に-1つの重要な注意事項。 * Spring Securityのコアチームは、新しいOAuth2スタックの実装を進めています*-いくつかの側面はすでに出ており、いくつかはまだ進行中です。
以下に、その取り組みの背景を説明する簡単なビデオを示します。
 
さあ、すぐに飛び込みましょう。

2. 認可サーバー

最初に、承認サーバーを簡単なSpring Bootアプリケーションとして設定してみましょう。

* 2.1。 Mavenの構成*

次の一連の依存関係を設定します。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>
トークンストアのJDBCを使用した実装を使用するため、spring-jdbcとMySQLを使用していることに注意してください。

* 2.2。 _ @ EnableAuthorizationServer_ *

次に、アクセストークンの管理を行う承認サーバーの構成を開始します。
@Configuration
@EnableAuthorizationServer
public class AuthServerOAuth2Config
  extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(
      AuthorizationServerSecurityConfigurer oauthServer)
      throws Exception {
        oauthServer
          .tokenKeyAccess("permitAll()")
          .checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
      throws Exception {
        clients.jdbc(dataSource())
          .withClient("sampleClientId")
          .authorizedGrantTypes("implicit")
          .scopes("read")
          .autoApprove(true)
          .and()
          .withClient("clientIdPassword")
          .secret("secret")
          .authorizedGrantTypes(
            "password","authorization_code", "refresh_token")
          .scopes("read");
    }

    @Override
    public void configure(
      AuthorizationServerEndpointsConfigurer endpoints)
      throws Exception {

        endpoints
          .tokenStore(tokenStore())
          .authenticationManager(authenticationManager);
    }

    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource());
    }
}
ご了承ください:
  • トークンを永続化するために、_JdbcTokenStore_を使用しました

  • implicit」付与タイプのクライアントを登録しました

  • 別のクライアントを登録し、「_ password_」を承認しました。
    “_authorization_code_â€および“_refresh_token_â€付与タイプ

  • password」付与タイプを使用するには、配線して使用する必要があります
    AuthenticationManager Bean

* 2.3。 データソース設定*

次に、データソースを_JdbcTokenStore_によって使用されるように構成します。
@Value("classpath:schema.sql")
private Resource schemaScript;

@Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
    DataSourceInitializer initializer = new DataSourceInitializer();
    initializer.setDataSource(dataSource);
    initializer.setDatabasePopulator(databasePopulator());
    return initializer;
}

private DatabasePopulator databasePopulator() {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScript(schemaScript);
    return populator;
}

@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
    dataSource.setUrl(env.getProperty("jdbc.url"));
    dataSource.setUsername(env.getProperty("jdbc.user"));
    dataSource.setPassword(env.getProperty("jdbc.pass"));
    return dataSource;
}
_JdbcTokenStore_を使用しているので、データベーススキーマを初期化する必要があるため、_DataSourceInitializer_ –および次のSQLスキーマを使用したことに注意してください。
drop table if exists oauth_client_details;
create table oauth_client_details (
  client_id VARCHAR(255) PRIMARY KEY,
  resource_ids VARCHAR(255),
  client_secret VARCHAR(255),
  scope VARCHAR(255),
  authorized_grant_types VARCHAR(255),
  web_server_redirect_uri VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(255)
);

drop table if exists oauth_client_token;
create table oauth_client_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255)
);

drop table if exists oauth_access_token;
create table oauth_access_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication LONG VARBINARY,
  refresh_token VARCHAR(255)
);

drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication LONG VARBINARY
);

drop table if exists oauth_code;
create table oauth_code (
  code VARCHAR(255), authentication LONG VARBINARY
);

drop table if exists oauth_approvals;
create table oauth_approvals (
    userId VARCHAR(255),
    clientId VARCHAR(255),
    scope VARCHAR(255),
    status VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP
);

drop table if exists ClientDetails;
create table ClientDetails (
  appId VARCHAR(255) PRIMARY KEY,
  resourceIds VARCHAR(255),
  appSecret VARCHAR(255),
  scope VARCHAR(255),
  grantTypes VARCHAR(255),
  redirectUrl VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(255)
);
必ずしも明示的な_DatabasePopulator_ Beanが必要なわけではないことに注意してください。

* 2.4。 セキュリティ構成*

最後に、承認サーバーを保護しましょう。
クライアントアプリケーションがアクセストークンを取得する必要がある場合、簡単なフォームログイン駆動認証プロセスの後にアクセストークンを取得します。
@Configuration
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
      throws Exception {
        auth.inMemoryAuthentication()
          .withUser("john").password("123").roles("USER");
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean()
      throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll();
    }
}
ここでの簡単な注意点は、*パスワードフローではフォームログイン構成は不要*-暗黙的なフローのみであるため、使用しているOAuth2フローによってはスキップできる場合があることです。

*3. リソースサーバー

*
次に、リソースサーバーについて説明します。これは基本的に、最終的に消費できるようにしたいREST APIです。

* 3.1。 Mavenの構成*

リソースサーバーの構成は、以前の承認サーバーアプリケーションの構成と同じです。

* 3.2。 トークンストア設定*

次に、_TokenStore_を設定して、認可サーバーがアクセストークンの保存に使用するのと同じデータベースにアクセスします。
@Autowired
private Environment env;

@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
    dataSource.setUrl(env.getProperty("jdbc.url"));
    dataSource.setUsername(env.getProperty("jdbc.user"));
    dataSource.setPassword(env.getProperty("jdbc.pass"));
    return dataSource;
}

@Bean
public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource());
}
この単純な実装では、承認サーバーとリソースサーバーが別々のアプリケーションであっても、* SQLでバックアップされたトークンストアを共有していることに注意してください。
もちろん、その理由は、リソースサーバーが、承認サーバーによって発行されたアクセストークンの有効性を確認できる必要があるからです。

* 3.3。 リモートトークンサービス*

リソースサーバーで_TokenStore_を使用する代わりに、_RemoteTokeServices_を使用できます。
@Primary
@Bean
public RemoteTokenServices tokenService() {
    RemoteTokenServices tokenService = new RemoteTokenServices();
    tokenService.setCheckTokenEndpointUrl(
      "http://localhost:8080/spring-security-oauth-server/oauth/check_token");
    tokenService.setClientId("fooClientIdPassword");
    tokenService.setClientSecret("secret");
    return tokenService;
}
ご了承ください:
  • この_RemoteTokenService_は_CheckTokenEndPoint_を使用します
    AccessTokenを検証し、_Authentication_オブジェクトを取得する承認サーバー。

  • AuthorizationServerBaseURLで見つけることができます-_ / oauth /check_token_“

  • 認可サーバーは任意のTokenStoreタイプを使用できます
    [JdbcTokenStore, JwtTokenStore, …] – this won’t affect the
    _RemoteTokenService_またはリソースサーバー。

* 3.4。 サンプルコントローラー*

次に、_Foo_リソースを公開する単純なコントローラーを実装しましょう。
@Controller
public class FooController {

    @PreAuthorize("#oauth2.hasScope('read')")
    @RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
    @ResponseBody
    public Foo findById(@PathVariable long id) {
        return
          new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
    }
}
このリソースにアクセスするためにクライアントがどのように_“readâ€_スコープを必要とするかに注意してください。
また、グローバルメソッドセキュリティを有効にし、_MethodSecurityExpressionHandler_を構成する必要があります。
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig
  extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}
基本的な_Foo_リソースは次のとおりです。
public class Foo {
    private long id;
    private String name;
}

* 3.5。 Web設定*

最後に、APIの非常に基本的なWeb構成を設定しましょう。
@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig implements WebMvcConfigurer {}

4. フロントエンド-セットアップ

次に、クライアントの単純なフロントエンドのAngular実装を見ていきます。
最初に、https://cli.angular.io/ [Angular CLI]を使用して、フロントエンドモジュールを生成および管理します。
*まず、Angular CLIはnpmツールであるため、https://nodejs.org/en/download/ [node and npm] *をインストールします。
次に、https://github.com/eirslett/frontend-maven-plugin [_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>
そして最後に、* Angular CLIを使用して新しいモジュールを生成します:*
ng new oauthApp
2つのフロントエンドモジュールがあることに注意してください。1つはパスワードフロー用で、もう1つは暗黙的なフロー用です。
次のセクションでは、各モジュールのAngularアプリのロジックについて説明します。

*5. Angular *を使用したパスワードフロー

ここでは、OAuth2パスワードフローを使用します。これが、*これが単なる概念実証であり、本番対応アプリケーション*ではない理由です。 クライアント資格情報がフロントエンドに公開されていることに気付くでしょう。これについては、今後の記事で説明します。
ユースケースは簡単です。ユーザーが資格情報を提供すると、フロントエンドクライアントはそれらを使用して承認サーバーからアクセストークンを取得します。

* 5.1。 App Service *

_AppService_tsにある_AppService_から始めましょう。これには、サーバーとの対話のロジックが含まれています。
  • obtainAccessToken():ユーザー資格情報を指定してアクセストークンを取得する

  • saveToken():ng2-cookiesを使用してアクセストークンをcookieに保存します
    としょうかん

  • getResource():IDを使用してサーバーからFooオブジェクトを取得します

  • checkCredentials():ユーザーがログインしているかどうかを確認します

  • logout():アクセストークンCookieを削除してユーザーをログアウトする

export class Foo {
  constructor(
    public id: number,
    public name: string) { }
}

@Injectable()
export class AppService {
  constructor(
    private _router: Router, private _http: Http){}

  obtainAccessToken(loginData){
    let params = new URLSearchParams();
    params.append('username',loginData.username);
    params.append('password',loginData.password);
    params.append('grant_type','password');
    params.append('client_id','fooClientIdPassword');
    let headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Basic '+btoa("fooClientIdPassword:secret")});
    let options = new RequestOptions({ headers: headers });

    this._http.post('http://localhost:8081/spring-security-oauth-server/oauth/token',
      params.toString(), options)
      .map(res => res.json())
      .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);
    this._router.navigate(['/']);
  }

  getResource(resourceUrl) : Observable<Foo>{
    var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Bearer '+Cookie.get('access_token')});
    var options = new RequestOptions({ headers: headers });
    return this._http.get(resourceUrl, options)
                   .map((res:Response) => res.json())
                   .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
  }

  checkCredentials(){
    if (!Cookie.check('access_token')){
        this._router.navigate(['/login']);
    }
  }

  logout() {
    Cookie.delete('access_token');
    this._router.navigate(['/login']);
  }
}
ご了承ください:
  • アクセストークンを取得するには、POST_を「 / oauth / token_」に送信します
    終点

  • クライアントクレデンシャルと基本認証を使用して、このエンドポイントにアクセスします

  • 次に、クライアントIDとともにユーザー資格情報を送信し、
    URLエンコードされたグラントタイプパラメータ

  • アクセストークンを取得した後、* Cookieに保存します*

    ここでは、Cookieの保存が特に重要です。Cookieは保存目的でのみ使用されており、認証プロセスを直接駆動するためではありません。 *これは、クロスサイトリクエストフォージェリ(CSRF)タイプの攻撃および脆弱性から保護するのに役立ちます。*

* 5.2。 ログインコンポーネント*

次に、ログインフォームを担当する_ LoginComponent _を見てみましょう。
@Component({
  selector: 'login-form',
  providers: [AppService],
  template: `<h1>Login</h1>
    <input type="text" [(ngModel)]="loginData.username" />
    <input type="password"  [(ngModel)]="loginData.password"/>
    <button (click)="login()" type="submit">Login</button>`
})
export class LoginComponent {
    public loginData = {username: "", password: ""};

    constructor(private _service:AppService) {}

    login() {
        this._service.obtainAccessToken(this.loginData);
    }

* 5.3。 ホームコンポーネント*

次に、ホームページの表示と操作を担当する_HomeComponent_:
@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `<span>Welcome !!</span>
    <a (click)="logout()" href="#">Logout</a>
    <foo-details></foo-details>`
})

export class HomeComponent {
    constructor(
        private _service:AppService){}

    ngOnInit(){
        this._service.checkCredentials();
    }

    logout() {
        this._service.logout();
    }
}

* 5.4。 Fooコンポーネント*

最後に、食品の詳細を表示するcFComponent:
@Component({
  selector: 'foo-details',
  providers: [AppService],
  template: `<h1>Foo Details</h1>
    <label>ID</label> <span>{{foo.id}}</span>
    <label>Name</label> <span>{{foo.name}}</span>
    <button (click)="getFoo()" type="submit">New Foo</button>`
})

export class FooComponent {
    public foo = new Foo(1,'sample foo');
    private foosUrl = 'http://localhost:8082/spring-security-oauth-resource/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: `<router-outlet></router-outlet>`
})

export class AppComponent {}
そして、すべてのコンポーネント、サービス、およびルートをラップする_ * AppModule * _:
@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    LoginComponent,
    FooComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot([
     { path: '', component: HomeComponent },
    { path: 'login', component: LoginComponent }])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

6. 暗黙のフロー

次に、Implicit Flowモジュールに注目します。

* 6.1。 App Service *

同様に、サービスから開始しますが、今回は、アクセストークンを自分で取得する代わりに、ライブラリhttps://github.com/manfredsteyer/angular-oauth2-oidc[angular-oauth2-oidc]を使用します。
@Injectable()
export class AppService {

  constructor(
    private _router: Router, private _http: Http, private oauthService: OAuthService){
        this.oauthService.loginUrl = 'http://localhost:8081/spring-security-oauth-server/oauth/authorize';
        this.oauthService.redirectUri = 'http://localhost:8086/';
        this.oauthService.clientId = "sampleClientId";
        this.oauthService.scope = "read write foo bar";
        this.oauthService.setStorage(sessionStorage);
        this.oauthService.tryLogin({});
    }

  obtainAccessToken(){
      this.oauthService.initImplicitFlow();
  }

  getResource(resourceUrl) : Observable<Foo>{
    var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
     'Authorization': 'Bearer '+this.oauthService.getAccessToken()});
    var options = new RequestOptions({ headers: headers });
    return this._http.get(resourceUrl, options)
      .map((res:Response) => res.json())
      .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
  }

  isLoggedIn(){
    if (this.oauthService.getAccessToken() === null){
       return false;
    }
    return true;
  }

  logout() {
      this.oauthService.logOut();
      location.reload();
  }
}
アクセストークンを取得した後、リソースサーバー内から保護されたリソースを使用するたびに、_Authorization_ヘッダーを介して使用することに注意してください。

* 6.2。 ホームコンポーネント*

シンプルなホームページを処理する_HomeComponent_:
@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `
    <button *ngIf="!isLoggedIn" (click)="login()" type="submit">Login</button>
    <div *ngIf="isLoggedIn">
        <span>Welcome !!</span>
        <a (click)="logout()" href="#">Logout</a>
        <br/>
        <foo-details></foo-details>
    </div>`
})

export class HomeComponent {
    public isLoggedIn = false;

    constructor(
        private _service:AppService){}

    ngOnInit(){
        this.isLoggedIn = this._service.isLoggedIn();
    }

    login() {
        this._service.obtainAccessToken();
    }

    logout() {
        this._service.logout();
    }
}

* 6.3。 Fooコンポーネント*

_FooComponent_は、パスワードフローモジュールとまったく同じです。

* 6.4。 アプリモジュール*

最後に、_AppModule_:
@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    FooComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    OAuthModule.forRoot(),
    RouterModule.forRoot([
     { path: '', component: HomeComponent }])
  ],
  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_で、たとえばポート8086で実行するようにします。
"start": "ng serve --port 8086"

8. 結論

この記事では、OAuth2を使用してアプリケーションを認証する方法を学びました。
このチュートリアルの完全な実装は、https://github.com/eugenp/spring-security-oauth/ [GitHubプロジェクト]にあります。