在本教程中,你将创建一个单页应用程序(SPA),使用Spring Boot资源服务器和Vue前端客户端。你将看到如何配置Spring Boot以使用JSON Web Tokens(JWT)进行认证和授权,将Okta作为OAuth 2.0和OpenID Connect(OIDC)的提供者。你还会看到如何用Vue CLI启动一个Vue客户端应用,以及如何使用Okta签到小工具来保护它。
Okta是一家计算机安全服务公司,为保障网络应用安全提供了很多很好的资源。Okta Sign-In Wdiget是保护前端应用程序的一个好方法,因为它允许你轻松地添加一个安全的登录表单,该表单可配置为单点登录和与外部供应商(如谷歌、Facebook和LinkedIn)的社交登录。它提供了一个使用PKCE(代码交换的证明密钥)的授权代码OAuth 2.0流程的实现。
PKCE是对授权代码流程的修改,不要求应用程序拥有客户秘密,这使得它适用于代码基本公开的客户应用程序。在前端应用程序上实施安全的授权代码流需要正确地处理重定向和令牌交换,这可能有点复杂。幸运的是,Okta已经大大简化了这个过程,为你处理了很多复杂的问题。
目录
- 创建Spring Boot资源服务器
- Spring Boot和CORS
- 为Okta Auth配置资源服务器
- 使用Vue CLI来引导客户端应用程序
- 在Okta上创建一个单页的OIDC应用
- 完成Vue前端客户端应用程序
- 测试完成的服务器和客户端
- 了解更多关于应用安全的信息
要求
在你开始之前,你需要确保你已经安装了一些工具。
-
Okta CLI:Okta CLI是创建使用Okta安全的项目的一个简单方法。按照Okta CLI项目网站上的安装说明进行操作。在继续学习本教程之前,你应该使用CLI登录你的现有账户或注册一个新账户。
-
Java 11:该项目使用Java 11。OpenJDK 11也可以工作。 说明可在OpenJDK网站上找到。OpenJDK也可以用Homebrew来安装。另外,SDKMAN是安装和管理Java版本的另一个很好的选择。
-
Vue CLI:你将使用Vue CLI来生成入门的Vue客户端项目。根据Vue网站上的说明安装它。
创建Spring Boot资源服务器
为了启动Spring Boot资源服务器项目,你将使用Spring Initializr。它旨在帮助开发人员快速配置和生成Spring Boot项目。有一个很好的网络界面,你应该看一看。然而,在这个项目中,你将使用REST接口来下载一个预配置的项目。
这个项目将有一个Vue客户端和一个Spring Boot服务器项目,所以你可能想做一个父目录,叫做Spring Boot SPA 。使用下面的命令下载并提取父目录下的启动项目。
curl https://start.spring.io/starter.tgz \
-d type=maven-project \
-d language=java \
-d platformVersion=2.5.4 \
-d jvmVersion=11 \
-d artifactId=spring-boot-spa \
-d baseDir=spring-boot-spa \
-d packageName=com.okta.springbootspa \
-d group=com.okta \
-d dependencies=web,okta \
| tar -xzvf -
上面的命令配置了Spring Boot项目的各个方面,包括Spring Boot版本和Java版本。它还配置了两个依赖项。
-
第一个,
web,包括Spring MVC,Spring的标准Web包。 -
第二个,
okta,包括Okta的Spring Boot启动器。
你可以在GitHub上查看Okta Spring Boot Starter项目的更多信息,但简单地说,它简化了Okta对Spring Boot项目的安全使用。对于资源服务器,你通常需要包括Spring Security OAuth Resource Server的依赖,但Okta Spring Boot Starter为你提供了这个。
现在你已经有了基本的应用程序,用以下代码替换DemoApplication.java 。
src/main/java/com/okta/springbootspa/DemoApplication.java
package com.okta.springbootspa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
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.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@RestController
@CrossOrigin
class CaffeineLevelRestController {
String getCaffeineLevel() {
List<String> givenList = Arrays.asList(
"Head on table asleep. Needs coffee now!",
"Not at all. What's wrong?!",
"Mildly. Boring.",
"Making progress.",
"Everything is awesome. Stuff is definitely happening.",
"Eyeballs are rolling around in my head and I'm shouting at my coworker about JHipster.",
"The LD50 of caffeine is 100 cups. Your developer has had 99 and is talking to the bike rack outside while jogging in place."
);
Random rand = new Random();
String caffeineLevelString = givenList.get(rand.nextInt(givenList.size()));
return caffeineLevelString;
}
@GetMapping("/howcaffeinatedami")
public String getCaffeineLevel(Principal principal) {
String userName = principal != null ? principal.getName() : "Anonymous";
return userName + ", your developer's caffeine level is: " + getCaffeineLevel();
}
}
@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.oauth2ResourceServer().jwt();
}
}
}
这个资源服务器的例子定义了一个端点,/howcaffeinatedami ,它返回一个随机的(希望是幽默的)字符串,指定你的开发人员当前的咖啡因水平。
还有一个内部类(在底部),它扩展了WebSecurityConfigurerAdapter ,为应用程序配置Spring Security。该配置类做了三件事:
-
确保所有请求都需要认证。
-
将Spring Boot配置为使用标准的资源服务器安全配置与JSON Web Tokens(JWT);以及
-
启用CORS(跨源资源共享)。
你还需要在application.properties 文件中添加一行。这一行将资源服务器端口从默认的8080 改为8082 。你必须这样做,因为VueJS应用程序将运行在8080 。
src/main/resources/application.properties
server.port=8082
现在你已经有了资源服务器的代码。你仍然需要配置OAuth 2.0和OIDC设置,以使用Okta作为提供者,但首先,我想解释一下CORS(跨源资源共享)。它不仅仅是当你试图在本地开发应用程序时,你的浏览器向你抛出的一个恼人的错误!它也是一个很好的解决方案。如果你已经完全理解了CORS,或者不耐烦继续前进,请随意跳过下一节。
Spring Boot和CORS
CORS是一种协议,允许浏览器和服务器明确地允许跨源资源共享。默认情况下,在浏览器中运行的JavaScript受到同源策略的限制。这意味着,一个Javascript应用程序只能调用驻扎在它被加载的同一域名上的服务器。作为一个起点,从安全的角度来看,这是很有意义的,可以防止很多滥用。然而,如果开发者不能允许例外的话,这个政策将是相当严格的。
CORS允许资源服务器明确地启用跨源请求,告诉浏览器它允许什么类型的请求以及来自什么源头。它是一个白名单方案,即所有的跨源请求都会被浏览器拒绝,除非服务器被明确配置为允许这些请求。
请注意,这个协议是在服务器和浏览器之间进行调解的。客户端应用程序,即本例中的Vue应用程序,实际上不需要对CORS做任何事情。从客户端应用程序的角度来看,对资源服务器的 HTTP 请求要么成功要么失败。如果因为 CORS 而失败,该请求将返回401 (Unauthorized) 。
在这个例子中,资源服务器驻留在http://localhost:8082 ,Vue客户端将从Vue开发服务器加载,地址是http://localhost:8080 。因为端口号是域名的一部分,这两个URL被浏览器认为是不同的来源。当Vue应用试图向Spring Boot资源服务器发出请求时,除非正确配置了CORS,否则浏览器会抛出一个错误。
请注意,不是每个请求都会触发 CORS 检查。某些类型的简单请求是允许的,特别是只允许表单和文本内容类型的请求以及GET,HEAD, 和POST 请求类型。更多信息请看Mozilla开发者文档。然而,任何带有Content-Type 头信息application-json 的请求,如果是向一个跨源域发出的,都会触发 CORS 检查。
当一个合适的请求被发送到不同域的资源时,浏览器要做的第一件事就是发送一个 CORS 预检请求。这实际上是一个完全独立的请求,由浏览器在发送实际请求之前进行。CORS预检请求本质上是询问浏览器是否配置了CORS,是否允许客户端想要发送的请求类型(DELETE、GET、POST等),以及是否允许来自特定原域的请求。预检请求与我们通常所说的认证或授权没有关系。它只是一个快速检查,看看是否配置了 CORS,以及理论上,服务器是否允许基于 HTTP 动词和客户域的请求。
如果预检头没有得到正确处理,CORS将向客户端应用程序返回一个错误。如果预检头不允许请求的类型或不允许来自客户端原点的请求,CORS也将向客户端返回一个错误。实际的、原始的对服务器的请求将永远不会被提出。只有当预检请求被正确处理后,浏览器才会允许从客户端应用程序到资源服务器的请求进行。
下面是一个CORS请求的一般概要:
- JavaScript客户端应用程序向不同领域的资源服务器发出请求
- 浏览器拦截请求,将其标记为需要 CORS 验证,并向资源服务器发送 CORS 预检请求
- 服务器响应预检请求,表示它将允许客户域发出该类型的请求
- 浏览器向服务器发送原始请求
- 服务器处理原始请求,使用OAuth 2.0和OIDC对其进行认证和授权,然后再返回一个回复。
从开发者的角度来看,CORS是需要在服务器上配置的东西。Spring Boot让这一切变得简单。在Java代码中,有两个地方配置了CORS。通常必须为Spring Boot应用程序启用CORS。这是在SecurityConfiguration 类中的configure(HttpSecurity http) 方法中完成的,方法是将cors() 方法添加到http 配置链中。
第二个地方是CaffeineLevelRestController 类上的@CrossOrigin 注解。这告诉Spring Boot为这个控制器的所有端点配置CORS。默认情况下,这将允许来自任何域的跨源请求。为了将请求限制在我们的客户应用域,我们可以使用注解@CrossOrigin(origins = "http://localhost:8080") 。
为Okta Auth配置资源服务器
你应该已经使用Okta CLI注册了一个新账户或登录了一个现有账户。如果没有,请现在就做。在bash shell中,输入okta login ,你应该看到一个类似的信息。Okta Org already configured: https://dev-133337.okta.com/.这是您的Okta基础域。
你对资源服务器唯一需要做的配置是在application.properties 文件中添加okta.oauth2.issuer 属性。Spring Security和Okta Spring Boot Starter将使用发行者端点来发现它所需要的任何其他配置(除了aud 要求,它默认为api://default )。
你的发行者只是你的Okta域加上/oauth2/default ,比如https://dev-133337.okta.com/oauth2/default 。
另一个找到发行者URI的方法是打开你的Okta开发者仪表盘。在左边的菜单中选择安全和API。这将显示你的授权服务器。很可能你只有默认的授权服务器,那是在你注册时为你设置的。这个页面将显示受众(同样,可能是api://default )和发行人URI。

把发行人URI添加到你的属性文件中,它应该看起来像下面这样,用你的实际Okta域替换{yourOktaDomain} 。
src/main/resources/application.properties
server.port=8082
okta.oauth2.issuer=/oauth2/default
你现在有了一个工作的、安全的资源服务器。继续运行该应用程序。
./mvnw spring-boot:run
你应该得到大量的控制台输出,以下面的内容结尾:
2021-09-01 11:49:42.136 INFO 370077 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8082 (http) with context path ''
2021-09-01 11:49:42.143 INFO 370077 --- [ main] com.okta.springbootspa.DemoApplication : Started DemoApplication in 2.021 seconds (JVM running for 2.199)
使用Vue CLI来引导客户端应用程序
下一步是建立前端的客户端应用程序。这个应用程序将使用Vue和Okta签到小工具来实现一个安全的客户端应用程序。Okta签到小工具允许你轻松地在任何应用程序中添加单点登录(SSO)和安全登录表单。
下面关于将Okta签到小工具与Vue整合的说明主要来自Okta网站上的这篇文章,如果你有任何问题,这篇文章是一个很好的参考。
使用Vue CLI创建应用程序。从项目根目录(Spring Boot项目目录上方的目录)运行以下命令。它将创建一个新的目录,okta-app ,它将包含客户端应用程序。
vue create okta-app
选择Manually select features 。
确保Router 和Choose Vue version 被选中。Babel 和Linter / Formatter 也被我自动选中,这很好。
选择版本3.x 。
启用history mode for router (输入y )。
选择ESLint with error prevention only 。
其余选项的默认值都很好:保存时提示,在专用文件中保存配置,以及不将此保存为预设。
导航到项目目录。
cd okta-app
你需要安装几个依赖项。前三个是Okta安全和签到小工具所需要的。最后一个,axios ,是你用来向资源服务器发出HTTP请求的库。
npm install --save \
@okta/okta-signin-widget@5.12.0 \
@okta/okta-vue@5.0.1 \
@okta/okta-auth-js@5.5.0 \
axios@0.22.0
创建一个src/okta/index.js 文件。
import OktaSignIn from '@okta/okta-signin-widget'
import { OktaAuth } from '@okta/okta-auth-js'
const yourOktaUri = '${yourOktaUri}';
const clientId = '${clientId}';
const oktaSignIn = new OktaSignIn({
baseUrl: yourOktaUri,
clientId: clientId,
redirectUri: 'http://localhost:8080/login/callback',
authParams: {
pkce: true,
issuer: `${yourOktaUri}/oauth2/default`,
display: 'page',
scopes: ['openid', 'profile', 'email']
}
});
const oktaAuth = new OktaAuth({
issuer: `${yourOktaUri}/oauth2/default`,
clientId: clientId,
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email']
})
export { oktaAuth, oktaSignIn };
在上面的文件中,你需要用适当的值替换占位符${...} 。不过,在这之前,你需要使用CLI在Okta服务器上创建OIDC应用程序,你将在下面直接完成。
在Okta上创建一个单页的OIDC应用程序
这一次,您将为前端创建一个单页应用程序(而不是您在资源服务器上使用的Web应用程序类型)。在shell中输入以下命令。
okta apps create
按enter ,接受默认的应用程序名称 okta-app 。(这里的应用程序名称是指在Okta服务器上创建的OIDC应用程序)。
对于应用程序的类型,选择2: Single Page App 。
将重定向URI改为http://localhost:8080/login/callback 。
按enter ,接受默认的注销后重定向。
你应该看到一些像下面这样的输出。
Okta application configuration:
Issuer: https://{yourOktaUri}/oauth2/default
Client ID: {clientId}
当然,括号里的值将是你的实际值。
你需要回到src/okta/index.js 文件,用你的值替换文件顶部的yourOktaUri 和clientId 变量。 yourOktaUri 的值是Okta的URI,没有任何进一步的路径指定符。它将看起来像这样。https://dev-133337.okta.com
Okta CLI为你做的一件事是把你的应用程序的基本URL添加到Okta认证服务器的CORS信任来源中。这是必要的,因为Okta签到小工具将进行跨源请求,如上所述,除非CORS得到正确处理,否则这些请求将被阻止。你可以到你的Okta开发者仪表盘上看看,在左侧菜单中选择安全和API,在API标签中选择受信任的来源。你会看到(在这种情况下)http://localhost:8080 被添加为类型为CORS Redirect 的可信来源。

完成Vue前端客户端应用程序
你需要创建的下一个文件是Okta签到小工具的封装器。
创建一个src/components/Login.vue 文件,内容如下。
<template>
<div class="login">
<div id="okta-signin-container"></div>
</div>
</template>
<script>
import '@okta/okta-signin-widget/dist/css/okta-sign-in.min.css'
import {oktaSignIn} from '../okta'
export default {
name: 'Login',
mounted: function () {
this.$nextTick(function () {
oktaSignIn.showSignInAndRedirect(
{ el: '#okta-signin-container' }
)
})
},
unmounted () {
// Remove the widget from the DOM on path change
oktaSignIn.remove()
}
}
</script>
这个文件是Okta关于在Vue中使用Okta签到小工具的文档的逐字记录。该文档是获取更多信息的绝佳资源。
为了使认证工作顺利进行,你需要定义四条路线:
/:一个默认页面来处理应用程序的基本控制。/profile:一个通往当前用户资料的受保护路径。/login:显示签到页面。/login/callback:一个在重定向后解析令牌的路由。
更新src/App.vue ,提供必要的导航链接:
<template>
<div id="app2">
<nav>
<div>
<router-link to="/">
Home
</router-link>
<router-link to="/login" v-if="!authenticated">
Login
</router-link>
<router-link to="/profile" v-if="authenticated" >
Profile
</router-link>
<a v-if="authenticated" v-on:click="logout()">
Logout
</a>
</div>
</nav>
<div id="content">
<router-view/>
</div>
</div>
</template>
<script>
export default {
name: 'app',
data: function () {
return { authenticated: false }
},
async created () {
await this.isAuthenticated()
this.$auth.authStateManager.subscribe(this.isAuthenticated)
},
watch: {
// Everytime the route changes, check for auth status
'$route': 'isAuthenticated'
},
methods: {
async isAuthenticated () {
this.authenticated = await this.$auth.isAuthenticated()
},
async logout () {
await this.$auth.signOut()
}
}
}
</script>
<style>
nav div a { margin-right: 10px }
#app {
width: 800px;
margin: 0 auto;
}
a {
text-decoration: underline;
cursor: pointer;
}
</style>
创建src/components/Home.vue ,以定义一个主页。
<template>
<div id="home">
<h1>Okta Single-Page App Demo</h1>
<div v-if="!this.$root.authenticated">
<p>How much caffeine has your developer had today? <router-link role="button" to="/login">Log in to find out!</router-link></p>
</div>
<div v-if="this.$root.authenticated">
<p>Welcome, {{claims.name}}!</p>
<p>
{{this.caffeineLevel}}
</p>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'home',
data: function () {
return {
claims: '',
caffeineLevel: ''
}
},
created () { this.setup() },
methods: {
async setup () {
if (this.$root.authenticated) {
this.claims = await this.$auth.getUser()
let accessToken = this.$auth.getAccessToken();
console.log(`Authorization: Bearer ${accessToken}`);
try {
let response = await axios.get('http://localhost:8082/howcaffeinatedami',
{ headers: {'Authorization': 'Bearer ' + accessToken } } );
this.caffeineLevel = response.data;
}
catch (error) {
this.caffeineLevel = `${error}`
}
}
}
}
}
</script>
上面的代码做了几件值得指出的事情。它演示了如何检查用户是否经过认证,如何从$auth 对象中获得用户对象,以及如何获得访问令牌以便在向资源服务器发出的请求中使用。
在src/components/Profile.vue 添加一个Profile 组件。这个路由将只对拥有有效访问令牌的用户可见。
<template>
<div id="profile">
<h1>My User Profile (ID Token Claims)</h1>
<p>
Below is the information from your ID token.
</p>
<table>
<thead>
<tr>
<th>Claim</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr v-for="(claim, index) in claims" :key="index">
<td>{{claim.claim}}</td>
<td :id="'claim-' + claim.claim">{{claim.value}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'Profile',
data () {
return {
claims: []
}
},
async created () {
this.claims = await Object.entries(await this.$auth.getUser()).map(entry => ({ claim: entry[0], value: entry[1] }))
}
}
</script>
/login 路由是登录小工具的封装器,如果用户已经登录了,就会重定向。你已经在上面为这个路由创建了组件。
/login/callback 路由是@okta/okta-vue 包的一部分。它处理了很多OAuth 2.0授权代码流的机制,以及管理令牌解析、令牌存储和认证后的重定向。
接下来你需要定义Vue路由器。用以下内容替换src/router/index.js 。你可能会注意到,这段代码需要对两条路由进行认证:主页路由和配置文件路由。
import { createRouter, createWebHistory } from 'vue-router'
import { LoginCallback, navigationGuard } from '@okta/okta-vue'
import HomeComponent from '@/components/Home'
import LoginComponent from '@/components/Login'
import ProfileComponent from '@/components/Profile'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: HomeComponent,
meta: {
requiresAuth: true
}
},
{
path: '/login',
component: LoginComponent
},
{
path: '/login/callback',
component: LoginCallback
},
{
path: '/profile',
component: ProfileComponent,
meta: {
requiresAuth: true
}
}
]
})
router.beforeEach(navigationGuard)
export default router
将src/main.js 中的代码替换为以下内容。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import OktaVue from '@okta/okta-vue'
import { oktaAuth } from './okta';
createApp(App)
.use(router)
.use(OktaVue, {
oktaAuth,
onAuthRequired: () => {
router.push('/login')
},
onAuthResume: () => {
router.push('/login')
},
})
.mount('#app')
现在你可以运行Vue应用程序了。
npm run serve
测试完成的服务器和客户端
确保你的资源服务器仍在运行。这里有一些命令,以防你需要重启服务器(当然,需要从Spring Boot项目的根目录中运行)。
./mvnw spring-boot:run
在你的浏览器中打开应用程序,网址是http://localhost:8080 。
由于该应用程序被设置为保护主页,你将立即被引导到Okta登录界面。使用您的Okta凭证登录。
当您输入凭证时,Okta登录小部件和Vue应用程序将遵循OAuth 2.0授权代码流程。在这个流程中,客户端将登录凭证发送给Okta认证服务器,如果认证成功,会收到一个授权码。客户端应用程序调用Okta令牌端点,用这个授权码换取JWT(JSON网络令牌)。标准的OAuth 2.0授权代码流程要求应用程序将客户秘密与代码一起发送到令牌端点。
然而,在客户端应用程序的背景下,把客户秘密放在公共浏览器的代码中会严重违反安全。相反,Okta使用PKCE(代码交换的证明密钥)。在这个修改后的流程中,客户端生成一个一次性密钥,与请求一起发送,并与授权的JWT关联。这被用来确保只有请求JWT的客户端可以使用它。
注意,它已经从你的认证信息中提取了你的名字和电子邮件。它还向Spring Boot REST服务器发出了一个HTTP请求,使用JWT来确定你的开发者的咖啡因水平(/howcaffeinatedami 端点)。不要担心!虽然上面的图片报告说你的开发人员的咖啡因水平是 "完全没有",但不要担心。我可以向你保证,这是不正确的,只是为了达到温和的幽默效果而做出的随机反应。
你也可以去profile ,在http://localhost:8080/profile ,这将显示认证令牌中的所有要求。
了解更多关于应用安全的信息
在这篇文章中,你看到了如何使用Spring Boot来创建一个简单的资源服务器,以及Vue来创建一个前端客户端。你看到了如何使用Okta来实现一个安全的应用栈。前端客户端使用Okta Sign-In Widget来实现OAuth 2.0授权代码流和PKCE的安全令牌交换。资源服务器也通过使用Okta的Spring Boot Starter来保证安全,这使得在项目中添加JWT auth变得快速而简单。