Рубрики
Без рубрики

Spring REST API + OAuth2 + Угловой

Узнайте, как настроить OAuth2 для API Spring REST с помощью Spring Security 5 и как использовать его из клиента Angular.

Автор оригинала: Sampada Wagde.

1. Обзор

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

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

  • Сервер авторизации
  • Сервер ресурсов
  • Код авторизации пользовательского интерфейса: интерфейсное приложение, использующее поток кода авторизации

Мы будем использовать стек OAuth в Spring Security 5. Если вы хотите использовать стек Spring Security OAuth legacy, ознакомьтесь с этой предыдущей статьей: Spring REST API + OAuth2 + Angular (используя стек Spring Security OAuth Legacy) .

Дальнейшее чтение:

Использование JWT с Spring Security OAuth

OAuth2.0 и динамическая регистрация клиента (с использованием устаревшего стека Spring Security OAuth)

Давайте сразу перейдем к делу.

2. Сервер авторизации OAuth2 (КАК)

Проще говоря, Сервер авторизации-это приложение, которое выдает токены для авторизации.

Ранее стек Spring Security OAuth предлагал возможность настройки сервера авторизации в качестве приложения Spring. Но проект устарел, главным образом потому, что OAuth является открытым стандартом со многими хорошо зарекомендовавшими себя поставщиками, такими как Okta, Keycloak и ForgeRock, и это лишь некоторые из них.

Из них мы будем использовать Keycloak . Это сервер управления идентификацией и доступом с открытым исходным кодом, администрируемый Red Hat, разработанный на Java компанией JBoss. Он поддерживает не только OAuth2, но и другие стандартные протоколы, такие как OpenID Connect и SAML.

Для этого урока/| мы будем настраивать встроенный сервер Keycloak в приложении Spring Boot .

3. Сервер ресурсов (RS)

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

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

Pom нашего сервера ресурсов во многом такой же, как и предыдущий pom сервера авторизации, без ключевой части и с дополнительной spring-boot-starter-oauth2-resource-server зависимостью :


    org.springframework.boot
    spring-boot-starter-oauth2-resource-server

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-url свойство указывает на URI, содержащий открытый ключ, чтобы наш сервер ресурсов мог проверить целостность токенов.

Свойство эмитент-uri представляет собой дополнительную меру безопасности для проверки эмитента токенов (который является Сервером авторизации). Однако добавление этого свойства также требует, чтобы Сервер авторизации был запущен до того, как мы сможем запустить приложение Сервера ресурсов.

Далее, давайте настроим конфигурацию безопасности для 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 запрашивающий должен иметь полномочия write в дополнение к read . Однако для любой другой конечной точки запрос должен быть просто аутентифицирован любым пользователем.

Кроме того, метод oauth2 ResourceServer() указывает, что это сервер ресурсов с jwt()- отформатированными токенами.

Еще один момент, который следует отметить здесь,-это использование метода cors() для разрешения заголовков управления доступом в запросах. Это особенно важно, поскольку мы имеем дело с клиентом Angular, и наши запросы будут поступать с другого исходного URL-адреса.

3.4. Модель и Репозиторий

Далее, давайте определим javax.persistence.Сущность для нашей модели, Foo :

@Entity
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    
    // constructor, getters and setters
}

Тогда нам нужен репозиторий Foo s. Мы будем использовать 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. Образец Контроллера

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

@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 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 выше; это конфигурация уровня контроллера, которая нам нужна, чтобы разрешить CORS из нашего углового приложения, работающего по указанному URL-адресу.

Вот наша еда для :

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

4. Настройка Переднего Конца

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

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

Во-первых, мы устанавливаем node и npm , так как Angular CLI-это инструмент npm.

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


    
        
            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
                    
                
            
        
    

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

ng new oauthApp

В следующем разделе мы обсудим логику приложения Angular.

5. Поток Кода Авторизации С Использованием Углового

Здесь мы будем использовать поток кода авторизации OAuth2.

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

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

Давайте начнем с нашего основного компонента, компонента Home , с которого начинается все действие:

@Component({
  selector: 'home-header',
  providers: [AppService],
  template: `
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 = '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(); } }

В начале, когда пользователь не вошел в систему, появляется только кнопка входа в систему. После нажатия этой кнопки пользователь переходит на URL-адрес авторизации Ass, где он вводит имя пользователя и пароль. После успешного входа в систему пользователь перенаправляется обратно с кодом авторизации, а затем мы получаем маркер доступа с помощью этого кода.

5.2. Сервис приложений

Теперь давайте посмотрим на AppService — расположенный в app.service.ts — который содержит логику взаимодействия с сервером:

  • retrieveToken() : для получения маркера доступа с помощью кода авторизации
  • сохранить токен() : чтобы сохранить токен доступа в файле cookie с помощью библиотеки ng2-cookies
  • getResource() : чтобы получить объект Foo с сервера, используя его идентификатор
  • проверьте учетные данные() : чтобы проверить, вошел ли пользователь в систему или нет
  • выход из системы() : для удаления файла 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 {
    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

Наконец, наш Пищевой компонент для отображения сведений о наших продуктах питания:

@Component({
  selector: 'foo-details',
  providers: [AppService],  
  template: `

Foo Details

{{foo.id}}
{{foo.name}}
` }) 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. Компонент приложения

Наш простой Компонент приложения для работы в качестве корневого компонента:

@Component({
  selector: 'app-root',
  template: `
  `
})

export class AppComponent { }

И модуль App , в который мы упаковываем все наши компоненты, сервисы и маршруты:

@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. Затем нам нужно перейти в наш каталог угловых приложений:

cd src/main/resources

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

npm start

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

"start": "ng serve"

в package.json; например, чтобы запустить его на порту 8089, добавьте:

"start": "ng serve --port 8089"

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

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

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