Тебе повезло! Я только что опубликовал сообщение в блоге сегодня, в котором показано, как взять приложение Angular + Spring Boot, которое запускается отдельно (с SDK Okta), и упаковать их в один JAR. Вы по-прежнему можете разрабатывать каждое приложение независимо, используя ng serve
и ./gradlew bootRun
, но вы также можете запускать их в одном экземпляре, используя ./gradlew bootRun -Pprod
. Недостатком работы в режиме prod является то, что вы не получите горячей перезагрузки в Angular. Вот шаги, которые я использовал в вышеупомянутом руководстве.
Создайте новую службу AuthService, которая будет взаимодействовать с вашим Spring Boot API для аутентификации logi c.
import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { User } from './user';
import { map } from 'rxjs/operators';
const headers = new HttpHeaders().set('Accept', 'application/json');
@Injectable({
providedIn: 'root'
})
export class AuthService {
$authenticationState = new BehaviorSubject<boolean>(false);
constructor(private http: HttpClient, private location: Location) {
}
getUser(): Observable<User> {
return this.http.get<User>(`${environment.apiUrl}/user`, {headers}).pipe(
map((response: User) => {
if (response !== null) {
this.$authenticationState.next(true);
return response;
}
})
);
}
isAuthenticated(): Promise<boolean> {
return this.getUser().toPromise().then((user: User) => {
return user !== undefined;
}).catch(() => {
return false;
})
}
login(): void {
location.href =
`${location.origin}${this.location.prepareExternalUrl('oauth2/authorization/okta')}`;
}
logout(): void {
const redirectUri = `${location.origin}${this.location.prepareExternalUrl('/')}`;
this.http.post(`${environment.apiUrl}/api/logout`, {}).subscribe((response: any) => {
location.href = response.logoutUrl + '?id_token_hint=' + response.idToken
+ '&post_logout_redirect_uri=' + redirectUri;
});
}
}
Создайте файл user.ts
в том же каталоге для хранения вашей модели User
.
export class User {
sub: number;
fullName: string;
}
Обновите app.component.ts
, чтобы использовать новый AuthService
вместо OktaAuthService
.
import { Component, OnInit } from '@angular/core';
import { AuthService } from './shared/auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'Notes';
isAuthenticated: boolean;
isCollapsed = true;
constructor(public auth: AuthService) {
}
async ngOnInit() {
this.isAuthenticated = await this.auth.isAuthenticated();
this.auth.$authenticationState.subscribe(
(isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated
);
}
}
Измените кнопки в app.component.html
, чтобы они ссылались на службу auth
вместо oktaAuth
.
<button *ngIf="!isAuthenticated" (click)="auth.login()"
class="btn btn-outline-primary" id="login">Login</button>
<button *ngIf="isAuthenticated" (click)="auth.logout()"
class="btn btn-outline-secondary" id="logout">Logout</button>
Обновите home.component.ts
, чтобы также использовать AuthService
.
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../shared/auth.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
isAuthenticated: boolean;
constructor(public auth: AuthService) {
}
async ngOnInit() {
this.isAuthenticated = await this.auth.isAuthenticated();
}
}
Если вы использовали OktaDev Schematics для интеграции Okta в ваше приложение Angular, удалите src/app/auth-routing.module.ts
и src/app/shared/okta
.
Измените app.module.ts
, чтобы удалить импорт AuthRoutingModule
, добавьте HomeComponent
в качестве объявления и импортируйте HttpClientModule
.
Добавьте маршрут для HomeComponent
в app-routing.module.ts
.
import { HomeComponent } from './home/home.component';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: 'home',
component: HomeComponent
}
];
Создайте файл proxy.conf.js
для проксирования определенных запросов к ваш Spring Boot API на http://localhost:8080
.
const PROXY_CONFIG = [
{
context: ['/user', '/api', '/oauth2', '/login'],
target: 'http://localhost:8080',
secure: false,
logLevel: "debug"
}
]
module.exports = PROXY_CONFIG;
Добавьте этот файл в качестве опции proxyConfig
в angular.json
.
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "notes:build",
"proxyConfig": "src/proxy.conf.js"
},
...
},
Удалите Angular SDK Okta и OktaDev Sc hematics из вашего проекта Angular.
npm uninstall @okta/okta-angular @oktadev/schematics
На этом этапе ваше приложение Angular не будет содержать код c, специфичный для Okta, для аутентификации. Вместо этого он полагается на ваше приложение Spring Boot, чтобы предоставить это.
Чтобы настроить приложение Spring Boot для включения Angular, вам необходимо настроить Gradle (или Maven) для создания приложения Spring Boot при переходе -Pprod
, вам необходимо настроить маршруты, чтобы они поддерживали SPA, и измените Spring Security, чтобы разрешить доступ к HTML, CSS и JavaScript.
В моем примере я использовал Gradle и Kotlin.
Сначала создайте RouteController.kt
, который направляет все запросы на index.html
.
package com.okta.developer.notes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import javax.servlet.http.HttpServletRequest
@Controller
class RouteController {
@RequestMapping(value = ["/{path:[^\\.]*}"])
fun redirect(request: HttpServletRequest): String {
return "forward:/"
}
}
Измените SecurityConfiguration.kt
, чтобы разрешить анонимный доступ к stati c веб-файлам, /user
info endpoint и для добавления дополнительных заголовков безопасности.
package com.okta.developer.notes
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.web.csrf.CookieCsrfTokenRepository
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter
import org.springframework.security.web.util.matcher.RequestMatcher
@EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
//@formatter:off
http
.authorizeRequests()
.antMatchers("/**/*.{js,html,css}").permitAll()
.antMatchers("/", "/user").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer().jwt()
http.requiresChannel()
.requestMatchers(RequestMatcher {
r -> r.getHeader("X-Forwarded-Proto") != null
}).requiresSecure()
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
http.headers()
.contentSecurityPolicy("script-src 'self'; report-to /csp-report-endpoint/")
.and()
.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN)
.and()
.featurePolicy("accelerometer 'none'; camera 'none'; microphone 'none'")
//@formatter:on
}
}
Создайте UserController.kt
, который можно использовать, чтобы определить, вошел ли пользователь в систему.
package com.okta.developer.notes
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class UserController() {
@GetMapping("/user")
fun user(@AuthenticationPrincipal user: OidcUser?): OidcUser? {
return user;
}
}
Ранее Angular выполнен выход из системы. Добавьте LogoutController
, который будет обрабатывать истечение сеанса, а также отправлять информацию обратно в Angular, чтобы он мог выйти из Okta.
package com.okta.developer.notes
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.client.registration.ClientRegistration
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
import org.springframework.security.oauth2.core.oidc.OidcIdToken
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest
@RestController
class LogoutController(val clientRegistrationRepository: ClientRegistrationRepository) {
val registration: ClientRegistration = clientRegistrationRepository.findByRegistrationId("okta");
@PostMapping("/api/logout")
fun logout(request: HttpServletRequest,
@AuthenticationPrincipal(expression = "idToken") idToken: OidcIdToken): ResponseEntity<*> {
val logoutUrl = this.registration.providerDetails.configurationMetadata["end_session_endpoint"]
val logoutDetails: MutableMap<String, String> = HashMap()
logoutDetails["logoutUrl"] = logoutUrl.toString()
logoutDetails["idToken"] = idToken.tokenValue
request.session.invalidate()
return ResponseEntity.ok().body<Map<String, String>>(logoutDetails)
}
}
Наконец, я настроил Gradle для создания JAR с Angular включен.
Начните с импорта NpmTask
и добавления подключаемого модуля Node Gradle в build.gradle.kts
:
import com.moowork.gradle.node.npm.NpmTask
plugins {
...
id("com.github.node-gradle.node") version "2.2.4"
...
}
Затем определите расположение вашего Angular приложения и конфигурацию для узла плагин.
val spa = "${projectDir}/../notes";
node {
version = "12.16.2"
nodeModulesDir = file(spa)
}
Добавьте buildWeb
задачу:
val buildWeb = tasks.register<NpmTask>("buildNpm") {
dependsOn(tasks.npmInstall)
setNpmCommand("run", "build")
setArgs(listOf("--", "--prod"))
inputs.dir("${spa}/src")
inputs.dir(fileTree("${spa}/node_modules").exclude("${spa}/.cache"))
outputs.dir("${spa}/dist")
}
И измените задачу processResources
на сборку Angular при передаче -Pprod
.
tasks.processResources {
rename("application-${profile}.properties", "application.properties")
if (profile == "prod") {
dependsOn(buildWeb)
from("${spa}/dist/notes") {
into("static")
}
}
}
Теперь вы можете объединить оба приложения, используя ./gradlew bootJar -Pprod
, или увидеть, как они работают, используя ./gradlew bootRun -Pprod
.