如何使用Java构建单点登录


在开发应用程序时,通常只有一台资源服务器为多个客户端应用程序提供数据。尽管这些应用程序可能具有相似的用户,但它们可能具有执行所需的不同权限。设想一种情况,其中第一个应用程序的一部分用户应有权访问第二个应用程序(以管理控制台应用程序与客户端或用户应用程序相对应);您将如何执行此操作?

在本文中,我将向您展示如何使用Okta和Spring Boot通过两个客户端应用程序和一个资源服务器来实现单点登录。我还将讨论如何使用访问策略来强制执行身份验证和授权策略,以及如何基于应用程序范围来限制对资源服务器的访问。

在进入代码之前,您需要适当的用户身份验证配置。今天,您将使用Okta作为OAuth 2.0和OpenID Connect(OIDC)提供程序。这将使您能够管理用户和组,并轻松启用诸如社交和多因素日志身份验证之类的选项。

首先,您需要先注册并创建一个免费的Okta开发人员帐户(如果尚未注册)。您会收到一封电子邮件,其中包含有关如何完成帐户设置的说明。完成此操作后,导航回到您的Okta帐户以设置Web应用程序,用户,资源服务器和授权服务器。首次登录时,可能需要单击黄色的管理按钮才能访问开发人员的控制台。

创建两个OpenID Connect应用程序 第一步是创建两个OIDC应用程序。OpenID Connect是建立在OAuth 2.0之上的身份验证协议,它是一种授权协议。每个OIDC应用程序都为每个Web应用程序实例定义一个身份验证提供程序终结点。

在Okta开发人员控制台中,导航到应用程序,然后单击添加应用程序。选择Web,然后单击Next。使用以下值填充字段:

FIELD VALUE
Name OIDC App 1
Base URIs http://localhost:8080
Login redirect URIs http://localhost:8080/login/oauth2/code/okta

单击完成。

向下滚动并记下Client ID和Client Secret。您将很快使用这些值。

使用以下值对第二个应用程序重复这些步骤:

FIELD VALUE
Name OIDC App 2
Base URIs http://localhost:8081
Login redirect URIs http://localhost:8081/login/oauth2/code/okta

单击完成。

你还需要的Client ID,并Client Secret从该OIDC申请为好。

为您的Java应用程序创建测试用户 接下来,您需要创建两个用户。第一个用户只能登录第一个应用程序(OIDC App 1),第二个用户可以登录两个应用程序。

在开发人员控制台中,单击“用户” >“人员”,然后单击“添加人员”。使用下表填写第一个用户的信息表。也使用下表对第二个用户重复此操作。

FIRST USER SECOND USER COMMENTS
First Name Amanda Tanya Can be anything you like
Last Name Tester Tester Can be anything you like
Username amandaTester@mail.com tanyaTester@mail.com Might prefer to test with an email you can actually access
Primary Email amandaTester@mail.com tanyaTester@mail.com
Secondary Email
Groups
Password Set by Admin Set by Admin This is to simplify the demo. In a production environment, you will likely want this set to “set by user”
Password Value Test1234 Test1234 Complexity requirements: - at least 8 characters - a lowercase letter - an uppercase letter - a number - no parts of your username
User must change password unchecked unchecked This is to simplify the demo. In a production environment, you will likely want this checked if you have admin set password.

记下两个用户的用户名和密码(稍后将与他们一起测试应用程序)。

创建用户后,您可以单击用户名,然后单击配置文件,然后单击“编辑”。在此处,为以下字段的每个用户添加一些信息:中间名和昵称。这将使您以后可以从应用程序中查看此信息。

为您的资源服务器创建服务应用程序 现在,您需要为资源服务器创建一个OIDC应用程序。这将配置对REST API的访问。

在Okta开发人员控制台中,导航到应用程序,然后单击添加应用程序。选择服务,然后单击下一步。使用以下值填充字段:

FIELD VALUE
Name OIDC Resource Server

单击完成。

向下滚动并复制Client ID和Client Secret。您将很快使用这些值。

创建授权服务器 Okta的最后一步是创建和配置授权服务器。这使您可以配置自定义声明并设置自定义访问策略。这确定Okta是否在请求令牌时发出令牌,该令牌控制用户访问客户端应用程序和资源服务器的能力。

导航对API >授权服务器。单击添加授权服务器,然后按如下所示填写值:

FIELD VALUE
Name OIDC Auth Server
Audience api://oidcauthserver
Description OIDC Auth Server

单击“完成”,然后单击“声明”选项卡。在“索赔”中,单击“添加索赔”,在下面的字段中填写“索赔1”的值,然后单击“创建”。您可以将以下未提及的任何值保留为默认值。完成后,重复并使用下面的“权利要求2”下的值创建第二个权利要求。

FIELD CLAIM 1 CLAIM 2
Name fullName userEmail
Include in token type Access Token Always Access Token Always
Value user.fullName user.email
Include in -> The following scopes profile email

接下来,您将为第一个应用程序添加访问策略。此应用程序将允许两个用户访问它。单击访问策略选项卡,添加新的访问策略,在这些字段中填写这些值,然后单击创建策略。

FIELD VALUE
Name OIDC App 1
Description OIDC App 1
Assign to The following clients
Assign to clients Start typing: OIDC in the input area below The following clients and click Add to the right of OIDC App 1.

接下来,点击添加规则。设置OIDC App 1的规则名称字段。取消选择除“授权码”之外的所有授权类型,然后单击“创建规则”。

这样可以确保请求必须使用授权代码流才能使Okta创建令牌。这是所有可用OAuth流中最安全的流。它确保通过对POST请求的响应来传递所有敏感信息(如令牌)。

接下来,您将为第二个应用程序添加访问策略。此应用程序将仅允许第二个用户Tanya Tester对其进行访问。在访问策略标签中,添加策略,在这些字段中填写这些值,然后点击创建策略。

FIELD VALUE
Name OIDC App 2
Description OIDC App 2
Assign to The following clients
Assign to clients Start typing: OIDC in the input area below The following clients and click Add to the right of OIDC App 2.

接下来,点击添加规则。设置OIDC App 2的规则名称字段。取消选择除“授权码”之外的所有授权类型。找到“用户”部分,然后选择标记为“已分配应用程序和下列成员之一”的第二个单选按钮:在出现的“用户”框中,开始输入内容Tanya并Tanya Tester从列表中进行选择。这表明只有该用户可以登录该OIDC App 2应用程序。

点击创建规则。

单击设置选项卡,然后复制颁发者URL。您将很快使用此值。

您已经在Okta中完成了所有配置。上代码!

创建OAuth 2.0资源应用 您将使用两个不同的代码库。第一个是资源服务器的代码库,如果客户被授权获取此类信息,它将用于向客户端应用程序提供其他用户信息。

首先下载GitHub存储库中可用的资源服务器代码。

git clone https://github.com/oktadeveloper/okta-java-spring-sso-example.git
cd okta-java-spring-sso-example/oauth2-resource-server

您将需要使用在Okta中创建的“ OIDC资源服务器”应用程序中的值来配置资源应用程序。打开src/main/resources/application.properties文件。

okta.oauth2.issuer={issuerUri}
okta.oauth2.clientId={clientId}
okta.oauth2.clientSecret={clientSecret}
okta.oauth2.audience=api://oidcauthserver
server.port=8082

{clientId}和替换为{clientSecret}您为上面的资源服务器写下的那些。这{issuerUri}是您在上面创建的授权服务器的颁发者URI。转到API和授权服务器,然后在OIDC身份验证服务器旁边的表中查找。

要查看资源服务器的功能,请查看DemoResourceServer该类中的代码。

src/main/java/com/okta/examples/sso/DemoResourceServer.java

package com.okta.examples.sso;

import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DemoResourceServer  {

    public static void main(String[] args) {
        SpringApplication.run(DemoResourceServer.class, args);
    }

    @GetMapping("/welecomeMessage")
    @PreAuthorize("hasAuthority('SCOPE_profile')")
    public String getWelcomeMessage(Principal principal) {
        JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) principal;
        String fullName = jwtAuth.getToken().getClaimAsString("fullName");
        return "Welcome " + fullName + "!";
    }

    @GetMapping("/userEmail")
    @PreAuthorize("hasAuthority('SCOPE_email')")
    public String getUserEmail(Principal principal) {
        JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) principal;
        String email = jwtAuth.getToken().getClaimAsString("userEmail");
        return email;
    }
}

此代码同时设置了Spring Boot应用程序和控制器。该@SpringBootApplication注解告诉它应支持自动配置,组件扫描,和豆注册该应用程序。

@RestController注解告诉系统这个文件是一个REST API控制器,它只是意味着它包含API端点的集合。该@EnableGlobalMethodSecurity注解告诉系统端点可能对方法的水平,这两种方法做安全放。每个get端点都使用@PreAuthorize注释来告知系统,调用应用程序必须具有指定的特定范围才能被授权。例如,如果/userEmail端点在没有email作用域的情况下被调用,它将引发错误。

getWelcomeMessage方法返回一条欢迎消息,其中包含用户的全名。该getUserEmail方法将返回用户的电子邮件。这两个数据都是从您之前在Okta控制台中设置的令牌声明中提取的。

打开一个Shell并使用Maven启动资源服务器的实例。

./mvnw spring-boot:run

现在它将在port上侦听8082。

创建OAuth 2.0客户端应用 您将使用的第二个代码库是两个不同客户端应用程序的代码库。两个客户端应用程序将使用相同的代码,但是将以不同的配置启动。

运行客户端应用程序时,将首先为OIDC App 1运行该应用程序,该应用程序已配置了配置文件范围。

您还将为OIDC App 2运行它,但对于此应用程序,将在配置文件和电子邮件范围都已设置的情况下运行它。

这是这两个应用程序之间的主要配置差异之一。

对于两个客户端应用程序实例,请从oauth2-client示例项目目录中的代码开始。

此Web应用程序非常简单。它导入所需的Okta和Spring依赖关系,然后仅定义可以在给定某些参数的情况下启动的客户端应用程序。该应用程序的完整代码在SingleSignOnApplication类中。

src/main/java/com/okta/examples/sso/SingleSignOnApplication.java

package com.okta.examples.sso;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.ModelAndView;
@Controller
@SpringBootApplication
public class SingleSignOnApplication {

    private WebClient webClient;
    @Value("#{ @environment['resourceServer.url'] }")
    private String resourceServerUrl;

    public static void main(String[] args) {
        SpringApplication.run(SingleSignOnApplication.class, args);

    }

    public SingleSignOnApplication(WebClient webClient) {
        this.webClient = webClient;
    }

    @GetMapping("/")

    public ModelAndView home(@AuthenticationPrincipal OidcUser user) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("user", user.getUserInfo());
        Map<String,String> userBasicProfile = new HashMap<String,String>();
        userBasicProfile.put("First Name",user.getGivenName());
        userBasicProfile.put("Middle Initial",user.getMiddleName());
        userBasicProfile.put("Last Name",user.getFamilyName());
        userBasicProfile.put("Nick Name",user.getNickName());
        String welcomeMessage = this.webClient.get()
                .uri(this.resourceServerUrl + "/welecomeMessage").retrieve()
                .bodyToMono(String.class).block();
        mav.addObject("welcomeMessage",welcomeMessage);

        try {
            String email = this.webClient.get()
                    .uri(this.resourceServerUrl + "/userEmail").retrieve()
                    .bodyToMono(String.class).block();

            if (email != null) {
                userBasicProfile.put("Email", email);
            }

        } catch (Exception e) {

            mav.addObject("emailError", true);
        }

        mav.addObject("profile", userBasicProfile);
        mav.setViewName("home");
        return mav;
    }

    @Configuration
    public static class OktaWebClientConfig {

        @Bean
        WebClient webClient(ClientRegistrationRepository clientRegistrations,
                            OAuth2AuthorizedClientRepository authorizedClients) {
            ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
                   clientRegistrations, authorizedClients);
            oauth2.setDefaultOAuth2AuthorizedClient(true);
            oauth2.setDefaultClientRegistrationId("okta");
            return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
        }
    }
}

该文件同时带有@Controller@SpringBootApplication注释。该@SpringBootApplication注解告诉它应支持自动配置,组件扫描,和豆注册该应用程序。

@Controller注解告诉系统这个文件是一个REST API控制器。在这种情况下,只有一个端点可以处理GET对基本/URL的请求。

该端点调用home方法中的代码,用最简单的术语来说,该方法将建立一堆要显示在页面上的数据,并告诉页面使用哪个模板来显示此数据。

在该home方法内,有两个对资源服务器的调用。首先,它调用资源服务器以获取欢迎消息以显示在页面上。只要应用程序配置了配置文件作用域集(如我之前提到的那样),该消息就会成功返回,它将为客户端应用程序的两个实例都设置。下一个呼叫将获取用户的电子邮件。仅在为应用程序设置了电子邮件范围的情况下,才成功返回电子邮件。

请记住,只有客户端应用程序的第二个实例将设置电子邮件范围,因此对于第一个实例,它将引发错误。这是使用范围确定授权的授权失败的示例。

如果无法检索到电子邮件,则设置一个标志,该标志告诉模板(在home.html文件中配置)显示一条消息,指出该应用程序无权获取用户的电子邮件。

使用Spring Run Profiles配置客户端应用程序 现在,您需要在oauth2-client项目文件夹中配置客户端应用程序的两个不同实例。您需要能够使用两个不同的配置值来运行客户端应用程序的两个不同的实例。为此,您将利用Spring Boot的运行配置文件。这通常用于分隔诸如testanddevproduction,但是没有理由我们不能在这里使用它。

如果查看oauth2-client/src/main/resources,您将看到三个.properties文件。

application.properties对所有三个配置文件都是通用的application-client1.properties具有客户端1application-client2.properties的配置值具有客户端2的配置值

打开oauth2-client/src/main/resources/application.properties并填写您在上面创建的资源服务器的颁发者URI。

要查找Issuer URI(如果您没有记下来的话),请转到API和授权服务器。在看旁边的桌子OIDC Auth服务器下发行URI。

okta.oauth2.issuer={yourIssuerUri}
resourceServer.url=http://localhost:8082

打开oauth2-client/src/main/resources/application-client1.properties并填写第一个OIDC客户端应用程序的客户端ID和客户端密钥。

如果需要再次查找这些值,请从Okta Developer的控制台转到Applications,单击表中的OIDC应用程序名称(OIDC App 1),然后单击“常规”选项卡。客户端ID和客户端密钥在底部。

okta.oauth2.clientId={yourClient1Id}
okta.oauth2.clientSecret={yourClient1Secret}
okta.oauth2.scopes=openid,profile
server.port=8080

打开oauth2-client/src/main/resources/application-client2.properties并填写第二个OIDC客户端应用程序的“客户端ID”和“客户端密钥”。

okta.oauth2.clientId={yourClient2Id}
okta.oauth2.clientSecret={yourClient2Secret}
okta.oauth2.scopes=openid,profile,email
server.port=8081

最后一步是运行此客户端应用程序的两个实例。在两个单独的Shell窗口中运行以下命令。这会为客户端加载每个运行配置文件。

在http:// localhost:8080上运行客户端应用程序1:

./mvnw spring-boot:run -Dspring-boot.run.profiles=client1

在http:// localhost:8081上运行客户端应用2:

./mvnw spring-boot:run -Dspring-boot.run.profiles=client2

那是很多事情的配置,所以让我们快速回顾一下刚刚设置并运行的内容。

  • http:// localhost:8082是您的本地资源服务器
  • http:// localhost:8080是客户端应用程序1(任何经过身份验证的用户都可以访问)
  • http:// localhost:8081是客户端应用程序2(访问策略设置为仅允许Tany Tester访问)

在Okta方面:

  • 您为服务器和两个客户端应用程序创建了匹配的OIDC应用程序。这将为每个应用程序生成唯一的客户端ID和客户端密钥,这使Okta可以对应用程序进行身份验证,并允许您使用Okta对其进行配置。
  • 您还创建了一个自定义授权服务器。这将管理来自应用程序的所有身份验证和授权请求。
  • 在授权服务器中,您创建了两个访问策略,每个客户端应用程序一个。两种访问策略均限制对授权码流的访问。第一个客户端应用程序对任何经过身份验证的用户(通过Okta的单点登录进行身份验证的任何用户)开放。第二个应用程序仅限于用户Tanya Tester。 因此,您创建了一个非常典型的生产场景,其中有一个资源服务器为多个客户端应用程序提供数据,并且您正在使用Okta的仪表板提供单点登录,管理用户以及设置对客户端应用程序和资源服务器的访问策略。 。

知道了?是时候尝试一下了。

测试您的Java单一登录 在接下来的几个步骤中,您将在两个不同的应用程序上登录和注销不同的Okta帐户。使用隐身窗口将避免注销Okta开发人员控制台或单一登录帐户。

打开一个新的隐身浏览器窗口,然后输入URL http://localhost:8080。这是第一个应用程序的URL OIDC App 1。

用tanyaTester@mail.com用户登录。您应该能够成功登录!

1590517616317.png

接下来,您可以将URL更改为http://localhost:8081。这是第二个应用程序的URL OIDC App 2。您会发现您不必再次登录。这是因为您已经登录,OIDC App 1并且是单点登录!

1590517641501.png

如果要关闭浏览器窗口,打开新的隐身浏览器,然后OIDC App 2再次登录,系统将提示您再次登录,因为它将不再具有会话。

测试您的访问策略 您已经看到Tanya Tester可以登录到两个应用程序。接下来,您将看到与amandaTester@mail.com用户一起登录每个应用程序时会发生什么。如果尚未关闭,请关闭用于测试的任何隐身浏览器窗口。

打开一个新的隐身浏览器窗口,然后输入URL http://localhost:8080。用amandaTester@mail.com用户登录。您应该能够成功登录!

1590517766409.png

关闭该浏览器窗口,然后打开一个新的隐身浏览器窗口,然后输入URL http://localhost:8081

amandaTester@mail.com用户登录。您将收到“访问被拒绝”错误。

1590517744922.png

出现此错误的原因是,您设置了访问策略,因此只能Tanya Tester登录OIDC App 2。

测试您的范围授权 最后,您将测试资源服务器如何处理每个应用程序的授权。

打开一个新的隐身浏览器窗口,然后输入URL http://localhost:8080。用tanyaTester@mail.com用户登录。您应该能够成功登录。

请注意,您将在顶部看到一条消息,其中包含特定于用户的欢迎消息。这是因为应用程序正在使用配置文件作用域,因此被允许访问欢迎消息端点。在其下,您将看到一条消息,指出该应用程序无权访问电子邮件信息。这是因为该应用程序实例未与电子邮件范围一起运行。

1590517766409.png

接下来,您可以将URL更改为http://localhost:8081。请记住,这是第二个应用程序的URL OIDC App 2。当您转到此页面时,您会注意到您没有看到有关无法访问该电子邮件的消息。相反,您会在配置文件信息中看到该电子邮件。

1590517795045.png


原文链接:http://codingdict.com