原文链接: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了。
$ cd simple
$ ../mvnw package
$ java -jar target/*.jar
这些例子都在
localhost:8080
上运行,因为它们使用的是注册在Facebook和Github上的OAuth2客户端应用程序。如果要在不同的主机或端口上运行这些应用程序,您需要注册自己的客户端应用程序,并将AutorizationServer
生成的clientSecret
配置到客户端应用程序的配置文件中。如果你使用默认值,就不会泄露你的Facebook或Github的凭证,但要注意你在互联网上发布的内容,不要将在AutorizationServer
上注册的客户端应用程序信息放在公开的代码版本控制服务器上。
在本节中,我们创建了一个使用Facebook进行身份认证的MVP版本。如果我们使用Spring Boot的自动配置特性,那么这会很容易实现。
首先,我们需要创建一个Spring Boot应用程序,它可以通过多种方式完成。最简单的方法是访问 http://start.spring.io ,并生成一个空项目(勾选“Web”依赖)。同样在命令行中这样做:
$ mkdir ui && cd ui
$ 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
<!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,那么我们不妨从主页上的一些基本内容着手。
如果你启动应用并加载主页,你会发现 index.html
中的 css
引用并没有被加载。我们可以通过添加一些依赖项,来完成这些文件的加载:
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公司开源的 bootstrap
和 jQuery
(这是我们现在所需要的)。另一个依赖项是 webjars-locator
,它是由webjar站点提供的一个库,它可以在Spring中使用来提供web页面使用的静态资源而不需要知道确切的版本(因此, index.html
文件中引用的 /webjars/**
链接就不需要版本号)。只要您不关闭MVC的自动配置,Spring Boot 应用程序就会默认使用 webjars-locator
。
基于上面的操作,我们应该已经为我们的应用提供了一个漂亮的主页。
为了使应用程序安全,我们只需要将Spring Security作为依赖项添加进去。如果我们这样做,会使用默认的 HTTPBasic
来保护它,因此,既然我们想要做一个“第三方登录”(委托给Facebook),我们也需要添加 SpringSecurityOAuth2
依赖项:
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 AuthorizationServer
,交对当前应用程序进行授权,就需要在 main类
上使用 @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配置的信息,你就可以再次运行该应用程序,并访问主页 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
<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
使我们需要一些客户端代码来操作 authenticated
、 unauthenticated
和 user
元素。下面是这些特性的一个简单实现(在 <body>
末尾删除它们):
index.html
<script type="text/javascript">
$.get("/user", function(data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide()
$(".authenticated").show()
});
</script>
为了实现这一点,我们需要在服务器端进行一些变更。 home
控制器 需要一个 /user
端点,该端点描述了当前经过身份认证的用户。这很容易做到,譬如在我们的 main类
上:
SocialApplication.java
@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
和 @RequestMapping
和 java.security.Principal
注入到 handler
方法中。
在
/user
端点中返回一个完整的Principal
并不是一个好主意(它可能包含您不愿意向浏览器客户端显示的信息)。我们这么做只是为了快点实现功能。在下文中,我们将在端点中进行转换,并隐藏不需要客户端知道的信息。
应用现在可以像以前一样正常运行和进行身份认证,但不会给用户一个机会点击我们刚刚新增的链接。为了使链接可见,我们还需要通过添加一个 WebSecurityConfigurer
来去掉对主页的安全性检查:
SocialApplication.java
@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在添加了 @EnableOAuth2Sso
注解的类上附加了一个特殊的含义:它使用这个注解来配置承载OAuth2身份认证处理器的安全过滤器链。因此,要使主页可见,我们需要做的就是显式地 authorizeRequests()
到主页及其包含的静态资源(还包括对处理认证的登录端点的访问)。所有其他请求(例如,到 /user
端点)都需要身份认证。
/error**
是一条不需要保护的path,因为如果应用程序中存在问题,我们希望Spring Boot能够呈现错误,即使用户是未经身份验证的。
有了这个更改,应用程序就完成了。如果运行这个应用程序并访问主页,您应该会看到一个漂亮的 loginwithFacebook
HTML链接。这个链接不直接指向Facebook,而是指向处理认证的本地路径(并发送一个重定向到Facebook)。一旦通过了身份认证,浏览器页面就会被重定向到本地应用,并且在页面显示你的用户名(假设你已经在Facebook上设置了你的权限,允许访问这些数据)。
Logout
按钮在本节中,我们修改 click 应用,主要是添加一个允许用户退出程序按钮。这似乎是一个简单的功能,但在具体实现时,需要谨慎一点,所以这是值得花一些时间讨论到底如何去做。大多数变更都是进行这样的操作,将应用从只读资源转换为可读可写( logout
需要进行状态更改),因此,在任何不只是静态内容的实际应用程序中,都需要进行相同的更改。
在客户端,我们只需要提供一个 logout
按钮和一些 JavaScript
调用服务器来发送取消认证的请求。首先,在 authenticated
部分的UI中,我们添加按钮:
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>
然后,我们提供在 JavaScript
中使用的 logout()
函数:
index.html
var logout = function() {
$.post("/logout", function() {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
})
return true;
}
logout()
函数向 /logout
端点发送一个 POST
请求 ,然后更改动态内容的展示。现在,我们可以切换到服务器端来实现该端点。
Logout
端点Spring Security已经构建了支持 /logout
的端点,这将为我们做正确的事情(清除会话并使 cookie
无效)。为了配置 端点
,我们只需在 WebSecurityConfigurer
中简单地扩展现有的 configure()
方法:
SocialApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
... // existing code here
.and().logout().logoutSuccessUrl("/").permitAll();
}
/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
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
... // existing code here
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
CSRFToken
由于在本例中没有使用特别高级别的框架,所以我们需要显式地添加 CSRF token
,这个 token
是我们从后端返回的 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>
然后,我们可以在 xhr
中使用 Cookie
对象的 get
方法,如下所示:
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.
//只将token发送到相对URL,即本地。
xhr.setRequestHeader("X-XSRF-TOKEN",
Cookies.get('XSRF-TOKEN'));
}
}
}
});
有了这些变化,我们就可以运行应用程序并尝试新的 logout
按钮了。启动应用程序,并在一个新的浏览器窗口中加载主页。点击 login
链接,浏览器会将你带到Facebook授权页(如果已经登录,你可能不会注意到重定向)。单击 Logout
按钮取消当前会话,并将该应用程序返回到未经验证的状态。如果好奇,您应该能够在浏览器与本地服务器交互的请求中看到新的 cookie
和 HTTP头
。
请记住, logout
端点与浏览器客户端一起工作,然后所有其他HTTP请求(POST、PUT、DELETE等等)也会正常工作。因此,对于具有更实际功能的应用程序来说,这应该是一个很好的平台。
在本节中,我们将通过在 @EnableOAuth2Sso
注解中分离“魔法”来修改我们已经构建的 logout应用程序,手动显式配置涉及的所有内容。
@EnableOAuth2Sso
背后有两个特性:OAuth2客户端和身份认证。客户端是可重用的,因此您也可以使用它与 AuthorizationServer
(在本例中为Facebook)提供的OAuth2资源交互(在本例中是Graph API)。身份认证部分将你的应用与Spring Security的其他部分保持一致,因此一旦与Facebook的交互结束,你的应用就会像其他安全的Spring应用一样。
客户端部分由Spring Security OAuth2提供,切换到另一个注解 @EnableOAuth2Client
。所以这个转换的第一步就是移除 @EnableOAuth2Sso
注解,并用抽象程度更低的注解替换:
SocialApplication.java
@SpringBootApplication
@EnableOAuth2Client
@RestController
public class SocialApplication extends WebSecurityConfigurerAdapter {
...
}
一旦这样做了,我们就会为我们创造一些有用的东西。首先,我们可以注入一个 OAuth2ClientContext
,并使用它来构建一个身份身份认证过滤器,将它添加到我们的安全配置中:
SocialApplication.java
@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
的新方法中创建的:
SocialApplication.java
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相关的客户端注册:
SocialApplication.java
@Bean
@ConfigurationProperties("facebook.client")
public AuthorizationCodeResourceDetails facebook() {
return new AuthorizationCodeResourceDetails();
}
为了完成身份认证,它需要知道Facebook的用户信息 端点
在哪:
SocialApplication.java
@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
最后,我们改变了登录的路径,在上面的 Filter
声明中是特定于Facebook的,所以我们需要在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 过滤器处理的,并且这个过滤器已经在应用程序上下文中使用了,因为我们使用了 @EnableOAuth2Client
。我们所需要的就是将过滤器连接起来,以便在Spring Boot应用程序中以正确的顺序调用它。要做到这一点,我们需要一个 FilterRegistrationBean
:
SocialApplication.java
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
我们注入已经存在并且可用的过滤器,并且以足够低的顺序确保能够在主Spring Security过滤器之前执行。通过这种方式,我们可以使用它来处理在身份认证请求,并使用 expceptions
来进行重定向。
随着这些更改就位,应用程序就可以很好地运行,并且在运行时就相当于我们在上一节中构建的 logout示例。将配置分解并使其显式告诉我们,Spring Boot所做的事情并没有什么神奇之处(它只是用来集中存放配置的),它还准备了我们的应用程序,以扩展自动提供的特性,添加我们自己的一些特性和业务需求。
Github
登陆在本节中,我们修改了上面创建的 app ,添加了一个链接,这样用户就可以选择使用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>
原则上,一旦开始添加认证Provider,我们可能需要更加小心从“/user” 端点
返回的数据。事实证明,Github和Facebook在用户信息中都有一个“name”字段,因此在实践中,我们的简单 端点
没有任何变化。
Github
身份认证过滤器服务器上的主要更改是添加额外的安全过滤器来处理来自新链接的 /login/github
请求。我们已经在 ssoFilter()
方法中为Facebook创建了一个自定义的身份认证过滤器,因此我们需要做的是用一个能够处理多个身份认证路径的 CompositeFilter
来替换它:
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上,因此需要使用 CompositeFilter
把这两个过滤器合并一下。
注意, 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一样)。
现在app应用程序已经可以运行了,用户可以选择在Facebook或Github上进行认证。
许多应用程序需要在本地保存有关其用户的数据,即使身份认证被委托给外部Provider。在这里我们没有展示代码,但在通过下面两个步骤很容易做到。
为您的数据库选择一个后端,并建立一些可以从外部认证填充全部或部分到自定义 User
对象的 Repository
(例如,使用Spring Data)。
为每个独立的用户提供一个 User
对象,通过 /user
端点中的 Repository
完成登陆操作。如果已经存在具有当前 Principal
身份的用户,则可以更新,否则创建。
提示:
在User
对象中添加一个字段以链接到外部Provider中的唯一标识符(不是用户名,而是外部Provider用户中特有的唯一标识)。
AuthorizationServer
在本节中,我们修改了前面构建的github 应用程序,使该应用程序具备创建自己的 Access Token能力的完整的OAuth2 AuthorizationServer
,不过仍然使用Facebook和Github进行身份认证。然后,这些 Token
可以用来保护后端资源,或者使用我们碰巧需要以相同方式保护的其他应用程序来实现SSO。
在开始使用 AuthorizationServer
特性之前,我们将整理两个外部 Provider
的配置代码。在 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;
}
此处复用了构建Facebook 过滤器的所有代码,即 ssoFilter(ClientResourcesclient,Stringpath)
,如下所示:
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
,该对象合并了 OAuth2ProtectedResourceDetails
和 ResourceServerProperties
,它们在应用程序的最后一个版本中被声明为单独的@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配置,但是对于每个 Provider
都有一个单独的方法:
SocialApplication.java
@Bean
@ConfigurationProperties("github")
public ClientResources github() {
return new ClientResources();
}
@Bean
@ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
AuthorizationServer
如果想要将我们的应用程序转换为一个OAuth2 AuthorizationServer
,那么没有太多的麻烦和仪式,至少要开始一些基本的特性(一个客户端和创建 AccessToken
的能力)。 AuthorizationServer
只不过是一堆端点,它们在Spring OAuth2中作为Spring MVC handler
的实现。我们已经有了一个安全的应用程序,所以实际上只是添加了 @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*
。有了外部Provider,我们必须注册并获得一个在应用中使用的客户ID和秘钥。在这种情况下,我们提供相同的特性,所以需要(至少一个)客户端来工作。
我们将
auto-approve-scopes
设置为匹配所有范围的正则表达式。在生产应用中,我们肯定不会这样做,但是它可以让示例程序快速的跑起来,并且不会破坏Spring OAuth2在需要AccessToken
时为用户弹出的whitelabel
授权页面。为了向Token
授权添加明确的授权操作,我们需要提供一个UI来替换whitelabel
版本(在/oauth/confirm_access
处)。
为了完成 AuthorizationServer
,我们只需要为它的UI提供安全配置。事实上,在这个简单的应用程序中,没有太多的用户界面,但是我们仍然需要保护 /oauth/authorize
端点,并确保带有"Login" 按钮的主页是可见的。这就是为什么我们有这样的方法:
@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、没有经过认证的用户在访问被保护的端点时,会被重定向到主页
AccessToken
AccessToken
现在可以从新的 AuthorizationServer
中获得。到目前为止,获得 Token
的最简单方法是把一个作为"acme"应用的客户端。如果运行应用程序并使用 curl
来获取 AccessToken
,你就可以看到这一点:
$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=client_credentials
{"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
:
$ curl acme:acmesecret@localhost: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"}
上面 curl
命令中使用"…"的地方,需要被替换成真正的密码。这种授权方式被称为“密码”授权,在这种授权类型下,你可以通过用户名和密码来换取一个 AccessToken
。
密码授权也主要用于测试,但是对于本地或移动应用程序,当您有本地用户数据库来存储和验证凭据时,它可能适合。对于大多数应用程序,或者任何具有“社交”登录的应用程序,比如我们的应用程序,您都需要"authorization code"授权,这意味着您需要一个浏览器(或表现得像浏览器的客户端)来处理重定向和cookie,以及呈现来自外部提供者的用户界面。
我们的 AuthorizationServer
本身是一个Web应用程序的客户端应用程序,所以使用Spring Boot很容易创建。下面是一个例子:
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会在启动SocialApplication
服务器时加载一些关于ClientApplication
的自动配置,从而导致启动错误。
客户端只包含一个主页(用来显示用户的名字),以及通过配置文件显式地给这个应用配置一个名称(通过 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。单击该链接会跳转到认证服务器,一旦您与您选择的社交 Provider
完成了身份认证,您将被重定向回客户端应用程序。
如果您在本地主机上同时运行客户端和
AuthorizationServer
,那么上下文路径必须被显式指定,否则会与cookie
路径冲突,并且两个应用程序也无法在SessionId
上达成一致。
要使用我们的新 AuthorizationServer
进行单点登录,就像我们一直在使用Facebook和Github一样,它需要有一个 /user
端点,该端点受到它所创建的 AccessToken
的保护。到目前为止,我们已经有了一个 /user
端点,并在用户验证时创建了 Cookie
。为了用本地授予的 AccessToken
来保护它,我们只需重新使用现有的端点,并在新路径上创建别名即可:
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;
}
现在,可以通过声明我们的应用程序是 ResourceServer
(以及 AuthorizationServer
)来使用 AccessToken
保护“/me”端点。我们创建了一个新的配置类(作为主应用程序中的n个内部类,但它也可以被分割成一个个单独的独立类):
SocialApplication.java
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration
extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/me")
.authorizeRequests().anyRequest().authenticated();
}
}
另外,我们需要为主应用程序安全Java配置指定一个 @Order
:
SocialApplication.java
@SpringBootApplication
...
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SocialApplication extends WebSecurityConfigurerAdapter {
...
}
默认情况下, @EnableResourceServer
注解创建了一个带有 @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER-1)
的安全过滤器,因此只要给主应用程序安全Java配置添加注解 @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
,就可以确保“/me”的优先级更高。
要测试这些新特性,你可以在浏览器中运行两个应用程序和访问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
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>
只有当需要显示 class
属性包含 .error
的html元素时,才会显示这些文本,因此我们需要写一些代码来完成这个功能:
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();
}
}
});
这个身份认证函数在加载时检查 location
属性的 href
值,如果发现URL中包含“error=true”,则会将会显示 class
属性包含 .error
的html元素。
为了在客户端中支持设置标识,我们需要能够捕获身份认证的错误,并使用查询参数中设置的标识重定向到主页。因此,我们需要一个端点,在就像下面这样的一个常规 @Controller
:
SocialApplication.java
@RequestMapping("/unauthenticated")
public String unauthenticated() {
return "redirect:/?error=true";
}
在示例应用程序中,我们将其放入主应用程序类中,该类现在是 @Controller
(不是 @RestController
),因此它可以处理重定向。我们需要做的最后一件事,就是添加从一个未经身份验证的响应( HTTP401
,又名 UNAUTHORIZED
)到刚刚添加的 /unauthenticated
端点的映射:
ServletCustomizer.java
@Configuration
public class ServletCustomizer {
@Bean
public EmbeddedServletContainerCustomizer customizer() {
return container -> {
container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated"));
};
}
}
(在示例中,为了简洁,会被添加到主应用程序内部的嵌套类中)。
如果用户不能或不想使用Github登录,则Spring Security已经提供了 401
响应,因此如果无法进行身份认证(例如,通过拒绝 token
授权),那么这个应用程序就已经满足要求了。
为了让事情变得更有趣味,我们将扩展认证规则来拒绝那些不在正确组织中的用户。使用Github API很容易找到关于用户的更多信息,因此我们只需要将这个逻辑嵌入到身份认证过程的正确部分。幸运的是,对于这样一个简单的用例,Spring Boot提供了一个简单的扩展点:如果我们声明一个 AuthoritiesExtractor
类型的 @Bean
,那么它将用于构造经过身份认证的用户的权限(通常是“角色”)。我们可以使用这个钩子来断言用户是在正确的组织中,如果没有则抛出异常:
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");
};
- <