本指南介绍如何使用“社交登录”构建示例应用,以执行各种操作OAuth 2.0和弹簧启动.
它从简单的单提供程序单一登录开始,一直到具有身份验证提供程序选择的客户端:GitHub或谷歌.
【资料图】
这些示例都是在后端使用 Spring Boot 和 Spring Security 的单页应用程序。他们也都使用普通jQuery在前端。但是,转换为不同的JavaScript框架或使用服务器端渲染所需的更改将是最小的。
所有示例均使用弹簧启动.
有几个示例相互构建,在每个步骤中添加新功能:
简单:一个非常基本的静态应用程序,只有一个主页,并通过Spring Boot的OAuth 2.0配置属性无条件登录(如果您访问主页,您将被自动重定向到GitHub)。点击:添加用户必须单击才能登录的显式链接。注销:还为经过身份验证的用户添加注销链接。两个提供商:添加第二个登录提供程序,以便用户可以在主页上选择要使用的登录提供程序。自定义错误:为未经身份验证的用户添加错误消息,以及基于 GitHub API 的自定义身份验证。可以在功能阶梯中跟踪从一个应用程序迁移到下一个应用程序所需的更改源代码.该应用程序的每个版本都是其自己的目录,以便您可以比较它们的差异。 |
每个应用程序都可以导入到 IDE 中。可以在 中运行该方法以启动应用。他们都想出了一个主页main
SocialApplication
http://localhost:8080(如果您想登录并查看内容,所有这些都要求您至少拥有一个 GitHub 和 Google 帐户)。
您还可以使用或通过构建 jar 文件并使用 and 运行它(根据mvn spring-boot:run
mvn package
java -jar target/*.jar
春季启动文档和其他可用文档).如果您使用包装纸在顶层,例如
$ cd simple$ ../mvnw package$ java -jar target/*.jar
这些应用程序都可以运行,因为它们将使用在GitHub和Google注册的OAuth 2.0客户端作为该地址。若要在不同的主机或端口上运行它们,需要以这种方式注册应用。如果使用默认值,则不会有将凭据泄露到本地主机之外的危险。但是,请注意在 Internet 上公开的内容,不要将自己的应用注册置于公共源代码管理中。 |
在本部分中,你将创建一个使用 GitHub 进行身份验证的最小应用程序。通过利用 Spring Boot 中的自动配置功能,这将非常容易。
首先,您需要创建一个 Spring Boot 应用程序,这可以通过多种方式完成。最简单的是去https://start.spring.io并生成一个空项目(选择“Web”依赖项作为起点)。等效地,在命令行上执行此操作:
$ mkdir ui && cd ui$ curl https://start.spring.io/starter.tgz -d style=web -d name=simple | tar -xzvf -
然后,您可以将该项目导入到您喜欢的 IDE 中(默认情况下它是一个普通的 Maven Java 项目),或者只是在命令行上处理文件。mvn
在新项目中,在文件夹中创建。您应该添加一些样式表和JavaScript链接,以便结果如下所示:index.html
src/main/resources/static
索引.html
Demo <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script> Demo
这些都不是演示 OAuth 2.0 登录功能所必需的,但最终拥有一个令人愉快的 UI 会很好,所以你不妨从主页中的一些基本内容开始。
如果启动应用程序并加载主页,则会注意到样式表尚未加载。因此,您还需要通过添加jQuery和Twitter Bootstrap来添加它们:
绒球.xml
org.webjars jquery 3.4.1 org.webjars bootstrap 4.3.1 org.webjars webjars-locator-core
最后一个依赖项是 webjars “定位器”,它由 webjars 站点作为库提供。Spring 可以使用定位器在 webjar 中定位静态资产,而无需知道确切的版本(因此 中的无版本链接)。默认情况下,webjar 定位器在 Spring Boot 应用程序中处于激活状态,只要您不关闭 MVC 自动配置即可。/webjars/**
index.html
完成这些更改后,应用应该有一个漂亮的主页。
为了使应用程序安全,您可以简单地将 Spring 安全性添加为依赖项。由于你想要做一个“社交”登录(委托给GitHub),你应该包括Spring Security OAuth 2.0客户端启动器:
绒球.xml
org.springframework.boot spring-boot-starter-oauth2-client
通过添加它,它将默认使用 OAuth 2.0 保护您的应用程序。
接下来,您需要将应用配置为使用 GitHub 作为身份验证提供程序。为此,请执行以下操作:
添加新的 GitHub 应用配置应用程序.yml启动应用程序要使用 GitHub 的 OAuth 2.0 身份验证系统进行登录,您必须首先添加新的 GitHub 应用.
选择“新建 OAuth 应用程序”,然后显示“注册新的 OAuth 应用程序”页面。输入应用名称和说明。然后,输入应用的主页,该主页应为http://localhost:8080,在这种情况下。最后,将授权回调 URL 指定为,然后单击注册应用程序。http://localhost:8080/login/oauth2/code/github
OAuth 重定向 URI 是最终用户的用户代理在向 GitHub 进行身份验证并在“授权应用程序”页面上授予对应用程序的访问权限后重定向回应用程序中的路径。
默认的重定向 URI 模板为 。注册 ID是 的唯一标识符。 |
application.yml
然后,要创建指向 GitHub 的链接,请将以下内容添加到您的:application.yml
应用程序.yml
spring: security: oauth2: client: registration: github: clientId: github-client-id clientSecret: github-client-secret# ...
只需使用刚刚通过 GitHub 创建的 OAuth 2.0 凭据,替换为客户端 ID 和客户端密码。github-client-id
github-client-secret
通过此更改,您可以再次运行应用并访问主页http://localhost:8080.现在,您应该被重定向到使用 GitHub 登录,而不是主页。如果您这样做,并接受要求您进行的任何授权,您将被重定向回本地应用程序,并且主页将可见。
如果您保持登录到 GitHub,则无需使用此本地应用程序重新进行身份验证,即使您在没有 Cookie 和缓存数据的新浏览器中打开它也是如此。(这就是单一登录的含义。
如果您正在使用示例应用程序完成本节,请务必清除浏览器缓存中的 Cookie 和 HTTP 基本凭据。对于单个服务器执行此操作的最佳方法是打开一个新的专用窗口。 |
授予对此示例的访问权限是安全的,因为只有本地运行的应用才能使用令牌,并且它要求的范围是有限的。但是,当您登录这样的应用程序时,请注意您正在批准的内容:他们可能会要求允许做超出您满意的事情(例如,他们可能会要求允许更改您的个人数据,这不太可能符合您的利益)。
您刚刚编写的应用程序(在 OAuth 2.0 术语中)是一个客户端应用程序,它使用授权代码授予从 GitHub(授权服务器)获取访问令牌。
然后,它使用访问令牌向 GitHub 询问一些个人详细信息(仅您允许它执行的操作),包括您的登录 ID 和您的姓名。在此阶段,GitHub 充当资源服务器,解码您发送的令牌,并检查它是否授予应用程序访问用户详细信息的权限。如果该过程成功,应用程序会将用户详细信息插入到 Spring 安全性上下文中,以便对您进行身份验证。
如果您查看浏览器工具(Chrome 或 Firefox 上的 F12)并跟踪所有跃点的网络流量,您将看到与 GitHub 来回重定向,最后您将以新标题返回主页。此 cookie(默认情况下)是 Spring (或任何基于 servlet)应用程序的身份验证详细信息的令牌。Set-Cookie
JSESSIONID
因此,我们有一个安全的应用程序,从某种意义上说,要查看任何内容,用户必须向外部提供商(GitHub)进行身份验证。
我们不想将其用于网上银行网站。但出于基本的识别目的,以及在网站的不同用户之间隔离内容,这是一个很好的起点。这就是为什么这种身份验证如今非常流行的原因。
在下一节中,我们将向应用程序添加一些基本功能。我们还将让用户在获得初始重定向到 GitHub 时更清楚地了解发生了什么。
在本部分中,您将修改简单您刚刚通过添加用于登录 GitHub 的显式链接构建的应用程序。新链接不会立即重定向,而是在主页上可见,用户可以选择登录或保持未经身份验证。只有当用户单击链接时,才会呈现安全内容。
若要在用户经过身份验证的条件下呈现内容,可以选择服务器端或客户端呈现。
在这里,您将使用杰奎里,但如果您更喜欢使用其他内容,则翻译客户端代码应该不是很困难。
要开始使用动态内容,您需要标记几个 HTML 元素,如下所示:
索引.html
With GitHub: click hereLogged in as:
默认情况下,第一个将显示,第二个不会。另请注意带有属性的空。 稍后,你将添加一个服务器端终结点,该终结点将以 JSON 形式返回登录的用户详细信息。 但是,首先,添加以下 JavaScript,它将访问该端点。根据端点的响应,此 JavaScript 将使用用户名填充标记并相应地切换: 索引.html 请注意,此 JavaScript 需要调用服务器端端点。 现在,您将添加刚才提到的服务器端终结点,调用它。它将发回当前登录的用户,我们可以在主类中轻松完成此操作: 社交应用.java 请注意 、 和注入到处理程序方法中的用法。 在终结点中返回 whole 不是一个好主意,因为它可能包含您不希望向浏览器客户端透露的信息。 您需要进行最后一项更改。 此应用程序现在可以正常工作并像以前一样进行身份验证,但它仍然会在显示页面之前重定向。为了使链接可见,我们还需要通过扩展来关闭主页上的安全性: 社交应用 Spring Boot 对 a 附加了特殊含义,该类的注释为 : 它使用它来配置携带 OAuth 2.0 身份验证处理器的安全过滤器链。 上述配置指示允许的端点的白名单,其他每个端点都需要身份验证。 您希望允许: 但是,您不会在此配置中看到任何有关此配置的内容。所有内容(包括)都保持安全,除非由于最后的配置而指明。 最后,由于我们通过 Ajax 与后端接口,因此我们希望将端点配置为使用 401 响应,而不是重定向到登录页面的默认行为。配置为我们实现了这一目标。 完成这些更改后,应用程序就完成了,如果您运行它并访问主页,您应该会看到一个样式精美的 HTML 链接,指向“使用 GitHub 登录”。该链接不会将您直接带到 GitHub,而是转到处理身份验证(并将重定向发送到 GitHub)的本地路径。进行身份验证后,您将被重定向回本地应用程序,它现在显示您的姓名(假设您已在 GitHub 中设置权限以允许访问该数据)。 在本节中,我们将修改点击我们通过添加一个允许用户注销应用程序的按钮来构建应用程序。这似乎是一个简单的功能,但它需要一点小心来实现,所以值得花一些时间讨论如何做到这一点。大多数更改都与以下事实有关:我们正在将应用程序从只读资源转换为读写资源(注销需要更改状态),因此在任何不仅仅是静态内容的实际应用程序中都需要相同的更改。 在客户端上,我们只需要提供一个注销按钮和一些 JavaScript 来回调服务器以请求取消身份验证。首先,在 UI 的“经过身份验证”部分,我们添加按钮: 索引.html 然后我们提供它在 JavaScript 中引用的函数: 索引.html 该函数执行 POST 操作,然后清除动态内容。现在我们可以切换到服务器端来实现该端点。 Spring 安全性内置了对端点的支持,该端点将为我们做正确的事情(清除会话并使 cookie 无效)。要配置端点,我们只需扩展以下中的现有方法: 社交应用.java 端点要求我们向其 POST 并保护用户免受跨站点请求伪造(CSRF,发音为“海上冲浪”)的侵害,它需要将令牌包含在请求中。令牌的值链接到当前会话,这是提供保护的原因,因此我们需要一种方法将这些数据放入我们的 JavaScript 应用程序中。 许多JavaScript框架都内置了对CSRF的支持(例如,在Angular中,他们称之为XSRF),但它的实现方式通常与Spring Security的开箱即用行为略有不同。例如,在Angular中,前端希望服务器向它发送一个名为“XSRF-TOKEN”的cookie,如果它看到这一点,它将把值作为名为“X-XSRF-TOKEN”的标头发送回去。我们可以用简单的jQuery客户端实现相同的行为,然后服务器端的更改将与其他前端实现一起工作,没有或很少的更改。为了向 Spring Security 传授这一点,我们需要添加一个创建 cookie 的过滤器。 在我们执行以下操作: 社交应用.java 由于此示例中我们没有使用更高级别的框架,因此您需要显式添加 CSRF 令牌,您刚刚将其作为后端的 cookie 提供。为了使代码更简单一些,请包含库: 绒球.xml 然后,您可以在 HTML 中引用它: 索引.html 最后,您可以在 XHR 中使用方便的方法: 索引.html 完成这些更改后,我们已准备好运行应用程序并尝试新的注销按钮。启动应用并在新的浏览器窗口中加载主页。单击“登录”链接将您带到GitHub(如果您已经登录,则可能不会注意到重定向)。单击“注销”按钮以取消当前会话并将应用程序返回到未经身份验证的状态。如果您好奇,您应该能够在浏览器与本地服务器交换的请求中看到新的 cookie 和标头。 请记住,现在注销端点正在与浏览器客户端一起使用,然后所有其他HTTP请求(POST,PUT,DELETE等)也将正常工作。因此,对于具有一些更现实功能的应用程序来说,这应该是一个很好的平台。 在本部分中,您将修改注销您已经构建的应用程序,添加贴纸页面,以便最终用户可以在多组凭据之间进行选择。 让我们将Google添加为最终用户的第二个选项。 要使用 Google 的 OAuth 2.0 身份验证系统进行登录,您必须在 Google API 控制台中设置一个项目以获取 OAuth 2.0 凭据。 谷歌的OAuth 2.0实现对于身份验证符合OpenID Connect 1.0规格和开放身份认证. 按照OpenID Connect页面,从“设置 OAuth 2.0”部分开始。 完成“获取 OAuth 2.0 凭据”说明后,您应该有一个新的 OAuth 客户端,其凭据由客户端 ID 和客户端密钥组成。 此外,还需要提供重定向 URI,就像之前为 GitHub 所做的那样。 在“设置重定向 URI”子部分中,确保“授权的重定向 URI”字段设置为 。 然后,您需要将客户端配置为指向谷歌。由于 Spring Security 是在考虑多个客户端的情况下构建的,因此您可以将我们的 Google 凭据添加到您为 GitHub 创建的凭据旁边: 应用程序.yml 如您所见,Google是Spring Security提供开箱即用支持的另一个提供商。 在客户端中,更改是微不足道的 - 您只需添加另一个链接: 索引.html URL 中的最终路径应与 中的客户端注册 ID 匹配。 Spring 安全性附带了一个默认的提供程序选择页面,可以通过指向而不是 来访问该页面。 许多应用程序需要在本地保存有关其用户的数据,即使身份验证委托给外部提供程序也是如此。我们不在这里显示代码,但很容易通过两个步骤完成。 提示:在对象中添加一个字段以链接到外部提供程序中的唯一标识符(不是用户名,而是外部提供程序中帐户的唯一名称)。 在本部分中,您将修改两个提供商之前构建的应用,用于向无法进行身份验证的用户提供一些反馈。同时,您将扩展身份验证逻辑以包含仅允许属于特定 GitHub 组织的用户的规则。“组织”是GitHub特定领域的概念,但可以为其他提供商设计类似的规则。例如,对于 Google,您可能只想对来自特定网域的用户进行身份验证。 这两个提供商示例使用 GitHub 作为 OAuth 2.0 提供程序: 应用程序.yml 在客户端上,您可能希望为无法进行身份验证的用户提供一些反馈。为了促进这一点,您可以添加一个 div,您最终将向其添加一条信息性消息。 索引.html 然后,添加对终结点的调用,并用结果填充 : 索引.html 错误函数与后端一起检查是否有任何要显示的错误 若要支持检索错误消息,需要在身份验证失败时捕获它。为此,您可以配置一个 ,如下所示: 每当身份验证失败时,上述内容都会将错误消息保存到会话中。 然后,您可以添加一个简单的控制器,如下所示: 社交应用.java 这将替换应用程序中的默认页面,这对于我们的情况来说很好,但可能不够复杂,无法满足您的需求。 如果用户不能或不想使用 GitHub 登录,则 Spring Security 已经发出 401 响应,因此如果您无法进行身份验证(例如,通过拒绝令牌授予),该应用程序已经在工作。 为了给事情增添趣味,您可以扩展身份验证规则以拒绝不在正确组织中的用户。 您可以使用 GitHub API 了解有关用户的更多信息,因此您只需将其插入身份验证过程的正确部分。 幸运的是,对于这样一个简单的用例,Spring Boot 提供了一个简单的扩展点:如果你声明了一个 of 类型,它将用于标识用户主体。您可以使用该挂钩来断言用户在正确的组织中,如果不是,则引发异常: 社交应用.java 请注意,此代码依赖于代表经过身份验证的用户访问 GitHub API 的实例。完成此操作后,它会遍历组织,寻找与“spring-projects”匹配的组织(这是用于存储Spring开源项目的组织)。如果您希望能够成功进行身份验证并且您不在 Spring 工程团队中,则可以在此处替换您自己的值。如果没有匹配项,它会抛出一个 ,Spring Security 会选取该响应并转换为 401 响应。 也必须创建为豆子,但这微不足道,因为它的成分都可以通过使用: 显然,上面的代码可以推广到其他身份验证规则,有些适用于GitHub,有些适用于其他OAuth 2.0提供程序。您所需要的只是提供程序 API 的一些知识。 我们已经看到了如何使用 Spring Boot 和 Spring Security 来构建多种风格的应用程序,只需很少的努力。贯穿所有示例的主题是使用外部 OAuth 2.0 提供程序进行身份验证。 所有示例应用都可以轻松扩展和重新配置,以用于更具体的用例,通常只需更改配置文件即可。请记住,如果你在自己的服务器中使用示例版本向 GitHub(或类似)注册并获取你自己的主机地址的客户端凭据。并且记住不要将这些凭据放在源代码管理中!
id
<script type="text/javascript"> $.get("/user", function(data) { $("#user").html(data.name); $(".unauthenticated").hide() $(".authenticated").show() });</script>
/user
端点
/user
/user
@SpringBootApplication@RestControllerpublic class SocialApplication { @GetMapping("/user") public Map
@RestController
@GetMapping
OAuth2User
OAuth2User
公开主页
WebSecurityConfigurerAdapter
@SpringBootApplication@RestControllerpublic class SocialApplication extends WebSecurityConfigurerAdapter { // ... @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests(a -> a .antMatchers("/", "/error", "/webjars/**").permitAll() .anyRequest().authenticated() ) .exceptionHandling(e -> e .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) ) .oauth2Login(); // @formatter:on }}
WebSecurityConfigurerAdapter
@SpringBootApplication
/
因为这是您刚刚动态化的页面,其某些内容对未经身份验证的用户可见/error
因为这是用于显示错误的 Spring 引导端点,并且/webjars/**
因为您希望您的 JavaScript 为所有访问者运行,无论是否经过身份验证/user
/user
.anyRequest().authenticated()
authenticationEntryPoint
添加注销按钮
客户端更改
logout()
var logout = function() { $.post("/logout", function() { $("#user").html(""); $(".unauthenticated").show(); $(".authenticated").hide(); }) return true;}
logout()
/logout
添加注销端点
/logout
configure()
WebSecurityConfigurerAdapter
@Overrideprotected void configure(HttpSecurity http) throws Exception { // @formatter:off http // ... existing code here .logout(l -> l .logoutSuccessUrl("/").permitAll() ) // ... existing code here // @formatter:on}
/logout
WebSecurityConfigurerAdapter
@Overrideprotected void configure(HttpSecurity http) throws Exception { // @formatter:off http // ... existing code here .csrf(c -> c .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ) // ... existing code here // @formatter:on}
在客户端中添加 CSRF 令牌
js-cookie
<script type="text/javascript" src="/webjars/js-cookie/js.cookie.js"></script>
Cookies
$.ajaxSetup({ beforeSend : function(xhr, settings) { if (settings.type == "POST" || settings.type == "PUT" || settings.type == "DELETE") { if (!(/^http:.*/.test(settings.url) || /^https:.*/ .test(settings.url))) { // Only send the token to relative URLs i.e. locally. xhr.setRequestHeader("X-XSRF-TOKEN", Cookies.get("XSRF-TOKEN")); } } }});
准备滚动!
使用 GitHub 登录
初始设置
设置重定向 URI
http://localhost:8080/login/oauth2/code/google
添加客户端注册
spring: security: oauth2: client: registration: github: clientId: github-client-id clientSecret: github-client-secret google: client-id: google-client-id client-secret: google-client-secret
添加登录链接
application.yml
/login
/oauth2/authorization/{registrationId}
如何添加本地用户数据库
User
实现并公开以调用授权服务器以及数据库。您的实现可以委托给默认实现,这将完成调用授权服务器的繁重工作。您的实现应返回扩展自定义对象和实现的内容。OAuth2UserService
User
OAuth2User
User
为未经身份验证的用户添加错误页面
切换到 GitHub
spring: security: oauth2: client: registration: github: client-id: bd1c0a783ccdd1c9b9e4 client-secret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1 # ...
检测客户端中的身份验证失败
/error
$.get("/error", function(data) { if (data) { $(".error").html(data); } else { $(".error").html(""); }});
添加错误消息
AuthenticationFailureHandler
protected void configure(HttpSecurity http) throws Exception { // @formatter:off http // ... existing configuration .oauth2Login(o -> o .failureHandler((request, response, exception) -> { request.getSession().setAttribute("error.message", exception.getMessage()); handler.onAuthenticationFailure(request, response, exception); }) );}
/error
@GetMapping("/error")public String error(HttpServletRequest request) { String message = (String) request.getSession().getAttribute("error.message"); request.getSession().removeAttribute("error.message"); return message;}
/error
在服务器中生成 401
@Bean
OAuth2UserService
@Beanpublic OAuth2UserService
WebClient
OAuth2AuthenticationException
WebClient
spring-boot-starter-oauth2-client
@Beanpublic WebClient rest(ClientRegistrationRepository clients, OAuth2AuthorizedClientRepository authz) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clients, authz); return WebClient.builder() .filter(oauth2).build();}
WebClient
结论