最近一段时间都在闭关学习,过程还是有点艰辛的,幸运的是还有优锐课老师带着,少走了很多弯路。很久也没有更新文章了,这篇想和大家分享的是,了解如何在使用Spring Boot入门程序的同时使用Spring Boot和OAuth构建安全的SPA,以获得对验证和权限映射的其他支持。

即使是最基本的JavaScript单页应用程序(SPA),也很可能需要安全地从源应用程序访问资源,并且如果你是像我这样的Java开发人员,则可能是Spring Boot应用程序,并且你可能想使用OAuth 2.0隐式流。通过此流程,你的客户端将在每个请求中发送一个承载令牌,并且你的服务器端应用程序将使用身份提供者(IdP)验证该令牌。

在本教程中,你将通过构建两个演示这些原理的小型应用程序来了解有关隐式流程的更多信息:一个带有一点JQuery的简单SPA客户端应用程序以及一个带有Spring Boot的后端服务。你将通过使用标准的Spring OAuth位开始,然后切换到Okta Spring Boot Starter并检查其添加的功能。前几节将与供应商无关,但是由于我并非一无所知,因此我将向你展示如何使用Okta作为你的IdP。

创建一个Spring Boot应用程序

如果你还没有尝试过start.spring.io,请立即单击以进行检查……单击几次,它将为你提供一个基本的,可运行的Spring Boot应用程序。

 curl https://start.spring.io/starter.tgz \
-d artifactId=oauth-implicit-example \
-d dependencies=security,web \
-d language=java \
-d type=maven-project \
-d baseDir=oauth-implicit-example \
| tar -xzvf -

如果要从浏览器下载项目,请转到:start.spring.io搜索并选择“security”依存关系,然后单击绿色的“Generate Project”按钮。

解压缩项目后,应该可以在命令行中启动它:./mvnw spring-boot:run。该应用程序尚无法执行任何操作,但这是一项“so far so good”的检查。用^C终止该过程,让我们开始实际编写代码!

写一些代码!

好吧,差不多。首先,将Spring OAuth 2.0依赖项添加到pom.xml

 <dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>

打开DemoApplication.java,如果你遵循(and you are right?),则应位于src/main/java/com/example/oauthimplicitexample中。不难发现,该项目仅包含两个Java类,其中一个是测试。

@EnableResourceServer注释该类,这将告诉Spring Security添加必要的过滤器和逻辑来处理OAuth隐式请求。

接下来,添加一个控制器:

 @RestController
public class MessageOfTheDayController {
@GetMapping("/mod")
public String getMessageOfTheDay(Principal principal) {
return "The message of the day is boring for user: " + principal.getName();
}
}

这就对了!基本上是Hello World。使用./mvnw spring-boot:run启动你的应用程序备份。你应该可以访问http://localhost:8080/mod

 curl -v http://localhost:8080/mod
HTTP/1.1 401
Content-Type: application/json;charset=UTF-8
WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}

401? 是的,默认情况下是安全的!另外,我们实际上并未提供OAuth IdP的任何配置详细信息。使用^C停止服务器,然后移至下一部分。

准备好你的OAuth信息

如上所述,你将继续使用Okta。你可以在https://developer.okta.com/上注册一个免费(永久)帐户。只需单击“注册”按钮并填写表格。完成此操作后,你将获得两件事,即Okta基本URL,看起来像:dev-123456.oktapreview.com和一封有关如何激活帐户的说明的电子邮件。

激活你的帐户,当你仍然在Okta开发人员控制台中时,最后一步是:创建Okta SPA应用程序。在顶部菜单栏上,单击“Applications”,然后单击“Add Application”。选择SPA,然后单击Next

用以下值填写表格:

将其他所有内容保留为默认值,然后单击“Done”。下一页的底部是你的客户ID,你将在下一步中使用该ID。

为Spring配置OAuth

生成的示例应用程序使用application.properties文件。我更喜欢YAML,因此我将文件重命名为application.yml

应用程序资源服务器仅需要知道如何验证访问令牌。由于OAuth 2.0或OIDC规范未定义访问令牌的格式,因此可以远程验证令牌。The generated sample application uses

 security:
oauth2:
resource:
userInfoUri: https://dev-123456.oktapreview.com/oauth2/default/v1/userinfo

此时,你可以启动应用程序并开始验证访问令牌!但是,当然,你将需要访问令牌来进行验证…

创建登录页面

为方便起见,你将重用现有的Spring Boot应用程序来托管SPA。通常,这些资产可以托管在其他地方:另一个应用程序,CDN等。出于本教程的目的,将一个孤独的index.html文件托管在另一个应用程序中似乎有点过头了。

创建一个新文件src/main/resources/static/index.html并用以下内容填充它:

 <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Okta Implicit Spring-Boot</title>
<base href="/">
<script src="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.3.0/js/okta-sign-in.min.js" type="text/javascript"></script>
<link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.3.0/css/okta-sign-in.min.css" type="text/css" rel="stylesheet">
<link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.3.0/css/okta-theme.css" type="text/css" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<!-- Render the login widget here -->
<div id="okta-login-container"></div>
<!-- Render the REST response here -->
<div id="cool-stuff-here"></div>
<!-- And a logout button, hidden by default -->
<button id="logout" type="button" class="btn btn-danger" style="display:none">Logout</button>
<script>
$.ajax({
url: "/sign-in-widget-config",
}).then(function(data) {
// we are priming our config object with data retrieved from the server in order to make this example easier to run
// You could statically define your config like if you wanted too:
/*
const data = {
baseUrl: 'https://dev-123456.oktapreview.com',
clientId: '00icu81200icu812w0h7',
redirectUri: 'http://localhost:8080',
authParams: {
issuer: 'https://dev-123456.oktapreview.com/oauth2/default',
responseType: ['id_token', 'token']
}
}; */
// we want the access token so include 'token'
data.authParams.responseType = ['id_token', 'token'];
data.authParams.scopes = ['openid', 'email', 'profile'];
data.redirectUri = window.location.href; // simple single page app
// setup the widget
window.oktaSignIn = new OktaSignIn(data);
// handle the rest of the page
doInit();
});
/**
* Makes a request to a REST resource and displays a simple message to the page.
* @param accessToken The access token used for the auth header
*/
function doAllTheThings(accessToken) {
// include the Bearer token in the request
$.ajax({
url: "/mod",
headers: {
'Authorization': "Bearer " + accessToken
},
}).then(function(data) {
// Render the message of the day
$('#cool-stuff-here').append("<strong>Message of the Day:</strong> "+ data);
})
.fail(function(data) {
// handle any errors
console.error("ERROR!!");
console.log(data.responseJSON.error);
console.log(data.responseJSON.error_description);
});
// show the logout button
$( "#logout" )[0].style.display = 'block';
}
function doInit() {
$( "#logout" ).click(function() {
oktaSignIn.signOut(() => {
oktaSignIn.tokenManager.clear();
location.reload();
});
});
// Check if we already have an access token
const token = oktaSignIn.tokenManager.get('my_access_token');
// if we do great, just go with it!
if (token) {
doAllTheThings(token.accessToken)
} else {
// otherwise show the login widget
oktaSignIn.renderEl(
{el: '#okta-login-container'},
function (response) {
// check if success
if (response.status === 'SUCCESS') {
// for our example we have the id token and the access token
oktaSignIn.tokenManager.add('my_id_token', response[0]);
oktaSignIn.tokenManager.add('my_access_token', response[1]);
// hide the widget
oktaSignIn.hide();
// now for the fun part!
doAllTheThings(response[1].accessToken);
}
},
function (err) {
// handle any errors
console.log(err);
}
);
}
}
</script>
</body>
</html>

此页面执行以下操作:

  • 显示Okta登录小部件并获取访问令牌
  • 调用/sign-in-widget-config控制器来配置所述小部件(我们假设此文件由其他服务提供)
  • 用户登录后,页面将调用/mod控制器(带有访问令牌)并显示结果

为了支持我们的HTML,我们需要为/sign-in-widget-config端点创建一个新的Controller

在与Spring Boot Application类相同的包中,创建一个新的SignInWidgetConfigControllerclass类:

 @RestController
public class SignInWidgetConfigController {
private final String issuerUrl;
private final String clientId;
public SignInWidgetConfigController(@Value("#{@environment['okta.oauth2.clientId']}") String clientId,
@Value("#{@environment['okta.oauth2.issuer']}") String issuerUrl) {
Assert.notNull(clientId, "Property 'okta.oauth2.clientId' is required.");
Assert.notNull(issuerUrl, "Property 'okta.oauth2.issuer' is required.");
this.clientId = clientId;
this.issuerUrl = issuerUrl;
}
@GetMapping("/sign-in-widget-config")
public WidgetConfig getWidgetConfig() {
return new WidgetConfig(issuerUrl, clientId);
}
public static class WidgetConfig {
public String baseUrl;
public String clientId;
public Map<String, Object> authParams = new LinkedHashMap<>();
WidgetConfig(String issuer, String clientId) {
this.clientId = clientId;
this.authParams.put("issuer", issuer);
this.baseUrl = issuer.replaceAll("/oauth2/.*", "");
}
}
}

将相应的配置添加到application.yml文件:

 okta:
oauth2:
# Client ID from above step
clientId: 00ICU81200ICU812
issuer: https://dev-123456.oktapreview.com/oauth2/default

最后一件事是允许公众访问index.html页面和 /sign-in-widget-config

在你的应用程序中定义ResourceServerConfigurerAdapter,以允许访问这些资源。

 @Bean
protected ResourceServerConfigurerAdapter resourceServerConfigurerAdapter() {
return new ResourceServerConfigurerAdapter() {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/index.html", "/sign-in-widget-config").permitAll()
.anyRequest().authenticated();
}
};
}

动起来!

使用./mvnw spring-boot:run再次启动你的应用程序,然后浏览至 http://localhost:8080/。你应该可以使用新的Okta帐户登录并查看当天的消息。

尝试Okta Spring Boot Starter

到现在为止(登录页面除外),你一直在使用Spring Security OAuth 2.0的即用型支持。这样做是因为:标准!这种方法存在一些问题:

  • 对我们应用程序的每个请求都需要不必要的往返回OAuth IdP
  • 我们不知道创建访问令牌时使用了哪些范围
  • 在这种情况下,用户的组/角色不可用

这些可能对你的应用程序来说可能不是问题,但是解决它们就像向你的POM文件添加另一个依赖项一样简单:

 <dependency>
<groupId>com.okta.spring</groupId>
<artifactId>okta-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>

如果需要,甚至可以缩减application.yml文件,其中任何security.* 属性将优先于 okta.* 属性:

 okta:
oauth2:
clientId: 00ICU81200ICU812
issuer: https://dev-123456.oktapreview.com/oauth2/default

重新启动你的应用程序,已经解决了前两个问题!

最后一个需要额外的步骤,你将不得不向Okta的访问令牌添加额外的数据:

回到Okta Developer Console,在菜单栏上单击API > Authorization Server。在此示例中,我们一直在使用“default”授权服务器,因此请单击“edit”,然后选择“Claims”标签。点击“Add Claim”,然后使用以下值填写表单:

  • Name: groups
  • Include in token type: Access Token
  • Value type: Groups
  • Filter: Regex - .*

将其余的保留为默认设置,然后点击“Create”。

okta-spring-boot-starter会自动将组声明中的值映射到Spring Security Authority。以标准的Spring Security方式,我们可以注释我们的方法来配置访问级别。

要启用@PreAuthorize批注,你需要将@EnableGlobalMethodSecurity添加到Spring Boot Application。如果还要验证OAuth范围,则需要添加OAuth2MethodSecurityExpressionHandler。只需将以下代码片段放入你的Spring Boot应用程序即可。

 @EnableGlobalMethodSecurity(prePostEnabled = true)
protected static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}

最后,使用@PreAuthorize更新MessageOfTheDayController(在这种情况下,你允许“所有人”或“电子邮件”范围内的任何人)。

 @RestController
public class MessageOfTheDayController {
@GetMapping("/mod")
@PreAuthorize("hasAuthority('Everyone') || #oauth2.hasScope('email')")
public String getMessageOfTheDay(Principal principal) {
return "The message of the day is boring for user: " + principal.getName();
}
}

感谢阅读~

使用Spring Boot和OAuth构建安全的SPA的更多相关文章

  1. Spring Boot——2分钟构建spring web mvc REST风格HelloWorld

    之前有一篇<5分钟构建spring web mvc REST风格HelloWorld>介绍了普通方式开发spring web mvc web service.接下来看看使用spring b ...

  2. [转]Spring Boot——2分钟构建spring web mvc REST风格HelloWorld

    Spring Boot——2分钟构建spring web mvc REST风格HelloWorld http://projects.spring.io/spring-boot/ http://spri ...

  3. Spring boot学习1 构建微服务:Spring boot 入门篇

    Spring boot学习1 构建微服务:Spring boot 入门篇 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框 ...

  4. spring boot 入门一 构建spring boot 工程

    最近在学习Spring boot,所以想通过博客的形式和大家分享学习的过程,同时也为了更好的学习技术,下面直接进入Spring boot的世界. 简介 spring boot 它的设计目的就是为例简化 ...

  5. Spring boot Mybatis整合构建Rest服务(超细版)

     Springboot+ Mybatis+MySql整合构建Rest服务(涵盖增.删.改.查) 1.概要 1.1 为什么要使用Spring  boot? 1.1.1 简单方便.配置少.整合了大多数框架 ...

  6. Spring Boot 集成 Swagger 构建接口文档

    在应用开发过程中经常需要对其他应用或者客户端提供 RESTful API 接口,尤其是在版本快速迭代的开发过程中,修改接口的同时还需要同步修改对应的接口文档,这使我们总是做着重复的工作,并且如果忘记修 ...

  7. jenkins集成spring boot持续化构建代码

    我个人使用的是阿里云的云服务器,项目采用的是spring boot为框架,现在要做的功能就是将本地开发的代码提交到github中,通过jenkins自动化集成部署到云服务器.接下来开始步骤. 1 首先 ...

  8. 使用spring boot+mybatis+mysql 构建RESTful Service

    开发目标 开发两个RESTful Service Method Url Description GET /article/findAll POST /article/insert 主要使用到的技术 j ...

  9. Spring Boot & Restful API 构建实战!

    作者:liuxiaopeng https://www.cnblogs.com/paddix/p/8215245.html 在现在的开发流程中,为了最大程度实现前后端的分离,通常后端接口只提供数据接口, ...

随机推荐

  1. 对 TD tree 的使用体验

    经过这几天对学长们的作品的应用,感触颇多,忍不住写写随笔. 先谈一下,最初的感受吧,那天下午观看,体验学长学姐们的作品时,感觉他们太厉害了,只比我们多学一年,就已经可以做出手机 app ,和 网页了. ...

  2. Dynamics CRM - 解决无法使用 Ribbon Workbench 2016 定制 Sub-Grid View Button 的问题(SubGrid MainTab 消失之谜)

    发现问题: 在 Dynamics CRM 开发中,会经常使用 Ribbon Workbench 工具来定制 Button 或者对已有 Button 进行自定义功能开发,比如隐藏 SubGrid 的 A ...

  3. c语言寒假大作战

    一.表格 问题 回答 这个作业属于那个课程 2019级计科一班 这个作业要求在哪里 寒假大作战01 这个作业的目标是 gitee注册.登录.上传文件.克隆仓库与 git基础命令学习与使用 作业正文 作 ...

  4. 干货分享:Essay写作收集论据的三个方法

    在很多时候,中国留学生写出的Essay在西方学术界看来是存在plagiarism的情况.并不是说咱们写的所有东西都是抄袭,而是思维逻辑和利用证据的方式与西方权威的academic writing不同. ...

  5. jobs|ps|杀死nohup

    方法1:如果没有退出客户端界面,可以先通过 “jobs” 命令查看程序是否在运行,此时只有序号没有PID号:输入命令 “jobs -l” 会显示程序的PID号,然后通过 “kill -9 PID”杀死 ...

  6. URL&HTTP协议详解

    本文来自公开课笔记,主要做知识的记录,谢谢! ·接口测试核心技术--URL&HTTP协议详解. ·URL: 统一资源定位符. 示例: https://ke.qq.com/course/3157 ...

  7. C++逐行读取txt

    C++读取txt文件的时候可以使用std::ifstream来实现,如果打开文件失败的话,其变量会是空的,所以可以用来判断是否打开成功.  #include <stdlib.h>  #in ...

  8. DRF项目之JWT认证方式的简介及使用

    什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点 ...

  9. kali由wifi握手包破解密码&&gnuplot使用

    1.kali密码破解(WiFi握手包) cap包密码破解,aircrack-ng wifi.cap -w psw.txt(你的字典文件) 2.画图工具gnuplot 1.txt中保存的是坐标,形式为: ...

  10. elasticsearch + springboot 整合

    https://blog.csdn.net/chengyuqiang/article/details/102938266 https://blog.csdn.net/chengyuqiang/arti ...