使用Spring Boot和OAuth构建安全的SPA
最近一段时间都在闭关学习,过程还是有点艰辛的,幸运的是还有优锐课老师带着,少走了很多弯路。很久也没有更新文章了,这篇想和大家分享的是,了解如何在使用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。
用以下值填写表格:
- Name: OAuth Implicit Tutorial
- Base URIs: http://localhost:8080/
- Login redirect URIs: http://localhost:8080/
将其他所有内容保留为默认值,然后单击“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类相同的包中,创建一个新的SignInWidgetConfigController
class类:
@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的更多相关文章
- Spring Boot——2分钟构建spring web mvc REST风格HelloWorld
之前有一篇<5分钟构建spring web mvc REST风格HelloWorld>介绍了普通方式开发spring web mvc web service.接下来看看使用spring b ...
- [转]Spring Boot——2分钟构建spring web mvc REST风格HelloWorld
Spring Boot——2分钟构建spring web mvc REST风格HelloWorld http://projects.spring.io/spring-boot/ http://spri ...
- Spring boot学习1 构建微服务:Spring boot 入门篇
Spring boot学习1 构建微服务:Spring boot 入门篇 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框 ...
- spring boot 入门一 构建spring boot 工程
最近在学习Spring boot,所以想通过博客的形式和大家分享学习的过程,同时也为了更好的学习技术,下面直接进入Spring boot的世界. 简介 spring boot 它的设计目的就是为例简化 ...
- Spring boot Mybatis整合构建Rest服务(超细版)
Springboot+ Mybatis+MySql整合构建Rest服务(涵盖增.删.改.查) 1.概要 1.1 为什么要使用Spring boot? 1.1.1 简单方便.配置少.整合了大多数框架 ...
- Spring Boot 集成 Swagger 构建接口文档
在应用开发过程中经常需要对其他应用或者客户端提供 RESTful API 接口,尤其是在版本快速迭代的开发过程中,修改接口的同时还需要同步修改对应的接口文档,这使我们总是做着重复的工作,并且如果忘记修 ...
- jenkins集成spring boot持续化构建代码
我个人使用的是阿里云的云服务器,项目采用的是spring boot为框架,现在要做的功能就是将本地开发的代码提交到github中,通过jenkins自动化集成部署到云服务器.接下来开始步骤. 1 首先 ...
- 使用spring boot+mybatis+mysql 构建RESTful Service
开发目标 开发两个RESTful Service Method Url Description GET /article/findAll POST /article/insert 主要使用到的技术 j ...
- Spring Boot & Restful API 构建实战!
作者:liuxiaopeng https://www.cnblogs.com/paddix/p/8215245.html 在现在的开发流程中,为了最大程度实现前后端的分离,通常后端接口只提供数据接口, ...
随机推荐
- Java compare方法和compareTo方法
Java Comparator接口排序用法,详细介绍可以阅读这个链接的内容:https://www.cnblogs.com/shizhijie/p/7657049.html 对于 public int ...
- vs2012(或2013)与虚拟机连调试
一.安装Windows Driver Kit 8 1首先在计算机上安装VS2012 (12很容易安装,安装步骤略),然后到官网上下载Windows Driver Kit 8 下载地址: http:// ...
- oracle(3)select语句中常用的关键字说明
1.select 查询表中的数据 select * from stu: ---查询stu表所有的数据,*代表所有2.dual ,伪表,要查询的数据不存在任何表中时使用 select sysdate f ...
- PHP的操作符与控制结构
一.操作符 操作符是用来对数组和变量进行某种操作运算的符号. 算术操作符 操作符 名称 示例 + 加 $a+$b - 减 $a-$b * 乘 $a*$b / 除 $a/$b % 取余 $a%$b 复 ...
- JSP页面获取其他页面传递的参数
jstl表达式获取方式: ${param.pid} el表达式获取方式: ${requestScope.attr} el表达式获取方式: ${attr} ---------------------- ...
- 覆盖.project
<?xml version="1.0" encoding="UTF-8"?> <projectDescription> <name ...
- idea xml文件去掉背景黄色
编写dao中的sql时,xml文件中背景一大片黄色,看着不舒服,如何去掉了? 1. File -> Settings... 2. 去消以下两项勾选 (Inspections -- 如 ...
- Vue组件template模板字符串几种写法
在定义Vue组件时,组件的模板template选项需要的是一个字符串,当其内容较复杂需要换行时,需要简单处理一下,具体有五种方式: 方式一:使用 \ 转义换行符 <!DOCTYPE html&g ...
- 程序员用 Python 扒出 B 站那些“惊为天人”的UP主!
前言 ! 近期B站的跨年晚会因其独特的创意席卷各大视频网站,给公司带来了极大的正面影响,股价也同时大涨,想必大家都在后悔没有早点买B站的股票: 然而今天我们要讨论的不是B站的跨年晚会,而是B站 ...
- 对比Node.js和Python 帮你确定理想编程解决方案!
世上没有最好的编程语言.有些编程语言比其他编程语言用于更具体的事情.比如,你可能需要移动应用程序,网络应用程序或更专业化的系统,则可能会有特定的语言.但是我们暂时假设你需要的是一个相对来说比较简单的网 ...