Автор оригинала: 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 { OptionalfindById(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 CollectionfindAll() { 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: `` }) 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: `` }) 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'); } }Foo Details
{{foo.id}}{{foo.name}}
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 .