本指南将向您展示如何使用OAuth2Spring Boot通过“社交登录”构建一个样例应用程序来执行各种操作。它以简单的单一提供商单点登录开始,并且可以与带有身份验证提供程序( FacebookGithub )的自托管OAuth2授权服务器一起使用。这些示例都是在后端使用Spring Boot和Spring OAuth的所有单页应用程序。它们都在前端都使用普通的jQuery ,但是转换为其他JavaScript框架或使用服务器端呈现所需的更改将很小。

因为其中一个示例是完整的OAuth2授权服务器,所以我们使用了填充JAR ,它支持从Spring Boot 2.0到旧的Spring Security OAuth2库的桥接。还可以使用Spring Boot安全功能中的本机OAuth2支持来实现更简单的示例。配置非常相似。

有几个相互补充的示例,它们增加了新功能:

  • 简单 :一个非常基本的静态应用,只有一个主页,并通过Spring Boot的无条件登录@EnableOAuth2Sso (如果您访问主页,您将被自动重定向到Facebook)。

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

  • 注销 :还为经过身份验证的用户添加注销链接。

  • 手册 :显示如何@EnableOAuth2Sso通过取消选择并手动配置所有组件来工作。

  • github :在Github中添加了第二个登录提供程序,因此用户可以在主页上选择要使用的登录提供程序。

  • auth-server :将应用程序转换为成熟的OAuth2授权服务器,可以发布自己的令牌,但仍使用外部OAuth2提供程序进行身份验证。

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

可以在源代码中跟踪从功能阶梯中的一个应用迁移到下一个应用所需的更改(源代码在Github中 )。存储库中的前6个更改正在转换一个应用程序,因此您可以轻松地看到它们之间的差异。您可能会在应用程序的早期提交和指南中看到的最终状态之间看到任何进一步的差异,这都是表面上的。

它们每个都可以导入到IDE中,并且有一个主类SocialApplication您可以在那里运行以启动应用程序。他们都提供了一个位于http:// localhost:8080的主页(如果要登录并查看内容,都要求您至少拥有一个Facebook帐户)。您还可以使用以下命令在命令行上运行所有应用程序mvn spring-boot:run或通过构建jar文件并运行mvn packagejava -jar target/*.jar (根据Spring Boot文档和其他可用文档 )。如果您在顶层使用包装器 ,则无需安装Maven,例如

$ cd simple
$ ../mvnw package
$ java -jar target/*.jar
所有的应用程序都可以运行localhost:8080因为他们使用在Facebook和Github上注册的OAuth2客户端作为该地址。要在不同的主机或端口上运行它们,您需要注册自己的应用程序并将凭据放入配置文件中。如果使用默认值,则不会有将您的Facebook或Github凭据泄漏到本地主机之外的危险,但是请注意在Internet上公开的内容,并且不要将自己的应用程序注册置于公共源代码控制中。

使用Facebook单一登录

在本节中,我们创建一个使用Facebook进行身份验证的最小应用程序。如果我们利用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”。

添加主页

在您的新项目中创建一个index.html在“ src / main / resources / static”文件夹中。您应该添加一些样式表和Java脚本链接,以便结果如下所示:

index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>Demo</title>
    <meta name="description" content=""/>
    <meta name="viewport" content="width=device-width"/>
    <base href="/"/>
    <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
	<h1>Demo</h1>
	<div class="container"></div>
</body>
</html>

演示OAuth2登录功能不需要这样做,但是我们希望最终拥有一个漂亮的UI,因此我们不妨从主页中的一些基本内容开始。

如果启动应用程序并加载主页,您会注意到尚未加载样式表。因此,我们也需要添加它们,并且可以通过添加一些依赖项来做到这一点:

pom.xml
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>2.1.1</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.2.0</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>

我们添加了Twitter引导程序和jQuery(这是我们现在所需要的)。另一个依赖项是webjars“定位器”,由webjars站点作为库提供,并且Spring可以使用它来定位webjars中的静态资产,而无需知道确切的版本(因此无版本) /webjars/**中的链接index.html )。只要您不关闭MVC自动配置,默认情况下就会在Spring Boot应用程序中激活webjar定位器。

完成这些更改后,我们应该为我们的应用程序提供一个漂亮的主页。

保护应用程序

为了使应用程序安全,我们只需要添加Spring Security作为依赖项即可。如果这样做的话,默认值将是使用HTTP Basic对其进行保护,因此由于我们要进行“社交”登录(委托给Facebook),因此我们还添加了Spring Security OAuth2依赖项:

pom.xml
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security.oauth.boot</groupId>
	<artifactId>spring-security-oauth2-autoconfigure</artifactId>
	<version>2.0.0.RELEASE</version>
</dependency>

要链接到Facebook,我们需要一个@EnableOAuth2Sso在主类上的注释:

SocialApplication.java
@SpringBootApplication
@EnableOAuth2Sso
public class SocialApplication {

  ...

}

和一些配置(转换application.properties到YAML,以提高可读性):

application.yml
security:
  oauth2:
    client:
      clientId: 233668646673605
      clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
      accessTokenUri: https://graph.facebook.com/oauth/access_token
      userAuthorizationUri: https://www.facebook.com/dialog/oauth
      tokenName: oauth_token
      authenticationScheme: query
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://graph.facebook.com/me
...

该配置指的是在其开发人员站点中向Facebook注册的客户端应用,您必须在其中提供该应用的注册重定向(主页)。此地址已注册到“ localhost:8080”,因此仅在在该地址上运行的应用程序中有效。

进行此更改后,您可以再次运行该应用程序,并访问位于http:// localhost:8080的主页。除了首页以外,您还应该重定向到使用Facebook登录。如果这样做,并接受要求您进行的任何授权,您将被重定向回本地应用程序,并且主页将可见。如果您保持登录状态,即使您在没有Cookie和缓存数据的全新浏览器中打开该应用程序,也不必使用该本地应用程序重新进行身份验证。(这就是“单点登录”的意思。)

如果您正在使用示例应用程序来完成本节,请确保清除浏览器的cookie和HTTP Basic凭据缓存。在Chrome中,对单个服务器执行此操作的最佳方法是打开一个新的隐身窗口。

授予对此示例访问权限是安全的,因为只有在本地运行的应用程序才能使用令牌,并且它要求的范围是有限的。但是,当您登录这样的应用程序时,请注意您正在批准的内容:他们可能会要求您做比您满意的事情(例如,他们可能会要求您更改您的个人数据,这不太可能出现在您的个人资料中)利益)。

刚刚发生了什么?

您刚刚以OAuth2术语编写的应用程序是一个客户端应用程序,它使用授权代码授予从Facebook(授权服务器)获取访问令牌。然后,它使用访问令牌向Facebook询问一些个人详细信息(仅包括您允许的操作),包括您的登录ID和名称。在此阶段,facebook充当资源服务器,对您发送的令牌进行解码并检查它是否授予应用程序访问用户详细信息的权限。如果该过程成功,则该应用会将用户详细信息插入到Spring Security上下文中,以便对您进行身份验证。

如果您在浏览器工具(Chrome上为F12)中查找并跟踪所有跃点的网络流量,则将看到与Facebook来回重定向,最后,您将看到一个带有新内容的主页。 Set-Cookie标头。这个Cookie( JSESSIONID默认情况下)是您的Spring(或任何基于servlet的)应用程序身份验证详细信息的令牌。

因此,我们拥有一个安全的应用程序,从某种意义上来说,它必须查看用户必须向外部提供商(Facebook)进行身份验证的任何内容。我们不希望将其用于互联网银行网站,而是出于基本标识的目的,并在您网站的不同用户之间隔离内容,这是一个很好的起点,这说明了为什么这种身份验证在当今非常流行。在下一节中,我们将向该应用程序添加一些基本功能,并使用户将初始重定向到Facebook时所发生的情况更加清楚。

添加欢迎页面

在本节中,我们通过添加显式链接来登录Facebook来修改我们刚刚构建的简单应用程序。新链接将立即显示在主页上,而不是立即重定向,用户可以选择登录或不进行身份验证。仅当用户单击链接时,才会向他显示安全内容。

主页中的条件内容

为了使某些内容取决于用户是否通过身份验证,我们可以使用服务器端渲染(例如,使用Freemarker或Tymeleaf),或者我们可以使用一些JavaScript要求浏览器访问它。为此,我们将使用AngularJS ,但是如果您喜欢使用其他框架,则翻译客户端代码应该不会很困难。

要开始使用动态内容,我们需要标记HTML的一部分以显示它:

index.html
<div class="container unauthenticated">
    With Facebook: <a href="/login">click here</a>
</div>
<div class="container authenticated" style="display:none">
    Logged in as: <span id="user"></span>
</div>

此HTML使我们需要一些可操作authenticatedunauthenticateduser元素。这是这些功能的简单实现(将其放在 ):

index.html
<script type="text/javascript">
    $.get("/user", function(data) {
        $("#user").html(data.userAuthentication.details.name);
        $(".unauthenticated").hide()
        $(".authenticated").show()
    });
</script>

服务器端更改

为此,我们需要在服务器端进行一些更改。“ home”控制器需要位于“ / user”的端点,该端点描述了当前已认证的用户。这很容易做到,例如在我们的主课中:

社会应用
@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class SocialApplication {

  @RequestMapping("/user")
  public Principal user(Principal principal) {
    return principal;
  }

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

}

注意使用@RestController@RequestMappingjava.security.Principal我们注入处理程序方法。

归还全部不是一个好主意Principal在一个/user这样的端点(它可能包含您不希望向浏览器客户端显示的信息)。我们这样做只是为了使某些东西快速工作。在指南的后面,我们将转换端点以隐藏不需要浏览器的信息。

该应用程序现在可以像以前一样正常工作并进行身份验证,但是不会给用户提供机会单击我们刚刚提供的链接。为了使链接可见,我们还需要通过添加一个首页来关闭主页上的安全性。 WebSecurityConfigurer

社会应用
@SpringBootApplication
@EnableOAuth2Sso
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/**")
      .authorizeRequests()
        .antMatchers("/", "/login**", "/webjars/**", "/error**")
        .permitAll()
      .anyRequest()
        .authenticated();
  }

}

Spring Boot赋予了特殊含义WebSecurityConfigurer在上课的@EnableOAuth2Sso注解:它使用它来配置承载OAuth2身份验证处理器的安全过滤器链。因此,要使主页可见,我们需要做的就是明确authorizeRequests()到主页及其包含的静态资源(我们还包括对处理身份验证的登录端点的访问)。所有其他要求(例如/user端点)要求身份验证。

/error**是不受保护的路径,因为我们希望Spring Boot能够在应用程序出现问题时呈现错误,即使用户未经身份验证也是如此。

完成该更改后,应用程序就完成了,如果您运行它并访问主页,您应该会看到一个漂亮的HTML链接,以“使用Facebook登录”。该链接将您直接带到Facebook,而不是直接带到处理身份验证的本地路径(并将重定向发送到Facebook)。通过身份验证后,您将重定向回本地应用程序,该应用程序现在将在其中显示您的姓名(假设您已在Facebook中设置了权限以允许访问该数据)。

添加注销按钮

在本部分中,我们通过添加允许用户退出应用程序的按钮来修改我们构建的click应用程序。这似乎是一个简单的功能,但是实现时需要一些注意,因此值得花一些时间来讨论确切的操作方法。大多数更改与以下事实有关:我们正在将应用程序从只读资源转换为可读写资源(注销需要状态更改),因此,在任何实际的应用程序中都需要进行相同的更改。不只是静态内容。

客户端更改

在客户端上,我们只需要提供一个注销按钮和一些JavaScript即可回调服务器以请求取消身份验证。首先,在用户界面的“已验证”部分中,添加按钮:

index.html
<div class="container authenticated">
  Logged in as: <span id="user"></span>
  <div>
    <button onClick="logout()" class="btn btn-primary">Logout</button>
  </div>
</div>

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

index.html
var logout = function() {
    $.post("/logout", function() {
        $("#user").html('');
        $(".unauthenticated").show();
        $(".authenticated").hide();
    })
    return true;
}

logout()函数执行POST /logout然后清除动态内容。现在,我们可以切换到服务器端以实现该端点。

添加注销端点

Spring Security内置了对/logout终结点,它将为我们做正确的事情(清除会话并使cookie无效)。要配置端点,我们只需扩展现有端点configure()我们的方法WebSecurityConfigurer

SocialApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().logout().logoutSuccessUrl("/").permitAll();
}

/logout端点要求我们对其进行POST,并且为了保护用户免受跨站点请求伪造(CSRF,发音为“海上冲浪”)的影响,它要求在请求中包含令牌。令牌的值链接到提供保护的当前会话,因此我们需要一种方法来将这些数据导入我们的JavaScript应用程序。

许多JavaScript框架都内置了对CSRF的支持(例如,在Angular中称其为XSRF),但是通常以与Spring Security的开箱即用行为稍有不同的方式实现。例如,在Angular中,前端希望服务器向其发送一个名为“ XSRF-TOKEN”的cookie,如果看到该cookie,它将把值作为名为“ X-XSRF-TOKEN”的标头发送回去。我们可以使用简单的jQuery客户端实现相同的行为,然后服务器端的更改将与其他前端实现一起使用,而无需更改或更改很少。为了向Spring Security教授这一点,我们需要添加一个创建cookie的过滤器,并且还需要告知现有CRSF过滤器有关标头名称的信息。在里面WebSecurityConfigurer

SocialApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")
    ... // existing code here
    .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}

在客户端中添加CSRF令牌

由于在此示例中我们没有使用更高级别的框架,因此我们需要显式添加CSRF令牌,该令牌可以从后端作为cookie使用。为了使代码更简单,我们包括一个附加的库:

pom.xml
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>js-cookie</artifactId>
    <version>2.1.0</version>
</dependency>

将其导入HTML:

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

然后我们可以使用Cookies xhr中的便捷方法:

index.html
$.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'));
    }
  }
}
});

准备推出!

完成这些更改后,我们就可以运行该应用程序并尝试新的注销按钮。启动应用程序并将主页加载到新的浏览器窗口中。单击“登录”链接以将您带到Facebook(如果您已经在那登录,则可能不会注意到重定向)。单击“注销”按钮以取消当前会话,并使应用程序返回未经身份验证的状态。如果您感到好奇,则应该能够在浏览器与本地服务器交换的请求中看到新的cookie和标头。

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

手动配置OAuth2客户端

在本节中,我们修改注销我们已经采摘除了“魔术”,在内置应用@EnableOAuth2Sso注释,手动配置其中的所有内容使其明确。

客户端和认证

背后有2个功能@EnableOAuth2Sso :OAuth2客户端和身份验证。客户端是可重用的,因此您也可以使用它与授权服务器(在这种情况下为Facebook)提供的OAuth2资源(在此情况下为Graph API )进行交互。身份验证块使您的应用程序与Spring Security的其余部分保持一致,因此一旦与Facebook发生冲突后,您的应用程序的行为就与其他任何安全的Spring应用程序完全一样。

客户端由Spring Security OAuth2提供,并通过其他注释打开@EnableOAuth2Client 。因此,此转换的第一步是删除@EnableOAuth2Sso并将其替换为较低级别的注释:

社会应用
@SpringBootApplication
@EnableOAuth2Client
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {
  ...
}

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

社会应用
@SpringBootApplication
@EnableOAuth2Client
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {

  @Autowired
  OAuth2ClientContext oauth2ClientContext;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/**")
      ...
      .and().addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
  }

  ...

}

此过滤器是使用新方法创建的OAuth2ClientContext

社会应用
private Filter ssoFilter() {
  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
  tokenServices.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(tokenServices);
  return facebookFilter;
}

筛选器还需要了解客户端在Facebook上的注册:

社会应用
  @Bean
  @ConfigurationProperties("facebook.client")
  public AuthorizationCodeResourceDetails facebook() {
    return new AuthorizationCodeResourceDetails();
  }

并完成身份验证,它需要知道用户信息端点在Facebook中的位置:

社会应用
  @Bean
  @ConfigurationProperties("facebook.resource")
  public ResourceServerProperties facebookResource() {
    return new ResourceServerProperties();
  }

请注意,使用这两个“静态”数据对象( facebook()facebookResource() )我们使用了@Bean装饰为@ConfigurationProperties 。这意味着我们可以将application.yml改为新的格式,其中配置前缀为facebook代替security.oauth2

application.yml
facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me

最后,我们将登录路径更改为特定于Facebook的登录名Filter上面的声明,因此我们需要在HTML中进行相同的更改:

index.html
<h1>Login</h1>
<div class="container unauthenticated">
	<div>
	With Facebook: <a href="/login/facebook">click here</a>
	</div>
</div>

处理重定向

我们需要做的最后一个更改是显式支持从应用程序到Facebook的重定向。在Spring OAuth2中使用Servlet处理Filter ,并且该过滤器已在应用程序上下文中可用,因为我们使用了@EnableOAuth2Client 。所需要做的就是连接过滤器,以便在我们的Spring Boot应用程序中以正确的顺序调用它。为此,我们需要一个FilterRegistrationBean

SocialApplication.java
@Bean
	public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
		FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
		registration.setFilter(filter);
		registration.setOrder(-100);
		return registration;
	}

我们自动连接已经可用的过滤器,并以足够低的顺序注册它,使其主Spring Security过滤器之前出现。通过这种方式,我们可以使用它来处理身份验证请求中异常指示的重定向。

有了这些更改,应用程序就可以运行了,在运行时相当于我们在上一节中构建的注销示例。分解配置并使其明确告诉我们,Spring Boot所做的事情没有什么神奇的(只是配置样板),它还准备了我们的应用程序以自动扩展自动提供的功能,并添加我们自己的意见。和业务需求。

用Github登录

在本节中,我们将修改我们已经构建的应用程序 ,添加一个链接,以便用户除了可以链接到Facebook的原始链接之外,还可以选择使用Github进行身份验证。

在客户端中,更改是微不足道的,我们只需添加另一个链接:

index.html
<div class="container unauthenticated">
  <div>
    With Facebook: <a href="/login/facebook">click here</a>
  </div>
  <div>
    With Github: <a href="/login/github">click here</a>
  </div>
</div>

原则上,一旦开始添加身份验证提供程序,我们可能需要更加注意“ / user”端点返回的数据。事实证明,Github和Facebook在他们的用户信息中的相同位置都有一个“名称”字段,因此对我们的简单终结点的实践没有任何改变。

添加Github身份验证过滤器

服务器上的主要更改是添加了一个附加的安全过滤器,以处理来自我们新链接的“ / login / github”请求。我们已经在我们的Facebook中创建了针对Facebook的自定义身份验证过滤器ssoFilter()方法,因此我们所需要做的就是将其替换为可以处理多个身份验证路径的组合:

SocialApplication.java
private Filter ssoFilter() {

  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();

  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
  tokenServices.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(tokenServices);
  filters.add(facebookFilter);

  OAuth2ClientAuthenticationProcessingFilter githubFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/github");
  OAuth2RestTemplate githubTemplate = new OAuth2RestTemplate(github(), oauth2ClientContext);
  githubFilter.setRestTemplate(githubTemplate);
  tokenServices = new UserInfoTokenServices(githubResource().getUserInfoUri(), github().getClientId());
  tokenServices.setRestTemplate(githubTemplate);
  githubFilter.setTokenServices(tokenServices);
  filters.add(githubFilter);

  filter.setFilters(filters);
  return filter;

}

我们的旧代码在哪里ssoFilter()已被复制,一次用于Facebook,一次用于Github,两个过滤器合并为一个复合文件。

请注意facebook()facebookResource()方法已经补充了类似的方法github()githubResource()

SocialApplication.java
@Bean
@ConfigurationProperties("github.client")
public AuthorizationCodeResourceDetails github() {
	return new AuthorizationCodeResourceDetails();
}

@Bean
@ConfigurationProperties("github.resource")
public ResourceServerProperties githubResource() {
	return new ResourceServerProperties();
}

以及相应的配置:

application.yml
github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

此处的客户详细信息已在Github中注册,地址也已注册localhost:8080 (与Facebook相同)。

该应用程序现在可以运行了,并为用户提供了使用Facebook或Github进行身份验证的选择。

如何添加本地用户数据库

即使将身份验证委派给外部提供商,许多应用程序仍需要在本地保留有关其用户的数据。我们在这里没有显示代码,但是很容易分两步进行。

  1. 为数据库选择一个后端,并为自定义设置一些存储库(例如,使用Spring Data) User满足您需求的对象,可以通过外部身份验证完全或部分填充。

  2. 提供一个User通过检查您的存储库中登录的每个唯一用户的对象/user端点。如果已经有一个具有当前身份的用户Principal ,可以对其进行更新,否则可以创建。

提示:在User对象链接到外部提供程序中的唯一标识符(不是用户名,而是外部提供程序中的帐户唯一的名称)。

托管授权服务器

在本节中,我们通过将应用程序制作为成熟的OAuth2授权服务器来修改我们构建的github应用程序,该服务器仍使用Facebook和Github进行身份验证,但能够创建自己的访问令牌。然后,这些令牌可用于保护后端资源,或与我们碰巧需要以相同方式保护的其他应用程序进行SSO。

整理身份验证配置

在开始使用授权服务器功能之前,我们将整理两个外部提供程序的配置代码。有一些重复的代码ssoFilter()方法,因此我们将其提取到共享方法中:

SocialApplication.java
private Filter ssoFilter() {
  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();
  filters.add(ssoFilter(facebook(), "/login/facebook"));
  filters.add(ssoFilter(github(), "/login/github"));
  filter.setFilters(filters);
  return filter;
}

新的便捷方法具有旧方法中所有重复的代码:

SocialApplication.java
private Filter ssoFilter(ClientResources client, String path) {
  OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
  OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
  filter.setRestTemplate(template);
  UserInfoTokenServices tokenServices = new UserInfoTokenServices(
      client.getResource().getUserInfoUri(), client.getClient().getClientId());
  tokenServices.setRestTemplate(template);
  filter.setTokenServices(tokenServices);
  return filter;
}

它使用一个新的包装对象ClientResources巩固了OAuth2ProtectedResourceDetailsResourceServerProperties被宣布为单独的@Beans在应用程序的最新版本中:

SocialApplication.java
class ClientResources {

  @NestedConfigurationProperty
  private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

  @NestedConfigurationProperty
  private ResourceServerProperties resource = new ResourceServerProperties();

  public AuthorizationCodeResourceDetails getClient() {
    return client;
  }

  public ResourceServerProperties getResource() {
    return resource;
  }
}
包装器使用@NestedConfigurationProperty指示注释处理器也为该元数据爬网该类型,因为它不代表单个值,而是一个完整的嵌套类型。

使用此包装器后,我们可以使用与以前相同的YAML配置,但是每个提供程序都可以使用一个方法:

SocialApplication.java
@Bean
@ConfigurationProperties("github")
public ClientResources github() {
  return new ClientResources();
}

@Bean
@ConfigurationProperties("facebook")
public ClientResources facebook() {
  return new ClientResources();
}

启用授权服务器

如果我们想将我们的应用程序转换为OAuth2授权服务器,则没有什么大惊小怪的事情,至少不需要从一些基本功能(一个客户端和创建访问令牌的功能)入手。Authorization Server只是一堆端点,它们在Spring OAuth2中作为Spring MVC处理程序实现。我们已经有一个安全的应用程序,因此实际上只需添加@EnableAuthorizationServer注解:

SocialApplication.java
@SpringBootApplication
@RestController
@EnableOAuth2Client
@EnableAuthorizationServer
public class SocialApplication extends WebSecurityConfigurerAdapter {

   ...

}

有了新的注释,Spring Boot将安装所有必要的端点并为其设置安全性,只要我们提供了我们要支持的OAuth2客户端的一些详细信息:

application.yml
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      scope: read,write
      auto-approve-scopes: '.*'

该客户相当于facebook.client*github.client*我们需要进行外部身份验证。与外部提供商一起,我们必须注册并获得客户端ID和在我们的应用程序中使用的秘密。在这种情况下,我们将提供与我们相同的功能,因此我们需要(至少一个)客户端才能运行。

我们已经设定auto-approve-scopes匹配所有范围的正则表达式。这不一定是我们将这个应用程序留在真实系统中的位置,但是它使我们可以快速工作,而不必替换白标签批准页面,否则,当用户想要访问令牌时,Spring OAuth2会为我们的用户弹出。要在令牌授予中添加明确的批准步骤,我们需要提供一个替换whitelabel版本的UI(位于/oauth/confirm_access )。

要完成Authorization Server,我们只需要为其UI提供安全性配置。实际上,这个简单的应用程序没有太多的用户界面,但是我们仍然需要保护/oauth/authorize端点,并确保带有“登录”按钮的主页可见。这就是为什么我们有这种方法的原因:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")                                       (1)
    .authorizeRequests()
      .antMatchers("/", "/login**", "/webjars/**").permitAll() (2)
      .anyRequest().authenticated()                            (3)
    .and().exceptionHandling()
      .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")) (4)
    ...
}
1个 默认情况下,所有请求均受保护
2 主页和登录端点被明确排除
3 所有其他端点都需要经过身份验证的用户
4 未经身份验证的用户将重定向到主页

如何获取访问令牌

现在可以从我们的新授权服务器获得访问令牌。到目前为止,获得令牌的最简单方法是将其作为“ acme”客户端。如果您运行应用程序并将其卷曲,则可以看到以下内容:

$ curl acme:[email protected]:8080/oauth/token -d grant_type=client_credentials
{"access_token":"370592fd-b9f8-452d-816a-4fd5c6b4b8a6","token_type":"bearer","expires_in":43199,"scope":"read write"}

客户凭证令牌在某些情况下很有用(例如测试令牌端点是否正常工作),但是要利用我们服务器的所有功能,我们希望能够为用户创建令牌。为了代表我们应用程序的用户获得令牌,我们需要能够对该用户进行身份验证。如果您在应用程序启动时仔细查看日志,您会发现默认的Spring Boot用户正在记录一个随机密码(根据Spring Boot用户指南 )。您可以使用此密码代表ID为“ user”的用户获取令牌:

$ curl acme:[email protected]:8080/oauth/token -d grant_type=password -d username=user -d password=...
{"access_token":"aa49e025-c4fe-4892-86af-15af2e6b72a2","token_type":"bearer","refresh_token":"97a9f978-7aad-4af7-9329-78ff2ce9962d","expires_in":43199,"scope":"read write"}

其中“…”应替换为实际密码。这称为“密码”授予,您在其中交换访问令牌的用户名和密码。

密码授予也主要用于测试,但是当您具有本地用户数据库来存储和验证凭据时,密码授予可能适用于本机或移动应用程序。对于大多数应用程序或任何具有“社交”登录名的应用程序(例如我们的应用程序),您需要“授权码”授予,这意味着您需要使用浏览器(或行为类似于浏览器的客户端)来处理重定向和Cookie,并进行渲染来自外部提供程序的用户界面。

创建客户端应用程序

使用Spring Boot可以轻松创建用于授权服务器的客户端应用程序,该客户端应用程序本身是一个Web应用程序。这是一个例子:

ClientApplication.java
@EnableAutoConfiguration
@Configuration
@EnableOAuth2Sso
@RestController
public class ClientApplication {

  @RequestMapping("/")
  public String home(Principal user) {
    return "Hello " + user.getName();
  }

  public static void main(String[] args) {
    new SpringApplicationBuilder(ClientApplication.class)
        .properties("spring.config.name=client").run(args);
  }

}
ClientApplication不得在以下类别的同一包(或子包)中创建类SocialApplication类。否则,Spring会加载一些ClientApplication启动时自动配置SocialApplication服务器,导致启动错误。

客户端的组成部分是一个主页(仅打印用户名)和一个配置文件的显式名称(通过spring.config.name=client )。当我们运行该应用程序时,它将寻找我们提供的配置文件,如下所示:

client.yml
server:
  port: 9999
  context-path: /client
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      access-token-uri: http://localhost:8080/oauth/token
      user-authorization-uri: http://localhost:8080/oauth/authorize
    resource:
      user-info-uri: http://localhost:8080/me

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

请注意server.context-path是明确设置的,因此如果您运行该应用程序进行测试,请记住主页是http:// localhost:9999 / client 。单击该链接应将您带到身份验证服务器,并且在您选择的社交服务提供商进行身份验证之后,您将被重定向回客户端应用程序。

如果您同时在本地主机上运行客户端和身份验证服务器,则上下文路径必须是明确的,否则Cookie路径会冲突,并且两个应用程序无法在会话标识符上达成共识。

保护用户信息端点

要使用新的Authorization Server进行单点登录,就像我们一直在使用Facebook和Github一样,它需要具有一个/user受其创建的访问令牌保护的端点。到目前为止,我们有一个/user端点,并通过用户验证时创建的cookie进行保护。除了使用本地授予的访问令牌来保护它之外,我们还可以重复使用现有的端点并在新路径上为其创建别名:

SocialApplication.java
@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
  Map<String, String> map = new LinkedHashMap<>();
  map.put("name", principal.getName());
  return map;
}
我们已经将Principal变成一个Map以便隐藏我们不想暴露给浏览器的部分,并统一两个外部身份验证提供程序之间端点的行为。原则上,我们可以在此处添加更多详细信息,例如特定于提供商的唯一标识符,或者如果可用,则提供电子邮件地址。

现在,可以通过声明我们的应用程序是资源服务器(以及授权服务器)来使用访问令牌来保护“ / me”路径。我们创建一个新的配置类(作为主应用程序中的内部类,但也可以拆分为一个单独的独立类):

SocialApplication.java
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration
    extends ResourceServerConfigurerAdapter {
  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/me")
      .authorizeRequests().anyRequest().authenticated();
  }
}

另外,我们需要指定一个@Order对于主要应用程序的安全性:

SocialApplication.java
@SpringBootApplication
...
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SocialApplication extends WebSecurityConfigurerAdapter {
  ...
}

@EnableResourceServer注释使用创建安全过滤器@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER-1)默认情况下,因此将主应用程序安全性移至@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)我们确保“ / me”规则优先。

测试OAuth2客户端

要测试新功能,您可以仅运行两个应用程序,然后在浏览器中访问http:// localhost:9999 / client 。客户端应用程序将重定向到本地授权服务器,然后向用户提供使用Facebook或Github进行身份验证的通常选择。完成此操作后,控制权将返回测试客户端,将授予本地访问令牌并完成身份验证(您应该在浏览器中看到“ Hello”消息)。如果您已经通过Github或Facebook进行了身份验证,您甚至可能不会注意到远程身份验证。

为未经身份验证的用户添加错误页面

在本节中,我们将修改我们先前构建的注销应用程序,切换到Github身份验证,并向无法身份验证的用户提供一些反馈。同时,我们借此机会扩展身份验证逻辑,使其包含仅允许用户属于特定Github组织的规则。“组织”是Github特定于域的概念,但是可以为其他提供商设计类似的规则,例如,对于Google,您可能只想验证来自特定域的用户。

切换到Github

注销示例使用Facebook作为OAuth2提供程序。我们可以通过更改本地配置轻松切换到Github:

application.yml
security:
  oauth2:
    client:
      clientId: bd1c0a783ccdd1c9b9e4
      clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
      accessTokenUri: https://github.com/login/oauth/access_token
      userAuthorizationUri: https://github.com/login/oauth/authorize
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://api.github.com/user

在客户端中检测身份验证失败

在客户端上,我们需要能够为无法认证的用户提供一些反馈。为方便起见,我们在div中添加了一条提示性消息:

index.html
<div class="container text-danger error" style="display:none">
There was an error (bad credentials).
</div>

仅当显示“错误”元素时才会显示此文本,因此我们需要一些代码来做到这一点:

index.html
$.ajax({
  url : "/user",
  success : function(data) {
    $(".unauthenticated").hide();
    $("#user").html(data.userAuthentication.details.name);
    $(".authenticated").show();
  },
  error : function(data) {
    $("#user").html('');
    $(".unauthenticated").show();
    $(".authenticated").hide();
    if (location.href.indexOf("error=true")>=0) {
      $(".error").show();
    }
  }
});

身份验证功能在加载浏览器时会检查浏览器的位置,并且如果发现其中包含“ error = true”的URL,则会设置该标志。

添加错误页面

为了支持客户端中的标记设置,我们需要能够捕获身份验证错误,并使用在查询参数中设置的标记重定向到主页。因此,我们需要定期@Controller像这样:

SocialApplication.java
@RequestMapping("/unauthenticated")
public String unauthenticated() {
  return "redirect:/?error=true";
}

在示例应用程序中,我们将其放在主应用程序类中,该类现在是@Controller (不是@RestController ),以便它可以处理重定向。我们需要的最后一件事是从未经身份验证的响应(HTTP 401,又名未经授权)到我们刚刚添加的“ /未经身份验证”端点的映射:

ServletCustomizer.java
@Configuration
public class ServletCustomizer {
  @Bean
  public EmbeddedServletContainerCustomizer customizer() {
    return container -> {
      container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated"));
    };
  }
}

(在示例中,为简洁起见,将其作为嵌套类添加到主应用程序中。)

在服务器中生成401

如果用户不能或不想使用Github登录,Spring Security将已经收到401响应,因此如果您无法通过身份验证(例如,拒绝令牌授予),则该应用程序已经可以运行。

为了使事情更加生动,我们将扩展身份验证规则,以拒绝不在正确组织中的用户。使用Github API可以轻松找到有关用户的更多信息,因此我们只需要将其插入身份验证过程的正确部分即可。幸运的是,对于这样一个简单的用例,Spring Boot提供了一个简单的扩展点:如果我们声明一个@Bean类型的AuthoritiesExtractor它将用于构造经过身份验证的用户的权限(通常为“角色”)。我们可以使用该钩子来断言用户在正确的组织中,如果不是,则抛出异常:

SocialApplication.java
@Bean
public AuthoritiesExtractor authoritiesExtractor(OAuth2RestOperations template) {
  return map -> {
    String url = (String) map.get("organizations_url");
    @SuppressWarnings("unchecked")
    List<Map<String, Object>> orgs = template.getForObject(url, List.class);
    if (orgs.stream()
        .anyMatch(org -> "spring-projects".equals(org.get("login")))) {
      return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
    }
    throw new BadCredentialsException("Not in Spring Projects origanization");
  };
}

请注意,我们已经自动连线了OAuth2RestOperations进入此方法,因此我们可以使用它代表经过身份验证的用户访问Github API。我们这样做,然后遍历组织,寻找与“ spring-projects”匹配的组织(这是用于存储Spring开源项目的组织)。如果您希望能够成功进行身份验证并且不在Spring Engineering团队中,则可以在其中替换您自己的值。如果没有匹配,我们抛出BadCredentialsException然后由Spring Security接收并转到401响应中。

OAuth2RestOperations也必须同时创建为Bean(从Spring Boot 1.4开始),但这很简单,因为其成分都可以通过使用而自动生成@EnableOAuth2Sso

@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
	return new OAuth2RestTemplate(resource, context);
}
显然,以上代码可以推广到其他身份验证规则,其中一些规则适用于Github,另一些规则适用于其他OAuth2提供程序。您需要的是OAuth2RestOperations以及提供者的API的一些知识。

结论

我们已经了解了如何使用Spring Boot和Spring Security毫不费力地构建多种样式的应用程序。通过所有示例运行的主要主题是使用外部OAuth2提供程序的“社交”登录。最终样本甚至可以用来“内部”提供这种服务,因为它具有与外部提供者相同的基本功能。所有示例应用程序都可以轻松扩展和重新配置,以用于更特定的用例,通常只需要更改配置文件即可。请记住,如果您使用自己服务器中的示例版本向Facebook或Github(或类似网站)注册,并获取自己主机地址的客户端凭据。切记不要将这些凭据放在源代码控制中!

是否要编写新指南或为现有指南做出贡献?查看我们的贡献准则

所有指南均以代码的ASLv2许可证和写作的Attribution,NoDerivatives创作共用许可证发布