推荐 原创 视频 Java开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发
Lambda在线 > 生活点亮技术 > 学习OAuth2,看这一篇就够了【基于Spring Boot2.X】

学习OAuth2,看这一篇就够了【基于Spring Boot2.X】

生活点亮技术 2018-10-21


原文链接:Spring Boot and OAuth2

译者: helloworldtang

本指南向您展示了如何使用OAuth2 和Spring Boot来构建一个使用"第三方登陆"来完成各种事情的示例应用程序。它从一个简单的、单一 Provider的SSO开始,然后使用一个自托管的OAuth2 AuthorizationServer,它可以选择用于身份认证的 Provider(Facebook或Github)。这些示例都是在后台使用Spring Boot和Spring OAuth的单页应用程序。它们也都使用了在前端很常用的 jQuery ,但是转换到不同的 JavaScript框架或使用服务器端渲染所需的更改也将是最小的。

因为其中一个示例是一个完整的OAuth2 AuthorizationServer,我们使用了 shim JAR,它支持从Spring Boot 2.0到Spring Security OAuth2的适配。通过Spring Boot安全特性中对原生OAuth2的支持,可以实现更简单的示例,并且配置也非常类似。

下面的示例项目都是依次在上一个的基础上,添加了新的特性:

  • simple: 一个非常基础的静态应用,只有一个主页,并且通过Spring Boot的 @EnableOAuth2Sso进行第三方登录(在访问主页,你将被自动重定向到Facebook)。

  • click:添加一个显式的链接,用户必须点击才能登录。

  • logout:为经过身份认证的用户添加一个 logout链接。

  • manual:通过手动配置来展示 @EnableOAuth2Sso是如何工作的。

  • github: 在这个应用程序中添加了第二个用于登陆的 Provider,这样用户就可以在主页上选择使用哪一个进行身份认证了。

  • auth-server:将该应用程序转换为完全成熟的能够发出自己 Token的OAuth2 AuthorizationServer,但仍然使用外部OAuth2 Provider进行身份认证。

  • custom-error:为未经身份验证的用户添加一条错误信息,以及基于Github API的自定义身份认证。

从一个应用程序迁移到新版本所需要的变更可以在源代码的commit日志中找到(源码在 Github)。代码仓库中的前6个变更是正在升级单个应用程序,因此您可以很容易地看到它们之间的差异。在早期提交的应用中,你可能会看到每次提交之间的差异,在这个教程中你看到的是成品,是一个最终状态。

上面示例中的每一个工程都可以导入到IDE中,并且每个工程都有一个包含 main的 SocialApplication,你可以运行这个类的 main方法来启动这个应用程序。它们的主页都可以通过http://localhost:8080 来访问(如果你想登录并查看页面信息,那么至少需要有一个Facebook账户)。你也可以在命令行使用 mvn spring-boot:run 或通过构建jar文件并使用 mvnpackage和 java-jar target/*.jar命令来运行应用程序(每个 Spring Boot docs 和其他 可用的文档)。如果您在项目使用了wrapper ,那么就不需要安装Maven了。

 
   
   
 
  1. $ cd simple

  2. $ ../mvnw package

  3. $ java -jar target/*.jar

这些例子都在 localhost:8080上运行,因为它们使用的是注册在Facebook和Github上的OAuth2客户端应用程序。如果要在不同的主机或端口上运行这些应用程序,您需要注册自己的客户端应用程序,并将 AutorizationServer生成的 clientSecret配置到客户端应用程序的配置文件中。如果你使用默认值,就不会泄露你的Facebook或Github的凭证,但要注意你在互联网上发布的内容,不要将在 AutorizationServer上注册的客户端应用程序信息放在公开的代码版本控制服务器上。

使用 Facebook进行单点登陆

在本节中,我们创建了一个使用Facebook进行身份认证的MVP版本。如果我们使用Spring Boot的自动配置特性,那么这会很容易实现。

创建一个新项目

首先,我们需要创建一个Spring Boot应用程序,它可以通过多种方式完成。最简单的方法是访问 http://start.spring.io ,并生成一个空项目(勾选“Web”依赖)。同样在命令行中这样做:

 
   
   
 
  1. $ mkdir ui && cd ui

  2. $ curl https://start.spring.io/starter.tgz -d style=web -d name=simple | tar -xzvf -

然后,您可以将该项目导入到您最常用的IDE中(默认情况下它是一个普通的基于Java的Maven项目),或者只在命令行上处理文件和使用 mvn命令。

创建一个主页

在新项目的"src/main/resources/static"文件夹下创建一个 index.html文件。然后在这个文件中添加一些第三方 css和 javascript依赖,如下所示:

index.html

 
   
   
 
  1. <!doctype html>

  2. <html lang="en">

  3. <head>

  4.    <meta charset="utf-8"/>

  5.    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>

  6.    <title>Demo</title>

  7.    <meta name="description" content=""/>

  8.    <meta name="viewport" content="width=device-width"/>

  9.    <base href="/"/>

  10.    <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>

  11.    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>

  12.    <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>

  13. </head>

  14. <body>

  15.    <h1>Demo</h1>

  16.    <div class="container"></div>

  17. </body>

  18. </html>

这些都不是演示OAuth2登录所必需的,但是如果希望有一个漂亮的UI,那么我们不妨从主页上的一些基本内容着手。

如果你启动应用并加载主页,你会发现 index.html中的 css引用并没有被加载。我们可以通过添加一些依赖项,来完成这些文件的加载:

pom.xml

 
   
   
 
  1. <dependency>

  2.    <groupId>org.webjars</groupId>

  3.    <artifactId>jquery</artifactId>

  4.    <version>2.1.1</version>

  5. </dependency>

  6. <dependency>

  7.    <groupId>org.webjars</groupId>

  8.    <artifactId>bootstrap</artifactId>

  9.    <version>3.2.0</version>

  10. </dependency>

  11. <dependency>

  12.    <groupId>org.webjars</groupId>

  13.    <artifactId>webjars-locator-core</artifactId>

  14. </dependency>

我们添加了Twitter公司开源的 bootstrap和 jQuery(这是我们现在所需要的)。另一个依赖项是 webjars-locator,它是由webjar站点提供的一个库,它可以在Spring中使用来提供web页面使用的静态资源而不需要知道确切的版本(因此, index.html文件中引用的 /webjars/**链接就不需要版本号)。只要您不关闭MVC的自动配置,Spring Boot 应用程序就会默认使用 webjars-locator

基于上面的操作,我们应该已经为我们的应用提供了一个漂亮的主页。

保护应用程序

为了使应用程序安全,我们只需要将Spring Security作为依赖项添加进去。如果我们这样做,会使用默认的 HTTPBasic来保护它,因此,既然我们想要做一个“第三方登录”(委托给Facebook),我们也需要添加 SpringSecurityOAuth2依赖项:

pom.xml

 
   
   
 
  1. <dependency>

  2.    <groupId>org.springframework.boot</groupId>

  3.    <artifactId>spring-boot-starter-security</artifactId>

  4. </dependency>

  5. <dependency>

  6.    <groupId>org.springframework.security.oauth.boot</groupId>

  7.    <artifactId>spring-security-oauth2-autoconfigure</artifactId>

  8.    <version>2.0.0.RELEASE</version>

  9. </dependency>

要想连接到Facebook AuthorizationServer,交对当前应用程序进行授权,就需要在 main上使用 @EnableOAuth2Sso注解:

SocialApplication.java

 
   
   
 
  1. @SpringBootApplication

  2. @EnableOAuth2Sso

  3. public class SocialApplication {

  4.  ...

  5. }

还需要一些配置(为了提高可读性,可以将 application.properties转换为YAML格式):
application.yml

 
   
   
 
  1. security:

  2.  oauth2:

  3.    client:

  4.      clientId: 233668646673605

  5.      clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d

  6.      accessTokenUri: https://graph.facebook.com/oauth/access_token

  7.      userAuthorizationUri: https://www.facebook.com/dialog/oauth

  8.      tokenName: oauth_token

  9.      authenticationScheme: query

  10.      clientAuthenticationScheme: form

  11.    resource:

  12.      userInfoUri: https://graph.facebook.com/me

  13. ...

有了上面在Facebook配置的信息,你就可以再次运行该应用程序,并访问主页 http://localhost:8080 。你应该被重定向到使用Facebook登录的页面,而不是主页。如果你这样做了并且接受被要求的任何授权,那么将被重定向到本地应用并且能够正常地访问主页了。如果你登录过Facebook,你就不需要用这个本地应用再重新认证,即使你在一个没有cookie和没有缓存数据的全新浏览器中打开它。(这就是单点登陆的意思。)

如果您是通过示例应用程序来学习本节的内容,请确保已经清除了浏览器缓存的cookie和 HTTPBasic凭证。如果您在使用Chrome浏览器,那么对单个站点而言,最好的方法是打开一个新的隐身窗口。

访问这个示例应用程序是安全的,因为只有在本地运行的应用程序才能使用 token,并且它所要求的 scope也是限定过的。当你登录这样的应用程序时,要注意你所授权的内容:他们可能会要求允许做更多的事情,而这些事情会让你感到不舒服。(例如,他们可能会请求允许更改你的个人数据,这可能就不是你想要的)。

刚才发生了什么?

在OAuth2术语中,您刚刚编写的应用程序是一个客户端应用程序,它使用 授权码许可从Facebook( AuthorizationServer)获得 AccessToken。然后用这个 AccessToken从Facebook获取一些个人信息(这是你授权应用程序做的事情),包括你的login ID和名字。在这个阶段,facebook充当了 ResourceServer,解码你发送的token,检查token允许应用程序访问的用户详细信息。如果这个过程成功,应用程序将用户详细信息插入到Spring Security上下文中,这样就通过了身份认证。

如果你看一下浏览器工具(在Chrome上的快捷键是F12),跟踪所有请求的网络流量,你会在Facebook上看到重定向,最后你返回首页,并且会有一个新增 Set-Cookie头的操作。这个 cookie(默认情况下是 JSESSIONID)是Spring(或任何基于 Servlet的)应用程序的认证要用到的 token

因此,我们拥有一个安全的应用程序,从某种意义上说,要查看任何内容,用户必须通过外部 Provider(Facebook)进行身份认证。我们不会在网银网站上使用它,但是为了基本的识别目的,为了隔离站点不同用户之间的内容,但这是一个很好的起点,这就解释了为什么这种认证在最近很流行。在下一节中,我们将向应用程序添加一些基本特性,并让用户更清楚地看到,当他们第一次重定向到Facebook时,会发生什么。

增加一个欢迎页

在本节中,我们修改了上面刚刚构建的 simple应用程序,主要的改动是添加一个使用Facebook登录的显式链接。新的链接将在主页上显示,而不是立即被重定向,用户可以选择登录或保持未认证。只有当用户已经单击过该链接,被保护的内容才会展示。

个性化展示内容

为了实现根据用户是否经过身份认证来渲染一些内容,我们可以使用服务器端渲染(例如,使用 Freemarker或 Thymeleaf),或者我们可以使用一些 JavaScript通过浏览器来完成这些请求。为了做到这一点,我们将使用 AngularJS,但是,如果您喜欢使用不同的框架,那么转换成其它语言的前端代码也不会那么困难。

要开始使用动态内容,我们需要在这些动态内容将要展示的地方写入 HTML代码:

index.html

 
   
   
 
  1. <div class="container unauthenticated">

  2.    With Facebook: <a href="/login">click here</a>

  3. </div>

  4. <div class="container authenticated" style="display:none">

  5.    Logged in as: <span id="user"></span>

  6. </div>

这个 HTML使我们需要一些客户端代码来操作 authenticated、 unauthenticated 和 user元素。下面是这些特性的一个简单实现(在 <body>末尾删除它们):

index.html

 
   
   
 
  1. <script type="text/javascript">

  2.    $.get("/user", function(data) {

  3.        $("#user").html(data.userAuthentication.details.name);

  4.        $(".unauthenticated").hide()

  5.        $(".authenticated").show()

  6.    });

  7. </script>

服务器端的一些变更

为了实现这一点,我们需要在服务器端进行一些变更。 home 控制器 需要一个 /user 端点,该端点描述了当前经过身份认证的用户。这很容易做到,譬如在我们的 main上:

SocialApplication.java

 
   
   
 
  1. @SpringBootApplication

  2. @EnableOAuth2Sso

  3. @RestController

  4. public class SocialApplication {

  5.  @RequestMapping("/user")

  6.  public Principal user(Principal principal) {

  7.    return principal;

  8.  }

  9.  public static void main(String[] args) {

  10.    SpringApplication.run(SocialApplication.class, args);

  11.  }

  12. }

请注意,我们将 @RestController和 @RequestMapping和 java.security.Principal注入到 handler方法中。

在 /user端点中返回一个完整的 Principal并不是一个好主意(它可能包含您不愿意向浏览器客户端显示的信息)。我们这么做只是为了快点实现功能。在下文中,我们将在端点中进行转换,并隐藏不需要客户端知道的信息。

应用现在可以像以前一样正常运行和进行身份认证,但不会给用户一个机会点击我们刚刚新增的链接。为了使链接可见,我们还需要通过添加一个 WebSecurityConfigurer来去掉对主页的安全性检查:

SocialApplication.java

 
   
   
 
  1. @SpringBootApplication

  2. @EnableOAuth2Sso

  3. @RestController

  4. public class SocialApplication extends WebSecurityConfigurerAdapter {

  5.  ...

  6.  @Override

  7.  protected void configure(HttpSecurity http) throws Exception {

  8.    http

  9.      .antMatcher("/**")

  10.      .authorizeRequests()

  11.        .antMatchers("/", "/login**", "/webjars/**", "/error**")

  12.        .permitAll()

  13.      .anyRequest()

  14.        .authenticated();

  15.  }

  16. }

Spring Boot在添加了 @EnableOAuth2Sso注解的类上附加了一个特殊的含义:它使用这个注解来配置承载OAuth2身份认证处理器的安全过滤器链。因此,要使主页可见,我们需要做的就是显式地 authorizeRequests()到主页及其包含的静态资源(还包括对处理认证的登录端点的访问)。所有其他请求(例如,到 /user端点)都需要身份认证。

/error**是一条不需要保护的path,因为如果应用程序中存在问题,我们希望Spring Boot能够呈现错误,即使用户是未经身份验证的。

有了这个更改,应用程序就完成了。如果运行这个应用程序并访问主页,您应该会看到一个漂亮的 loginwithFacebookHTML链接。这个链接不直接指向Facebook,而是指向处理认证的本地路径(并发送一个重定向到Facebook)。一旦通过了身份认证,浏览器页面就会被重定向到本地应用,并且在页面显示你的用户名(假设你已经在Facebook上设置了你的权限,允许访问这些数据)。

增加一个 Logout按钮

在本节中,我们修改 click 应用,主要是添加一个允许用户退出程序按钮。这似乎是一个简单的功能,但在具体实现时,需要谨慎一点,所以这是值得花一些时间讨论到底如何去做。大多数变更都是进行这样的操作,将应用从只读资源转换为可读可写( logout需要进行状态更改),因此,在任何不只是静态内容的实际应用程序中,都需要进行相同的更改。

客户端的变更

在客户端,我们只需要提供一个 logout按钮和一些 JavaScript调用服务器来发送取消认证的请求。首先,在 authenticated部分的UI中,我们添加按钮:

index.html

 
   
   
 
  1. <div class="container authenticated">

  2.  Logged in as: <span id="user"></span>

  3.  <div>

  4.    <button onClick="logout()" class="btn btn-primary">Logout</button>

  5.  </div>

  6. </div>

然后,我们提供在 JavaScript中使用的 logout()函数:

index.html

 
   
   
 
  1. var logout = function() {

  2.    $.post("/logout", function() {

  3.        $("#user").html('');

  4.        $(".unauthenticated").show();

  5.        $(".authenticated").hide();

  6.    })

  7.    return true;

  8. }

logout()函数向 /logout端点发送一个 POST请求 ,然后更改动态内容的展示。现在,我们可以切换到服务器端来实现该端点。

新增一个 Logout端点

Spring Security已经构建了支持 /logout 的端点,这将为我们做正确的事情(清除会话并使 cookie无效)。为了配置 端点,我们只需在 WebSecurityConfigurer中简单地扩展现有的 configure()方法:

SocialApplication.java

 
   
   
 
  1. @Override

  2. protected void configure(HttpSecurity http) throws Exception {

  3.  http.antMatcher("/**")

  4.    ... // existing code here

  5.    .and().logout().logoutSuccessUrl("/").permitAll();

  6. }

/logout 端点要求我们使用 POST方法来调用,并保护用户不受 CrossSiteRequestForgery(CSRF,发音为“sea surf”),它需要一个 token包含在请求中。 token的价值与当前 session相关联,这也是提供保护的原因,因此我们需要一种方法将这些数据放入我们的 JavaScript应用程序中。

许多 JavaScript框架都支持 CSRF(例如,在 Angular中称为 XSRF),但它通常以一种与Spring Security稍微不同的方式来实现。例如,在Angular中,前端希望服务器发送一个名为 XSRF-TOKEN的 cookie,如果收到了这个 cookie,Angular会将这个 cookie值存放在名为 X-XSRF-TOKEN的HTTP头中发送回后端服务器。我们可以用简单的jQuery客户端来实现相同的行为,那么服务器端出现变更或使用其他前端框架时,就不需要或需要很少更改。为了使用Spring Security,我们需要添加一个用来创建 cookie的过滤器,并且告诉现有的 CRSF过滤器关于HTTP头的名称。在 WebSecurityConfigurer中:

SocialApplication.java

 
   
   
 
  1. @Override

  2. protected void configure(HttpSecurity http) throws Exception {

  3.  http.antMatcher("/**")

  4.    ... // existing code here

  5.    .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

  6. }

在客户端新增一个 CSRFToken

由于在本例中没有使用特别高级别的框架,所以我们需要显式地添加 CSRF token,这个 token是我们从后端返回的 cookie中获取的。为了使代码更简单,我们还使用了一个额外的库:

pom.xml

 
   
   
 
  1. <dependency>

  2.    <groupId>org.webjars</groupId>

  3.    <artifactId>js-cookie</artifactId>

  4.    <version>2.1.0</version>

  5. </dependency>

在HTML中导入:

index.html

 
   
   
 
  1. <script type="text/javascript" src="/webjars/js-cookie/js.cookie.js"></script>

然后,我们可以在 xhr中使用 Cookie对象的 get方法,如下所示:

index.html

 
   
   
 
  1. $.ajaxSetup({

  2. beforeSend : function(xhr, settings) {

  3.  if (settings.type == 'POST' || settings.type == 'PUT'

  4.      || settings.type == 'DELETE') {

  5.    if (!(/^http:.*/.test(settings.url) || /^https:.*/

  6.        .test(settings.url))) {

  7.      // Only send the token to relative URLs i.e. locally.

  8.      //只将token发送到相对URL,即本地。

  9.      xhr.setRequestHeader("X-XSRF-TOKEN",

  10.          Cookies.get('XSRF-TOKEN'));

  11.    }

  12.  }

  13. }

  14. });

准备好了!

有了这些变化,我们就可以运行应用程序并尝试新的 logout按钮了。启动应用程序,并在一个新的浏览器窗口中加载主页。点击 login链接,浏览器会将你带到Facebook授权页(如果已经登录,你可能不会注意到重定向)。单击 Logout按钮取消当前会话,并将该应用程序返回到未经验证的状态。如果好奇,您应该能够在浏览器与本地服务器交互的请求中看到新的 cookie和 HTTP

请记住, logout端点与浏览器客户端一起工作,然后所有其他HTTP请求(POST、PUT、DELETE等等)也会正常工作。因此,对于具有更实际功能的应用程序来说,这应该是一个很好的平台。

OAuth2客户端的手动配置

在本节中,我们将通过在 @EnableOAuth2Sso注解中分离“魔法”来修改我们已经构建的 logout应用程序,手动显式配置涉及的所有内容。

客户端和身份认证

@EnableOAuth2Sso背后有两个特性:OAuth2客户端和身份认证。客户端是可重用的,因此您也可以使用它与 AuthorizationServer(在本例中为Facebook)提供的OAuth2资源交互(在本例中是Graph API)。身份认证部分将你的应用与Spring Security的其他部分保持一致,因此一旦与Facebook的交互结束,你的应用就会像其他安全的Spring应用一样。

客户端部分由Spring Security OAuth2提供,切换到另一个注解 @EnableOAuth2Client。所以这个转换的第一步就是移除 @EnableOAuth2Sso注解,并用抽象程度更低的注解替换:

SocialApplication.java

 
   
   
 
  1. @SpringBootApplication

  2. @EnableOAuth2Client

  3. @RestController

  4. public class SocialApplication extends WebSecurityConfigurerAdapter {

  5.  ...

  6. }

一旦这样做了,我们就会为我们创造一些有用的东西。首先,我们可以注入一个 OAuth2ClientContext,并使用它来构建一个身份身份认证过滤器,将它添加到我们的安全配置中:

SocialApplication.java

 
   
   
 
  1. @SpringBootApplication

  2. @EnableOAuth2Client

  3. @RestController

  4. public class SocialApplication extends WebSecurityConfigurerAdapter {

  5.  @Autowired

  6.  OAuth2ClientContext oauth2ClientContext;

  7.  @Override

  8.  protected void configure(HttpSecurity http) throws Exception {

  9.    http.antMatcher("/**")

  10.      ...

  11.      .and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);

  12.  }

  13.  ...

  14. }

这个过滤器是在我们使用 OAuth2ClientContext的新方法中创建的:

SocialApplication.java

 
   
   
 
  1. private Filter ssoFilter() {

  2.  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");

  3.  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);

  4.  facebookFilter.setRestTemplate(facebookTemplate);

  5.  UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());

  6.  tokenServices.setRestTemplate(facebookTemplate);

  7.  facebookFilter.setTokenServices(tokenServices);

  8.  return facebookFilter;

  9. }

这个过滤器还需要了解与Facebook相关的客户端注册:

SocialApplication.java

 
   
   
 
  1.  @Bean

  2.  @ConfigurationProperties("facebook.client")

  3.  public AuthorizationCodeResourceDetails facebook() {

  4.    return new AuthorizationCodeResourceDetails();

  5.  }

为了完成身份认证,它需要知道Facebook的用户信息 端点在哪:

SocialApplication.java

 
   
   
 
  1.  @Bean

  2.  @ConfigurationProperties("facebook.resource")

  3.  public ResourceServerProperties facebookResource() {

  4.    return new ResourceServerProperties();

  5.  }

注意,在这两个“静态”数据对象( facebook()和 facebookResource())中,我们使用了一个 @Bean来修饰 @ConfigurationProperties。这意味着我们可以把 application.yml转换成稍微新的格式,在这里,配置的前缀是 facebook,而不是 security.oauth2

application.yml

 
   
   
 
  1. facebook:

  2.  client:

  3.    clientId: 233668646673605

  4.    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d

  5.    accessTokenUri: https://graph.facebook.com/oauth/access_token

  6.    userAuthorizationUri: https://www.facebook.com/dialog/oauth

  7.    tokenName: oauth_token

  8.    authenticationScheme: query

  9.    clientAuthenticationScheme: form

  10.  resource:

  11.    userInfoUri: https://graph.facebook.com/me

最后,我们改变了登录的路径,在上面的 Filter声明中是特定于Facebook的,所以我们需要在HTML中做同样的变更:

index.html

 
   
   
 
  1. <h1>Login</h1>

  2. <div class="container unauthenticated">

  3.    <div>

  4.    With Facebook: <a href="/login/facebook">click here</a>

  5.    </div>

  6. </div>

处理重定向

我们需要做的最后一个改变是明确地支持从应用程序到Facebook的重定向。这是通过Spring OAuth2中的一个Servlet 过滤器处理的,并且这个过滤器已经在应用程序上下文中使用了,因为我们使用了 @EnableOAuth2Client。我们所需要的就是将过滤器连接起来,以便在Spring Boot应用程序中以正确的顺序调用它。要做到这一点,我们需要一个 FilterRegistrationBean

SocialApplication.java

 
   
   
 
  1. @Bean

  2. public FilterRegistrationBean oauth2ClientFilterRegistration(

  3.    OAuth2ClientContextFilter filter) {

  4.  FilterRegistrationBean registration = new FilterRegistrationBean();

  5.  registration.setFilter(filter);

  6.  registration.setOrder(-100);

  7.  return registration;

  8. }

我们注入已经存在并且可用的过滤器,并且以足够低的顺序确保能够在主Spring Security过滤器之前执行。通过这种方式,我们可以使用它来处理在身份认证请求,并使用 expceptions来进行重定向。

随着这些更改就位,应用程序就可以很好地运行,并且在运行时就相当于我们在上一节中构建的 logout示例。将配置分解并使其显式告诉我们,Spring Boot所做的事情并没有什么神奇之处(它只是用来集中存放配置的),它还准备了我们的应用程序,以扩展自动提供的特性,添加我们自己的一些特性和业务需求。

使用Github登陆

在本节中,我们修改了上面创建的 app ,添加了一个链接,这样用户就可以选择使用Github进行身份认证吧,除了已存在的跳转到Facebook的链接。

增加Github链接

在客户端,我们只需要很小的改动,只是新增了一个链接:

index.html

 
   
   
 
  1. <div class="container unauthenticated">

  2.  <div>

  3.    With Facebook: <a href="/login/facebook">click here</a>

  4.  </div>

  5.  <div>

  6.    With Github: <a href="/login/github">click here</a>

  7.  </div>

  8. </div>

原则上,一旦开始添加认证Provider,我们可能需要更加小心从“/user” 端点返回的数据。事实证明,Github和Facebook在用户信息中都有一个“name”字段,因此在实践中,我们的简单 端点没有任何变化。

增加Github身份认证过滤器

服务器上的主要更改是添加额外的安全过滤器来处理来自新链接的 /login/github请求。我们已经在 ssoFilter()方法中为Facebook创建了一个自定义的身份认证过滤器,因此我们需要做的是用一个能够处理多个身份认证路径的 CompositeFilter来替换它:

SocialApplication.java

 
   
   
 
  1. private Filter ssoFilter() {

  2.  CompositeFilter filter = new CompositeFilter();

  3.  List<Filter> filters = new ArrayList<>();

  4.  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");

  5.  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);

  6.  facebookFilter.setRestTemplate(facebookTemplate);

  7.  UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());

  8.  tokenServices.setRestTemplate(facebookTemplate);

  9.  facebookFilter.setTokenServices(tokenServices);

  10.  filters.add(facebookFilter);

  11.  OAuth2ClientAuthenticationProcessingFilter githubFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/github");

  12.  OAuth2RestTemplate githubTemplate = new OAuth2RestTemplate(github(), oauth2ClientContext);

  13.  githubFilter.setRestTemplate(githubTemplate);

  14.  tokenServices = new UserInfoTokenServices(githubResource().getUserInfoUri(), github().getClientId());

  15.  tokenServices.setRestTemplate(githubTemplate);

  16.  githubFilter.setTokenServices(tokenServices);

  17.  filters.add(githubFilter);

  18.  filter.setFilters(filters);

  19.  return filter;

  20. }

相关的配置代码在以前的 ssoFilter()方法中已经重复了,一次是在Facebook上,一次是在Github上,因此需要使用 CompositeFilter把这两个过滤器合并一下。

注意, facebook()和 facebookResource()方法和 github()和 githubResource()方法有重复代码:

SocialApplication.java

 
   
   
 
  1. @Bean

  2. @ConfigurationProperties("github.client")

  3. public AuthorizationCodeResourceDetails github() {

  4.    return new AuthorizationCodeResourceDetails();

  5. }

  6. @Bean

  7. @ConfigurationProperties("github.resource")

  8. public ResourceServerProperties githubResource() {

  9.    return new ResourceServerProperties();

  10. }

相应的配置:

application.yml

 
   
   
 
  1. github:

  2.  client:

  3.    clientId: bd1c0a783ccdd1c9b9e4

  4.    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1

  5.    accessTokenUri: https://github.com/login/oauth/access_token

  6.    userAuthorizationUri: https://github.com/login/oauth/authorize

  7.    clientAuthenticationScheme: form

  8.  resource:

  9.    userInfoUri: https://api.github.com/user

在Github注册客户端时,也使用 localhost:8080(与Facebook一样)。

现在app应用程序已经可以运行了,用户可以选择在Facebook或Github上进行认证。

如何使用本地用户

许多应用程序需要在本地保存有关其用户的数据,即使身份认证被委托给外部Provider。在这里我们没有展示代码,但在通过下面两个步骤很容易做到。

  1. 为您的数据库选择一个后端,并建立一些可以从外部认证填充全部或部分到自定义 User对象的 Repository(例如,使用Spring Data)。

  2. 为每个独立的用户提供一个 User对象,通过 /user端点中的 Repository完成登陆操作。如果已经存在具有当前 Principal身份的用户,则可以更新,否则创建。

提示:
在 User对象中添加一个字段以链接到外部Provider中的唯一标识符(不是用户名,而是外部Provider用户中特有的唯一标识)。

托管AuthorizationServer

在本节中,我们修改了前面构建的github 应用程序,使该应用程序具备创建自己的 Access Token能力的完整的OAuth2 AuthorizationServer,不过仍然使用Facebook和Github进行身份认证。然后,这些 Token可以用来保护后端资源,或者使用我们碰巧需要以相同方式保护的其他应用程序来实现SSO。

整理身份认证的配置

在开始使用 AuthorizationServer特性之前,我们将整理两个外部 Provider的配置代码。在 ssoFilter()方法中有一些重复的代码,因此我们将其抽取出来进行复用:

SocialApplication.java

 
   
   
 
  1. private Filter ssoFilter() {

  2.  CompositeFilter filter = new CompositeFilter();

  3.  List<Filter> filters = new ArrayList<>();

  4.  filters.add(ssoFilter(facebook(), "/login/facebook"));

  5.  filters.add(ssoFilter(github(), "/login/github"));

  6.  filter.setFilters(filters);

  7.  return filter;

  8. }

此处复用了构建Facebook 过滤器的所有代码,即 ssoFilter(ClientResourcesclient,Stringpath),如下所示:

SocialApplication.java

 
   
   
 
  1. private Filter ssoFilter(ClientResources client, String path) {

  2.  OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);

  3.  OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);

  4.  filter.setRestTemplate(template);

  5.  UserInfoTokenServices tokenServices = new UserInfoTokenServices(

  6.      client.getResource().getUserInfoUri(), client.getClient().getClientId());

  7.  tokenServices.setRestTemplate(template);

  8.  filter.setTokenServices(tokenServices);

  9.  return filter;

  10. }

并且它使用一个新的包装器对象 ClientResources,该对象合并了 OAuth2ProtectedResourceDetails和 ResourceServerProperties,它们在应用程序的最后一个版本中被声明为单独的@Beans

SocialApplication.java

 
   
   
 
  1. class ClientResources {

  2.  @NestedConfigurationProperty

  3.  private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

  4.  @NestedConfigurationProperty

  5.  private ResourceServerProperties resource = new ResourceServerProperties();

  6.  public AuthorizationCodeResourceDetails getClient() {

  7.    return client;

  8.  }

  9.  public ResourceServerProperties getResource() {

  10.    return resource;

  11.  }

  12. }

包装器使用 @NestedConfigurationProperty来告诉注解处理器为该类型生成单独的一个属性组,因为它不代表单个值,而是一个完整的嵌套类型。

有了这个包装器,我们就可以像以前一样使用相同的YAML配置,但是对于每个 Provider都有一个单独的方法:

SocialApplication.java

 
   
   
 
  1. @Bean

  2. @ConfigurationProperties("github")

  3. public ClientResources github() {

  4.  return new ClientResources();

  5. }

  6. @Bean

  7. @ConfigurationProperties("facebook")

  8. public ClientResources facebook() {

  9.  return new ClientResources();

  10. }

搭建AuthorizationServer

如果想要将我们的应用程序转换为一个OAuth2 AuthorizationServer,那么没有太多的麻烦和仪式,至少要开始一些基本的特性(一个客户端和创建 AccessToken的能力)。 AuthorizationServer只不过是一堆端点,它们在Spring OAuth2中作为Spring MVC handler的实现。我们已经有了一个安全的应用程序,所以实际上只是添加了 @EnableAuthorizationServer注解:

SocialApplication.java

 
   
   
 
  1. @SpringBootApplication

  2. @RestController

  3. @EnableOAuth2Client

  4. @EnableAuthorizationServer

  5. public class SocialApplication extends WebSecurityConfigurerAdapter {

  6.   ...

  7. }

有了这个新的注解,Spring Boot将配置好所有必要的 端点并为它们设置安全性,前提是我们提供了我们想要支持的OAuth2客户端的一些细节:

application.yml

 
   
   
 
  1. security:

  2.  oauth2:

  3.    client:

  4.      client-id: acme

  5.      client-secret: acmesecret

  6.      scope: read,write

  7.      auto-approve-scopes: '.*'

这个客户端相当于我们需要外部认证的客户端 facebook.client*和 github.client*。有了外部Provider,我们必须注册并获得一个在应用中使用的客户ID和秘钥。在这种情况下,我们提供相同的特性,所以需要(至少一个)客户端来工作。

我们将 auto-approve-scopes设置为匹配所有范围的正则表达式。在生产应用中,我们肯定不会这样做,但是它可以让示例程序快速的跑起来,并且不会破坏Spring OAuth2在需要 AccessToken时为用户弹出的 whitelabel授权页面。为了向 Token授权添加明确的授权操作,我们需要提供一个UI来替换 whitelabel版本(在 /oauth/confirm_access处)。

为了完成 AuthorizationServer,我们只需要为它的UI提供安全配置。事实上,在这个简单的应用程序中,没有太多的用户界面,但是我们仍然需要保护 /oauth/authorize端点,并确保带有"Login" 按钮的主页是可见的。这就是为什么我们有这样的方法:

 
   
   
 
  1. @Override

  2. protected void configure(HttpSecurity http) throws Exception {

  3.  http.antMatcher("/**")                                       (1)

  4.    .authorizeRequests()

  5.      .antMatchers("/", "/login**", "/webjars/**").permitAll() (2)

  6.      .anyRequest().authenticated()                            (3)

  7.      .and()

  8.      .exceptionHandling()

  9.      .authenticationEntryPoint(new  LoginUrlAuthenticationEntryPoint("/")) (4)

  10.    ...

  11. }

1、默认情况下,所有请求都受到保护 
2、主页和登录端点被显式地排除,即所有用户都可以就可以访问这两个端点 
3、所有其他端点都只可以被经过认证的用户访问 
4、没有经过认证的用户在访问被保护的端点时,会被重定向到主页

如何获取一个AccessToken

AccessToken现在可以从新的 AuthorizationServer中获得。到目前为止,获得 Token的最简单方法是把一个作为"acme"应用的客户端。如果运行应用程序并使用 curl来获取 AccessToken,你就可以看到这一点:

 
   
   
 
  1. $ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=client_credentials

  2. {"access_token":"370592fd-b9f8-452d-816a-4fd5c6b4b8a6","token_type":"bearer","expires_in":43199,"scope":"read write"}

客户端凭据 token在某些情况下是有用的(比如 token端点是否正常的测试),但是为了利用服务器的所有特性,我们希望能够为用户创建 token。要获得代表我们的应用程序用户的 token,我们需要能够对用户进行认证。如果在应用程序启动时仔细查看日志,您将会看到为Spring Boot默认用户生成的随机密码(根据Spring Boot用户指南)。您可以使用这个密码来代表用户使用用户名“user”来获得一个 Token

 
   
   
 
  1. $ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=...

  2. {"access_token":"aa49e025-c4fe-4892-86af-15af2e6b72a2","token_type":"bearer","refresh_token":"97a9f978-7aad-4af7-9329-78ff2ce9962d","expires_in":43199,"scope":"read write"}

上面 curl命令中使用"…"的地方,需要被替换成真正的密码。这种授权方式被称为“密码”授权,在这种授权类型下,你可以通过用户名和密码来换取一个 AccessToken

密码授权也主要用于测试,但是对于本地或移动应用程序,当您有本地用户数据库来存储和验证凭据时,它可能适合。对于大多数应用程序,或者任何具有“社交”登录的应用程序,比如我们的应用程序,您都需要"authorization code"授权,这意味着您需要一个浏览器(或表现得像浏览器的客户端)来处理重定向和cookie,以及呈现来自外部提供者的用户界面。

创建一个应用程序客户端

我们的 AuthorizationServer本身是一个Web应用程序的客户端应用程序,所以使用Spring Boot很容易创建。下面是一个例子:

ClientApplication.java

 
   
   
 
  1. @EnableAutoConfiguration

  2. @Configuration

  3. @EnableOAuth2Sso

  4. @RestController

  5. public class ClientApplication {

  6.  @RequestMapping("/")

  7.  public String home(Principal user) {

  8.    return "Hello " + user.getName();

  9.  }

  10.  public static void main(String[] args) {

  11.    new SpringApplicationBuilder(ClientApplication.class)

  12.        .properties("spring.config.name=client").run(args);

  13.  }

  14. }

ClientApplication类一定不能在 SocialApplication类的同一个包(或子包)中创建。否则,Spring会在启动 SocialApplication服务器时加载一些关于 ClientApplication的自动配置,从而导致启动错误。

客户端只包含一个主页(用来显示用户的名字),以及通过配置文件显式地给这个应用配置一个名称(通过 spring.config.name=client来配置)。当我们运行这个应用程序时,它会寻找一个我们提供的配置文件,如下所示:

client.yml

 
   
   
 
  1. server:

  2.  port: 9999

  3.  context-path: /client

  4. security:

  5.  oauth2:

  6.    client:

  7.      client-id: acme

  8.      client-secret: acmesecret

  9.      access-token-uri: http://localhost:8080/oauth/token

  10.      user-authorization-uri: http://localhost:8080/oauth/authorize

  11.    resource:

  12.      user-info-uri: http://localhost:8080/me

这个配置看起来很像我们在主应用程序中使用的值,但是使用 acme客户端而不是Facebook或Github客户端。应用程序将在端口9999上运行,以避免与主应用程序发生冲突。它指的是我们还没有实现的用户信息端点 /me

注意, server.context-path是显式设置的,因此如果您运行应用程序来测试它,请记住主页是http://localhost:9999/client。单击该链接会跳转到认证服务器,一旦您与您选择的社交 Provider完成了身份认证,您将被重定向回客户端应用程序。

如果您在本地主机上同时运行客户端和 AuthorizationServer,那么上下文路径必须被显式指定,否则会与 cookie路径冲突,并且两个应用程序也无法在 SessionId上达成一致。

保护用户信息的端点

要使用我们的新 AuthorizationServer进行单点登录,就像我们一直在使用Facebook和Github一样,它需要有一个 /user端点,该端点受到它所创建的 AccessToken的保护。到目前为止,我们已经有了一个 /user端点,并在用户验证时创建了 Cookie。为了用本地授予的 AccessToken来保护它,我们只需重新使用现有的端点,并在新路径上创建别名即可:

SocialApplication.java

 
   
   
 
  1. @RequestMapping({ "/user", "/me" })

  2. public Map<String, String> user(Principal principal) {

  3.  Map<String, String> map = new LinkedHashMap<>();

  4.  map.put("name", principal.getName());

  5.  return map;

  6. }

现在,可以通过声明我们的应用程序是 ResourceServer(以及 AuthorizationServer)来使用 AccessToken保护“/me”端点。我们创建了一个新的配置类(作为主应用程序中的n个内部类,但它也可以被分割成一个个单独的独立类):

SocialApplication.java

 
   
   
 
  1. @Configuration

  2. @EnableResourceServer

  3. protected static class ResourceServerConfiguration

  4.    extends ResourceServerConfigurerAdapter {

  5.  @Override

  6.  public void configure(HttpSecurity http) throws Exception {

  7.    http

  8.      .antMatcher("/me")

  9.      .authorizeRequests().anyRequest().authenticated();

  10.  }

  11. }

另外,我们需要为主应用程序安全Java配置指定一个 @Order

SocialApplication.java

 
   
   
 
  1. @SpringBootApplication

  2. ...

  3. @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)

  4. public class SocialApplication extends WebSecurityConfigurerAdapter {

  5.  ...

  6. }

默认情况下, @EnableResourceServer注解创建了一个带有 @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER-1)的安全过滤器,因此只要给主应用程序安全Java配置添加注解 @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER),就可以确保“/me”的优先级更高。

测试OAuth2客户端

要测试这些新特性,你可以在浏览器中运行两个应用程序和访问http://localhost:9999/client 。客户端应用程序将重定向到本地 AuthorizationServer,然后给用户提供与Facebook或Github进行身份认证的常规选择。一旦完整的控制权流转到测试客户端,就授予本地 AccessToken并完成身份认证(您应该可以在浏览器中看到“Hello”)。如果您已经通过Github或Facebook进行了身份认证,那么就暂时不再需要进行认证操作了。

给未通过身份认证的用户添加一个错误页

在本节中,我们修改了之前构建的logout应用程序,切换到使用Github Provider进行身份认证,并且还可能对不能完成身份认证的用户提供一些反馈。同时,我们借此机会扩展了身份认证逻辑,使其包含一个规则,该规则只允许用户隶属于特定的Github组织。“组织”是特定于Github中一个的概念,但是也可以为其他 Provider提供类似的规则,例如,使用Google,您可能只希望对来自特定域的用户进行身份认证。

切换到Github

logout 示例使用Facebook作为OAuth2 Provider。我们可以通过改变本地配置就轻松地切换到Github:

application.yml

 
   
   
 
  1. security:

  2.  oauth2:

  3.    client:

  4.      clientId: bd1c0a783ccdd1c9b9e4

  5.      clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1

  6.      accessTokenUri: https://github.com/login/oauth/access_token

  7.      userAuthorizationUri: https://github.com/login/oauth/authorize

  8.      clientAuthenticationScheme: form

  9.    resource:

  10.      userInfoUri: https://api.github.com/user

在客户端检测身份认证失败

在客户端,我们需要能够为无法通过认证的用户提供一些反馈。为了便于使用,我们添加了一个带有提示信息的 div

index.html

 
   
   
 
  1. <div class="container text-danger error" style="display:none">

  2. There was an error (bad credentials).

  3. </div>

只有当需要显示 class属性包含 .error的html元素时,才会显示这些文本,因此我们需要写一些代码来完成这个功能:

index.html

 
   
   
 
  1. $.ajax({

  2.  url : "/user",

  3.  success : function(data) {

  4.    $(".unauthenticated").hide();

  5.    $("#user").html(data.userAuthentication.details.name);

  6.    $(".authenticated").show();

  7.  },

  8.  error : function(data) {

  9.    $("#user").html('');

  10.    $(".unauthenticated").show();

  11.    $(".authenticated").hide();

  12.    if (location.href.indexOf("error=true")>=0) {

  13.      $(".error").show();

  14.    }

  15.  }

  16. });

这个身份认证函数在加载时检查 location属性的 href值,如果发现URL中包含“error=true”,则会将会显示 class属性包含 .error的html元素。

添加一个展示错误的页面

为了在客户端中支持设置标识,我们需要能够捕获身份认证的错误,并使用查询参数中设置的标识重定向到主页。因此,我们需要一个端点,在就像下面这样的一个常规 @Controller

SocialApplication.java

 
   
   
 
  1. @RequestMapping("/unauthenticated")

  2. public String unauthenticated() {

  3.  return "redirect:/?error=true";

  4. }

在示例应用程序中,我们将其放入主应用程序类中,该类现在是 @Controller (不是 @RestController),因此它可以处理重定向。我们需要做的最后一件事,就是添加从一个未经身份验证的响应( HTTP401,又名 UNAUTHORIZED)到刚刚添加的 /unauthenticated端点的映射:

ServletCustomizer.java

 
   
   
 
  1. @Configuration

  2. public class ServletCustomizer {

  3.  @Bean

  4.  public EmbeddedServletContainerCustomizer customizer() {

  5.    return container -> {

  6.      container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated"));

  7.    };

  8.  }

  9. }

(在示例中,为了简洁,会被添加到主应用程序内部的嵌套类中)。

在服务器生成一个401响应

如果用户不能或不想使用Github登录,则Spring Security已经提供了 401响应,因此如果无法进行身份认证(例如,通过拒绝 token授权),那么这个应用程序就已经满足要求了。

为了让事情变得更有趣味,我们将扩展认证规则来拒绝那些不在正确组织中的用户。使用Github API很容易找到关于用户的更多信息,因此我们只需要将这个逻辑嵌入到身份认证过程的正确部分。幸运的是,对于这样一个简单的用例,Spring Boot提供了一个简单的扩展点:如果我们声明一个 AuthoritiesExtractor类型的 @Bean,那么它将用于构造经过身份认证的用户的权限(通常是“角色”)。我们可以使用这个钩子来断言用户是在正确的组织中,如果没有则抛出异常:

SocialApplication.java

 
   
   
 
  1. @Bean

  2. public AuthoritiesExtractor authoritiesExtractor(OAuth2RestOperations template) {

  3.  return map -> {

  4.    String url = (String) map.get("organizations_url");

  5.    @SuppressWarnings("unchecked")

  6.    List<Map<String, Object>> orgs = template.getForObject(url, List.class);

  7.    if (orgs.stream()

  8.        .anyMatch(org -> "spring-projects".equals(org.get("login")))) {

  9.      return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");

  10.    }

  11.    throw new BadCredentialsException("Not in Spring Projects origanization");

  12.  };

  13. <