最近一段时间都在闭关学习,过程还是有点艰辛的,幸运的是还有优锐课老师带着,少走了很多弯路。很久也没有更新文章了,这篇想和大家分享的是,了解如何在使用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应用程序。
1 curl https://start.spring.io/starter.tgz \
2 -d artifactId=oauth-implicit-example \
3 -d dependencies=security,web \
4 -d language=java \
5 -d type=maven-project \
6 -d baseDir=oauth-implicit-example \
7 | tar -xzvf -
如果要从浏览器下载项目,请转到:start.spring.io搜索并选择“security”依存关系,然后单击绿色的“Generate Project”按钮。
解压缩项目后,应该可以在命令行中启动它:./mvnw spring-boot:run
。该应用程序尚无法执行任何操作,但这是一项“so far so good”的检查。用^C
终止该过程,让我们开始实际编写代码!
写一些代码!
好吧,差不多。首先,将Spring OAuth 2.0依赖项添加到pom.xml
中
1 <dependency>
2 <groupId>org.springframework.security.oauth</groupId>
3 <artifactId>spring-security-oauth2</artifactId>
4 <version>2.2.0.RELEASE</version>
5 </dependency>
打开DemoApplication.java
,如果你遵循(and you are right?),则应位于src/main/java/com/example/oauthimplicitexample
中。不难发现,该项目仅包含两个Java类,其中一个是测试。
用@EnableResourceServer
注释该类,这将告诉Spring Security添加必要的过滤器和逻辑来处理OAuth隐式请求。
接下来,添加一个控制器:
1 @RestController
2 public class MessageOfTheDayController {
3 @GetMapping("/mod")
4 public String getMessageOfTheDay(Principal principal) {
5 return "The message of the day is boring for user: " + principal.getName();
6 }
7 }
这就对了!基本上是Hello World。使用./mvnw spring-boot:run
启动你的应用程序备份。你应该可以访问http://localhost:8080/mod:
1 curl -v http://localhost:8080/mod
2 HTTP/1.1 401
3 Content-Type: application/json;charset=UTF-8
4 WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"
5 {
6 "error": "unauthorized",
7 "error_description": "Full authentication is required to access this resource"
8 }
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
1 security:
2 oauth2:
3 resource:
4 userInfoUri: https://dev-123456.oktapreview.com/oauth2/default/v1/userinfo
此时,你可以启动应用程序并开始验证访问令牌!但是,当然,你将需要访问令牌来进行验证…
创建登录页面
为方便起见,你将重用现有的Spring Boot应用程序来托管SPA。通常,这些资产可以托管在其他地方:另一个应用程序,CDN等。出于本教程的目的,将一个孤独的index.html文件托管在另一个应用程序中似乎有点过头了。
创建一个新文件src/main/resources/static/index.html
并用以下内容填充它:
1 <!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6 <meta name="description" content="">
7 <meta name="author" content="">
8 <title>Okta Implicit Spring-Boot</title>
9 <base href="/">
10 <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>
11 <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">
12 <link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.3.0/css/okta-theme.css" type="text/css" rel="stylesheet">
13 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
14 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
15 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
16 </head>
17 <body>
18 <!-- Render the login widget here -->
19 <div id="okta-login-container"></div>
20 <!-- Render the REST response here -->
21 <div id="cool-stuff-here"></div>
22 <!-- And a logout button, hidden by default -->
23 <button id="logout" type="button" class="btn btn-danger" style="display:none">Logout</button>
24 <script>
25 $.ajax({
26 url: "/sign-in-widget-config",
27 }).then(function(data) {
28 // we are priming our config object with data retrieved from the server in order to make this example easier to run
29 // You could statically define your config like if you wanted too:
30 /*
31 const data = {
32 baseUrl: 'https://dev-123456.oktapreview.com',
33 clientId: '00icu81200icu812w0h7',
34 redirectUri: 'http://localhost:8080',
35 authParams: {
36 issuer: 'https://dev-123456.oktapreview.com/oauth2/default',
37 responseType: ['id_token', 'token']
38 }
39 }; */
40 // we want the access token so include 'token'
41 data.authParams.responseType = ['id_token', 'token'];
42 data.authParams.scopes = ['openid', 'email', 'profile'];
43 data.redirectUri = window.location.href; // simple single page app
44 // setup the widget
45 window.oktaSignIn = new OktaSignIn(data);
46 // handle the rest of the page
47 doInit();
48 });
49 /**
50 * Makes a request to a REST resource and displays a simple message to the page.
51 * @param accessToken The access token used for the auth header
52 */
53 function doAllTheThings(accessToken) {
54 // include the Bearer token in the request
55 $.ajax({
56 url: "/mod",
57 headers: {
58 'Authorization': "Bearer " + accessToken
59 },
60 }).then(function(data) {
61 // Render the message of the day
62 $('#cool-stuff-here').append("<strong>Message of the Day:</strong> "+ data);
63 })
64 .fail(function(data) {
65 // handle any errors
66 console.error("ERROR!!");
67 console.log(data.responseJSON.error);
68 console.log(data.responseJSON.error_description);
69 });
70 // show the logout button
71 $( "#logout" )[0].style.display = 'block';
72 }
73 function doInit() {
74 $( "#logout" ).click(function() {
75 oktaSignIn.signOut(() => {
76 oktaSignIn.tokenManager.clear();
77 location.reload();
78 });
79 });
80 // Check if we already have an access token
81 const token = oktaSignIn.tokenManager.get('my_access_token');
82 // if we do great, just go with it!
83 if (token) {
84 doAllTheThings(token.accessToken)
85 } else {
86 // otherwise show the login widget
87 oktaSignIn.renderEl(
88 {el: '#okta-login-container'},
89 function (response) {
90 // check if success
91 if (response.status === 'SUCCESS') {
92 // for our example we have the id token and the access token
93 oktaSignIn.tokenManager.add('my_id_token', response[0]);
94 oktaSignIn.tokenManager.add('my_access_token', response[1]);
95 // hide the widget
96 oktaSignIn.hide();
97 // now for the fun part!
98 doAllTheThings(response[1].accessToken);
99 }
100 },
101 function (err) {
102 // handle any errors
103 console.log(err);
104 }
105 );
106 }
107 }
108 </script>
109 </body>
110 </html>
此页面执行以下操作:
- 显示Okta登录小部件并获取访问令牌
- 调用
/sign-in-widget-config
控制器来配置所述小部件(我们假设此文件由其他服务提供) - 用户登录后,页面将调用
/mod
控制器(带有访问令牌)并显示结果
为了支持我们的HTML,我们需要为/sign-in-widget-config
端点创建一个新的Controller
。
在与Spring Boot Application类相同的包中,创建一个新的SignInWidgetConfigController
class类:
1 @RestController
2 public class SignInWidgetConfigController {
3 private final String issuerUrl;
4 private final String clientId;
5 public SignInWidgetConfigController(@Value("#{@environment['okta.oauth2.clientId']}") String clientId,
6 @Value("#{@environment['okta.oauth2.issuer']}") String issuerUrl) {
7 Assert.notNull(clientId, "Property 'okta.oauth2.clientId' is required.");
8 Assert.notNull(issuerUrl, "Property 'okta.oauth2.issuer' is required.");
9 this.clientId = clientId;
10 this.issuerUrl = issuerUrl;
11 }
12 @GetMapping("/sign-in-widget-config")
13 public WidgetConfig getWidgetConfig() {
14 return new WidgetConfig(issuerUrl, clientId);
15 }
16 public static class WidgetConfig {
17 public String baseUrl;
18 public String clientId;
19 public Map<String, Object> authParams = new LinkedHashMap<>();
20 WidgetConfig(String issuer, String clientId) {
21 this.clientId = clientId;
22 this.authParams.put("issuer", issuer);
23 this.baseUrl = issuer.replaceAll("/oauth2/.*", "");
24 }
25 }
26 }
将相应的配置添加到application.yml
文件:
1 okta:
2 oauth2:
3 # Client ID from above step
4 clientId: 00ICU81200ICU812
5 issuer: https://dev-123456.oktapreview.com/oauth2/default
最后一件事是允许公众访问index.html
页面和 /sign-in-widget-config
在你的应用程序中定义ResourceServerConfigurerAdapter
,以允许访问这些资源。
1 @Bean
2 protected ResourceServerConfigurerAdapter resourceServerConfigurerAdapter() {
3 return new ResourceServerConfigurerAdapter() {
4 @Override
5 public void configure(HttpSecurity http) throws Exception {
6 http.authorizeRequests()
7 .antMatchers("/", "/index.html", "/sign-in-widget-config").permitAll()
8 .anyRequest().authenticated();
9 }
10 };
11 }
动起来!
使用./mvnw spring-boot:run
再次启动你的应用程序,然后浏览至 http://localhost:8080/。你应该可以使用新的Okta帐户登录并查看当天的消息。
尝试Okta Spring Boot Starter
到现在为止(登录页面除外),你一直在使用Spring Security OAuth 2.0的即用型支持。这样做是因为:标准!这种方法存在一些问题:
- 对我们应用程序的每个请求都需要不必要的往返回OAuth IdP
- 我们不知道创建访问令牌时使用了哪些范围
- 在这种情况下,用户的组/角色不可用
这些可能对你的应用程序来说可能不是问题,但是解决它们就像向你的POM文件添加另一个依赖项一样简单:
1 <dependency>
2 <groupId>com.okta.spring</groupId>
3 <artifactId>okta-spring-boot-starter</artifactId>
4 <version>0.2.0</version>
5 </dependency>
如果需要,甚至可以缩减application.yml
文件,其中任何security.*
属性将优先于 okta.*
属性:
1 okta:
2 oauth2:
3 clientId: 00ICU81200ICU812
4 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应用程序即可。
1 @EnableGlobalMethodSecurity(prePostEnabled = true)
2 protected static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
3 @Override
4 protected MethodSecurityExpressionHandler createExpressionHandler() {
5 return new OAuth2MethodSecurityExpressionHandler();
6 }
7 }
最后,使用@PreAuthorize
更新MessageOfTheDayController
(在这种情况下,你允许“所有人”或“电子邮件”范围内的任何人)。
1 @RestController
2 public class MessageOfTheDayController {
3 @GetMapping("/mod")
4 @PreAuthorize("hasAuthority('Everyone') || #oauth2.hasScope('email')")
5 public String getMessageOfTheDay(Principal principal) {
6 return "The message of the day is boring for user: " + principal.getName();
7 }
8 }