«1. Обзор

В этом руководстве мы защитим REST API с помощью OAuth и используем его из простого клиента Angular.

Приложение, которое мы собираемся создать, будет состоять из четырех отдельных модулей:

    Пользовательский интерфейс Сервера ресурсов Сервера авторизации, неявный — внешнее приложение, использующее пароль пользовательского интерфейса неявного потока — внешнее приложение, использующее Поток паролей

Примечание. В этой статье используется устаревший проект Spring OAuth. Версию этой статьи, использующую новый стек Spring Security 5, можно найти в нашей статье Spring REST API + OAuth2 + Angular.

Хорошо, давайте сразу приступим.

2. Сервер авторизации

Во-первых, давайте начнем настройку сервера авторизации как простого приложения Spring Boot.

2.1. Конфигурация Maven

Мы настроим следующий набор зависимостей:

Обратите внимание, что мы используем spring-jdbc и MySQL, потому что мы собираемся использовать реализацию хранилища токенов, поддерживаемую JDBC.

2.2. @EnableAuthorizationServer

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

Теперь приступим к настройке сервера авторизации, отвечающего за управление токенами доступа:

Обратите внимание:

Чтобы сохранить токены, мы использовали JdbcTokenStore. Мы зарегистрировали клиент для — «Неявный» тип гранта Мы зарегистрировали другого клиента и авторизовали типы гранта «password», «authorization_code» и «refresh_token». Чтобы использовать тип гранта «password», нам нужно подключиться и использовать bean-компонент AuthenticationManager ~ ~~ 2.3. Конфигурация источника данных

@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:

    Обратите внимание, что поскольку мы используем JdbcTokenStore, нам нужно инициализировать схему базы данных, поэтому мы использовали DataSourceInitializer — и следующая схема SQL:

Обратите внимание, что нам не обязательно нужен явный bean-компонент DatabasePopulator — мы можем просто использовать schema.sql — который Spring Boot использует по умолчанию.

2.4. Настройка безопасности

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

Наконец, давайте защитим сервер авторизации.

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)
);

Когда клиентскому приложению необходимо получить токен доступа, оно сделает это после простого процесса аутентификации, основанного на форме входа в систему: Поток паролей — только для неявного потока — поэтому вы можете пропустить его в зависимости от того, какой поток OAuth2 вы используете.

3. Сервер ресурсов

Теперь давайте обсудим сервер ресурсов; по сути, это REST API, который мы в конечном итоге хотим использовать.

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

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

Наша конфигурация сервера ресурсов такая же, как и предыдущая конфигурация приложения Authorization Server.

3.2. Конфигурация хранилища токенов

Далее мы настроим наш TokenStore для доступа к той же базе данных, которую сервер авторизации использует для хранения токенов доступа:

Обратите внимание, что для этой простой реализации мы совместно используем токен, поддерживаемый SQL store, даже если серверы авторизации и ресурсов являются отдельными приложениями.

Причина, конечно же, в том, что сервер ресурсов должен иметь возможность проверять достоверность токенов доступа, выданных сервером авторизации.

3.3. Remote Token Service

Вместо использования TokenStore на нашем сервере ресурсов мы можем использовать RemoteTokeServices:

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

Обратите внимание, что:

Этот RemoteTokenService будет использовать CheckTokenEndPoint на сервере авторизации для проверки AccessToken и получения от него объекта аутентификации. . Его можно найти по адресу AuthorizationServerBaseURL +» /oauth/check_token». Сервер авторизации может использовать любой тип TokenStore [JdbcTokenStore, JwtTokenStore, …] — это не повлияет на RemoteTokenService или сервер ресурсов.

3.4. Пример контроллера

Далее давайте реализуем простой контроллер, открывающий ресурс Foo:

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

Обратите внимание, что клиенту нужна область «чтения» для доступа к этому ресурсу.

    Нам также нужно включить глобальную защиту методов и настроить MethodSecurityExpressionHandler:

А вот наш основной ресурс Foo:

3.5. Веб-конфигурация

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

Наконец, давайте настроим очень простую веб-конфигурацию для API:

«

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig 
  extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}

«4. Внешний интерфейс — настройка

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

Теперь мы рассмотрим простую реализацию внешнего интерфейса Angular для клиента.

Во-первых, мы будем использовать Angular CLI для создания наших интерфейсных модулей и управления ими.

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig implements WebMvcConfigurer {}

Сначала мы установим node и npm, поскольку Angular CLI — это инструмент npm.

Затем нам нужно использовать frontend-maven-plugin для создания нашего проекта Angular с помощью maven:

И, наконец, сгенерировать новый модуль с помощью Angular CLI:

Обратите внимание, что мы У нас будет два внешних модуля — один для потока паролей, а другой для неявного потока.

В следующих разделах мы обсудим логику приложения 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>

5. Поток паролей с использованием Angular

ng new oauthApp

Здесь мы собираемся использовать поток паролей OAuth2 — вот почему это просто доказательство концепции, а не готовое к производству приложение. Вы заметите, что учетные данные клиента доступны внешнему интерфейсу — об этом мы поговорим в следующей статье.

Наш вариант использования прост: как только пользователь предоставляет свои учетные данные, внешний клиент использует их для получения токена доступа с сервера авторизации.

5.1. Служба приложений

Давайте начнем с нашей службы приложений, расположенной по адресу app.service.ts, которая содержит логику для взаимодействия с сервером: сохранить наш токен доступа в файле cookie, используя библиотеку ng2-cookies. getResource(): чтобы получить объект Foo с сервера, используя его идентификатор. пользователь вышел

Обратите внимание, что:

Чтобы получить токен доступа, мы отправляем POST в конечную точку «/oauth/token». Мы используем учетные данные клиента и базовую аутентификацию для доступа к этой конечной точке. Затем мы отправляем учетные данные пользователя вместе с идентификатором клиента и параметрами типа гранта. URL-адрес закодирован. После того, как мы получим токен доступа, мы сохраним его в файле cookie

Хранение файлов cookie здесь особенно важно, потому что мы используем только файл cookie для целей хранения, а не для непосредственного управления процессом аутентификации. Это помогает защититься от атак и уязвимостей, связанных с подделкой межсайтовых запросов (CSRF).

    5.2. Компонент входа в систему
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']);
  }
}

Теперь давайте взглянем на наш компонент входа в систему, который отвечает за форму входа в систему:

    5.3. Домашний компонент

Далее, наш HomeComponent, который отвечает за отображение и управление нашей домашней страницей:

5.4. Компонент Foo

Наконец, наш FooComponent для отображения подробностей о Foo:

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

Наш простой AppComponent, который будет выступать в качестве корневого компонента:

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

И AppModule, куда мы помещаем все наши компоненты, сервисы и маршруты:

6. Неявный поток

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

Далее, мы сосредоточимся на модуле Implicit Flow.

6.1. Служба приложений

@Component({
    selector: 'app-root',
    template: `<router-outlet></router-outlet>`
})

export class AppComponent {}

Аналогично мы начнем с нашей службы, но на этот раз мы будем использовать библиотеку angular-oauth2-oidc вместо того, чтобы самим получать токен доступа:

@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.2. Компонент Home

Наш компонент HomeComponent для обработки нашей простой домашней страницы:

6.3. Foo Component

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

Наш FooComponent точно такой же, как в модуле потока паролей.

6.4. App Module

Наконец, наш AppModule:

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

7. Запуск внешнего интерфейса

1. Чтобы запустить любой из наших внешних модулей, нам нужно сначала собрать приложение:

~~ ~ 2. Затем нам нужно перейти в каталог нашего приложения Angular:

3. Наконец, мы запустим наше приложение:

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    FooComponent    
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    OAuthModule.forRoot(),    
    RouterModule.forRoot([
     { path: '', component: HomeComponent }])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Сервер запустится по умолчанию на порту 4200, чтобы изменить порт любого модуля измените

в package.json, чтобы он работал на порту 8086, например:

mvn clean install

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

cd src/main/resources

В этой статье мы узнали, как авторизовать наше приложение с помощью OAuth2. .

npm start

Полную реализацию этого руководства можно найти в проекте GitHub.

"start": "ng serve"

«

"start": "ng serve --port 8086"

8. Conclusion

In this article, we learned how to authorize our application using OAuth2.

The full implementation of this tutorial can be found in the GitHub project.