认证后回调时出现404错误(Spring Boot + Angular + Okta)
Getting 404 error when callback after authentication(Spring Boot + Angular + Okta)
您好,我现在使用Angular + Spring Boot 来建站,在我的网站中,我使用Okta Single-Page App 进行身份验证。对于前端,我使用 okta-angular,并按照此处的说明进行操作:https://github.com/okta/okta-oidc-js/tree/master/packages/okta-angular。我正在使用隐式流。为了简单起见,我使用了 okta 托管的 sign-in 小部件。
我的前端代码是这样的:
app.module.ts
import {
OKTA_CONFIG,
OktaAuthModule
} from '@okta/okta-angular';
const oktaConfig = {
issuer: 'https://{yourOktaDomain}.com/oauth2/default',
clientId: '{clientId}',
redirectUri: 'http://localhost:{port}/implicit/callback',
pkce: true
}
@NgModule({
imports: [
...
OktaAuthModule
],
providers: [
{ provide: OKTA_CONFIG, useValue: oktaConfig }
],
})
export class MyAppModule { }
然后我在 app-routing.module.ts
中使用 OktaAuthGuard
import {
OktaAuthGuard,
...
} from '@okta/okta-angular';
const appRoutes: Routes = [
{
path: 'protected',
component: MyProtectedComponent,
canActivate: [ OktaAuthGuard ],
},
...
]
同样在 app-routing.module.ts 我正在使用 OktaCallBackComponent.
当然我有 login/logout 按钮在 headers:
import { Component, OnInit } from '@angular/core';
import {OktaAuthService} from '@okta/okta-angular';
@Component({
selector: 'app-header',
templateUrl: './app-header.component.html',
styleUrls: ['./app-header.component.scss']
})
export class AppHeaderComponent implements OnInit {
isAuthenticated: boolean;
constructor(public oktaAuth: OktaAuthService) {
// Subscribe to authentication state changes
this.oktaAuth.$authenticationState.subscribe(
(isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated
);
}
async ngOnInit() {
this.isAuthenticated = await this.oktaAuth.isAuthenticated();
}
login() {
this.oktaAuth.loginRedirect('/');
}
logout() {
this.oktaAuth.logout('/');
}
}
<nav class="navbar navbar-expand-lg navbar-light">
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" *ngIf="!isAuthenticated" (click)="login()"> Login </a>
<a class="nav-link" *ngIf="isAuthenticated" (click)="logout()"> Logout </a>
</li>
</ul>
</div>
</nav>
前端用户登录后,我会将Authoirization header传递给后端,并且
在后端,我使用 spring 安全来保护后端 api。
像这样:
import com.okta.spring.boot.oauth.Okta;
import lombok.RequiredArgsConstructor;
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.config.http.SessionCreationPolicy;
@RequiredArgsConstructor
@EnableWebSecurity
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// Disable CSRF (cross site request forgery)
http.csrf().disable();
// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.oauth2ResourceServer().opaqueToken();
Okta.configureResourceServer401ResponseBody(http);
}
}
如果我 运行 angular 和 spring 在终端中分别启动,一切正常。可以登录,后台可以获取用户信息
但问题是当我们使用 gradle 构建和部署时,我们会将 angular 编译后的代码放到 spring 引导项目下的静态文件夹中。此时如果我运行项目:
java -jar XX.jar
我在 localhost:8080 打开。
我登录了,那么这个时候认证回调会抛出404 not found错误
在我的理解中,原因是当我运行 jar 文件时,我没有为"callback" url定义控制器。但是如果我 运行 angular 和 spring 分别启动, angular 由 nodejs 托管,并且我使用了 okta callbackcomponent,所以一切正常。
那么我应该怎么做才能解决这个问题呢?我的意思是,我应该怎么做才能让它作为 jar 文件工作?我应该定义一个回调控制器吗?但是我应该在回调控制器中做什么?会不会和前端代码冲突??
你走运了!我刚刚在上述教程中发布了一个blog post today that shows how to take an Angular + Spring Boot app that runs separately (with Okta's SDKs) and package them in a single JAR. You can still develop each app independently using ng serve
and ./gradlew bootRun
, but you can also run them in a single instance using ./gradlew bootRun -Pprod
. The disadvantage to running in prod mode is you won't get hot-reload in Angular. Here are the steps I used。
创建一个新的 AuthService 服务,该服务将与您的 Spring 启动 API 进行身份验证逻辑通信。
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 在 http://localhost:8080
启动 API。
const PROXY_CONFIG = [
{
context: ['/user', '/api', '/oauth2', '/login'],
target: 'http://localhost:8080',
secure: false,
logLevel: "debug"
}
]
module.exports = PROXY_CONFIG;
将此文件添加为 angular.json
中的 proxyConfig
选项。
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "notes:build",
"proxyConfig": "src/proxy.conf.js"
},
...
},
从您的 Angular 项目中删除 Okta 的 Angular SDK 和 OktaDev 原理图。
npm uninstall @okta/okta-angular @oktadev/schematics
此时,您的 Angular 应用将不包含任何 Okta-specific 身份验证代码。相反,它依赖于您的 Spring 启动应用程序来提供。
要配置您的 Spring 启动应用程序以包含 Angular,您需要配置 Gradle(或 Maven)以在您传入时构建您的 Spring 启动应用程序-Pprod
,您需要将路由调整为 SPA-aware,并修改 Spring 安全性以允许访问 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
以允许匿名访问静态 Web 文件、/user
信息端点,并添加额外的安全性 headers。
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
来处理 session 的过期以及将信息发送回 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 构建一个包含 Angular 的 JAR。
首先导入 NpmTask
并在 build.gradle.kts
中添加节点 Gradle 插件:
import com.moowork.gradle.node.npm.NpmTask
plugins {
...
id("com.github.node-gradle.node") version "2.2.4"
...
}
然后,定义 Angular 应用的位置和 Node 插件的配置。
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")
}
并在传入-Pprod
时修改processResources
任务构建Angular
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
.
查看它们 运行
为了一个简单的解决方案,我在 spring 引导中添加了一个配置文件以将 implicit/callback 重新路由到 angular "index.html":
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException;
@Configuration
public class ReroutingConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/implicit/**", "/home")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
它有效,但我不确定这是否是一个好的做法。
您好,我现在使用Angular + Spring Boot 来建站,在我的网站中,我使用Okta Single-Page App 进行身份验证。对于前端,我使用 okta-angular,并按照此处的说明进行操作:https://github.com/okta/okta-oidc-js/tree/master/packages/okta-angular。我正在使用隐式流。为了简单起见,我使用了 okta 托管的 sign-in 小部件。
我的前端代码是这样的:
app.module.ts
import {
OKTA_CONFIG,
OktaAuthModule
} from '@okta/okta-angular';
const oktaConfig = {
issuer: 'https://{yourOktaDomain}.com/oauth2/default',
clientId: '{clientId}',
redirectUri: 'http://localhost:{port}/implicit/callback',
pkce: true
}
@NgModule({
imports: [
...
OktaAuthModule
],
providers: [
{ provide: OKTA_CONFIG, useValue: oktaConfig }
],
})
export class MyAppModule { }
然后我在 app-routing.module.ts
中使用 OktaAuthGuardimport {
OktaAuthGuard,
...
} from '@okta/okta-angular';
const appRoutes: Routes = [
{
path: 'protected',
component: MyProtectedComponent,
canActivate: [ OktaAuthGuard ],
},
...
]
同样在 app-routing.module.ts 我正在使用 OktaCallBackComponent.
当然我有 login/logout 按钮在 headers:
import { Component, OnInit } from '@angular/core';
import {OktaAuthService} from '@okta/okta-angular';
@Component({
selector: 'app-header',
templateUrl: './app-header.component.html',
styleUrls: ['./app-header.component.scss']
})
export class AppHeaderComponent implements OnInit {
isAuthenticated: boolean;
constructor(public oktaAuth: OktaAuthService) {
// Subscribe to authentication state changes
this.oktaAuth.$authenticationState.subscribe(
(isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated
);
}
async ngOnInit() {
this.isAuthenticated = await this.oktaAuth.isAuthenticated();
}
login() {
this.oktaAuth.loginRedirect('/');
}
logout() {
this.oktaAuth.logout('/');
}
}
<nav class="navbar navbar-expand-lg navbar-light">
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" *ngIf="!isAuthenticated" (click)="login()"> Login </a>
<a class="nav-link" *ngIf="isAuthenticated" (click)="logout()"> Logout </a>
</li>
</ul>
</div>
</nav>
前端用户登录后,我会将Authoirization header传递给后端,并且 在后端,我使用 spring 安全来保护后端 api。 像这样:
import com.okta.spring.boot.oauth.Okta;
import lombok.RequiredArgsConstructor;
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.config.http.SessionCreationPolicy;
@RequiredArgsConstructor
@EnableWebSecurity
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// Disable CSRF (cross site request forgery)
http.csrf().disable();
// No session will be created or used by spring security
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.oauth2ResourceServer().opaqueToken();
Okta.configureResourceServer401ResponseBody(http);
}
}
如果我 运行 angular 和 spring 在终端中分别启动,一切正常。可以登录,后台可以获取用户信息
但问题是当我们使用 gradle 构建和部署时,我们会将 angular 编译后的代码放到 spring 引导项目下的静态文件夹中。此时如果我运行项目:
java -jar XX.jar
我在 localhost:8080 打开。
我登录了,那么这个时候认证回调会抛出404 not found错误
在我的理解中,原因是当我运行 jar 文件时,我没有为"callback" url定义控制器。但是如果我 运行 angular 和 spring 分别启动, angular 由 nodejs 托管,并且我使用了 okta callbackcomponent,所以一切正常。
那么我应该怎么做才能解决这个问题呢?我的意思是,我应该怎么做才能让它作为 jar 文件工作?我应该定义一个回调控制器吗?但是我应该在回调控制器中做什么?会不会和前端代码冲突??
你走运了!我刚刚在上述教程中发布了一个blog post today that shows how to take an Angular + Spring Boot app that runs separately (with Okta's SDKs) and package them in a single JAR. You can still develop each app independently using ng serve
and ./gradlew bootRun
, but you can also run them in a single instance using ./gradlew bootRun -Pprod
. The disadvantage to running in prod mode is you won't get hot-reload in Angular. Here are the steps I used。
创建一个新的 AuthService 服务,该服务将与您的 Spring 启动 API 进行身份验证逻辑通信。
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 在 http://localhost:8080
启动 API。
const PROXY_CONFIG = [
{
context: ['/user', '/api', '/oauth2', '/login'],
target: 'http://localhost:8080',
secure: false,
logLevel: "debug"
}
]
module.exports = PROXY_CONFIG;
将此文件添加为 angular.json
中的 proxyConfig
选项。
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "notes:build",
"proxyConfig": "src/proxy.conf.js"
},
...
},
从您的 Angular 项目中删除 Okta 的 Angular SDK 和 OktaDev 原理图。
npm uninstall @okta/okta-angular @oktadev/schematics
此时,您的 Angular 应用将不包含任何 Okta-specific 身份验证代码。相反,它依赖于您的 Spring 启动应用程序来提供。
要配置您的 Spring 启动应用程序以包含 Angular,您需要配置 Gradle(或 Maven)以在您传入时构建您的 Spring 启动应用程序-Pprod
,您需要将路由调整为 SPA-aware,并修改 Spring 安全性以允许访问 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
以允许匿名访问静态 Web 文件、/user
信息端点,并添加额外的安全性 headers。
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
来处理 session 的过期以及将信息发送回 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 构建一个包含 Angular 的 JAR。
首先导入 NpmTask
并在 build.gradle.kts
中添加节点 Gradle 插件:
import com.moowork.gradle.node.npm.NpmTask
plugins {
...
id("com.github.node-gradle.node") version "2.2.4"
...
}
然后,定义 Angular 应用的位置和 Node 插件的配置。
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")
}
并在传入-Pprod
时修改processResources
任务构建Angular
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
.
为了一个简单的解决方案,我在 spring 引导中添加了一个配置文件以将 implicit/callback 重新路由到 angular "index.html":
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException;
@Configuration
public class ReroutingConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/implicit/**", "/home")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
它有效,但我不确定这是否是一个好的做法。