使用Spring Boot实现OAuth登录和对API的访问限制
要实现Spring Boot中的OAuth登录,我们可以利用Spring Security OAuth,但是由于没有太多文档,所以我将记下备忘录。
虽然如此,大致上就像以下的页面所写的那样。
在这篇文章中,首先我们会实现使用OAuth进行登录,然后创建一个供AngularJS等应用程序使用的API,并记录了对该API施加用户权限访问限制的步骤。
我将记下一种在以动态页面转换为中心构建的类似SPA的应用程序中实现登录和授权的方法。
创建Spring Boot应用程序
在创建Spring Boot应用程序时,有几种选择,但在这里我们决定从头开始进行创建。
实际上,您还可以通过网页上的模板项目来快速创建并下载。
首先,这是用于构建和执行Boot应用程序的构建脚本。
在项目的根目录下准备一个build.gradle文件。
buildscript {
ext {
springBootVersion = '1.3.2.RELEASE' // Spring Bootのバージョン
}
repositories {
mavenCentral() // Gradle Spring Bootプラグインを取得するリポジトリ(ここではMavenセントラルリポジトリ)
}
dependencies {
// Gradle Spring Bootプラグイン
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
}
}
// ビルドに必要なライブラリの読み込み
apply plugin: 'java'
apply plugin: 'spring-boot'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral() // アプリケーションを実行するのに必要なライブラリの取得先リポジトリ
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.security.oauth:spring-security-oauth2')
compile("org.springframework.boot:spring-boot-devtools") //コード更新時にアプリケーションをリロードする
}
bootRun {
jvmArgs = ['-Dspring.output.ansi.enabled=always'] //コンソールに色を付ける
}
接下来,我们将创建Boot应用程序的主类。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoSampleApplication {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
}
最后,我们将准备用于屏幕显示的HTML页面。
虽然在Boot中可以轻松地使用JSP或Thymeleaf等模板,但由于与本次主题无关,我们决定仅显示简易的HTML页面。
在Spring Boot中,可以将static或public目录下的文件托管在类路径上,因此可以将index.html放置在src/main/resources/public目录下,并让index.html在类路径上被复制。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample</p>
</body>
</html>
创建完上述三个文件后,请执行以下命令来启动应用程序。
gradle bootRun
如果您还没有安装gradle命令,需要从Gradle的页面上下载并安装。
我們將確認在 http://localhost:8080/ 上顯示畫面。
SpringSecurity内置于boot应用程序的类路径中,因此默认情况下会对应用程序进行基本身份验证。
因此,当访问http://localhost:8080/时,会显示用户认证对话框。
默认用户名是user,密码是在执行gradle命令的控制台上显示的,所以请按照那样输入。
从下一节开始,我们将把这个BASIC认证方式改为通过登录链接进行OAuth2认证。
在OAuth2中进行登录设置
首先,我们将使用OAuth2将BASIC认证更改为登录方式。在这次的OAuth2提供者中,我们将使用Slack。基本上,在Facebook或其他平台上的操作方法没有太大区别。
OAuth应用程序的注册
无论选择哪个供应商,首先需要注册一个客户端应用程序。对于Slack,您可以在下面的页面上通过“创建新应用程序”选项来注册应用程序。
请随便输入一个合适的名字作为 AppName,然后在 Redirect URI(s) 中添加 http://localhost:8080。
注册后会得到分发的“Client ID”和“Client Secret”,稍后会用到。
引导应用程序的设置
在Boot应用程序中,使用Spring Security OAuth来添加OAuth Consumer 2的配置。首先,在一个用于配置的Java类中使用@EnableOAuth2Sso(JavaDoc)进行注释。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
@SpringBootApplication
@EnableOAuth2Sso // これを追加する
public class SsoSampleApplication {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
}
通过此注解,将OAuth2客户端的配置(@EnableOAuth2Client(JavaDoc))以及使用该客户端进行身份验证的处理(OAuth2SsoDefaultConfiguration(JavaDoc))集成到Boot应用程序中。
以下介绍了一种通过自己实现@EnableOAuth2Sso来定制认证处理的方法。
@EnableOAuth2Sso通过读取Spring应用程序配置中的OAuth2配置来使用。
在类路径上创建application.properties或application.yml,并添加使用OAuth提供者(本例为Slack)的配置。
我认为与属性文件相比,YAML更容易阅读,因此这次我们选择使用application.yml。
security:
oauth2:
client:
clientId: 'xxxx.xxxx' # Slackのアプリケーション登録で表示された「Client ID」
clientSecret: 'xxxxx' # Slackのアプリケーション登録で表示された「Client Secret」
accessTokenUri: 'https://slack.com/api/oauth.access' # Slackを利用する場合の設定値
userAuthorizationUri: 'https://slack.com/oauth/authorize' # Slackを利用する場合の設定値
authenticationScheme: 'query' # Slackを利用する場合の設定値
scope: 'identify' # Slackを利用する場合の設定値
tokenName: 'token' # Slackを利用する場合の設定値
resource:
userInfoUri: 'https://slack.com/api/auth.test' # Slackを利用する場合の設定値
重启Boot应用程序并访问主页(http://localhost:8080/),应该会跳转到Slack页面,并可以进行OAuth登录。
创建登录链接
如果当前状态下,一旦显示主页,就会要求登录,但我会试着修改它,通过按下登录按钮来请求登录。
首先,在HTML上创建一个登录链接。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample <a href="/login">ログイン</a></p>
</body>
</html>
/login是由Spring Boot提供的特殊路径,只需访问此处即可开始登录处理。
不需要创建与/login对应的页面。
当转到/login时,将被重定向到OAuth提供者的登录页面,登录后将根据在Slack上设置的重定向URI(s)重定向到/login之前的页面。
不过,现在的情况是,如果继续按照这样的操作,在点击/login链接之前只会被要求登录,并且为了解决这个问题,我会修改Spring Security的设置,让可以在没有登录的情况下访问首页。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated();
}
}
通过这种方式,无需登录就可以显示主页,并且点击“登录”链接后可以进入 Slack 的认证页面进行登录。此外,登录信息会保存在会话中,所以一旦登录后,再次点击登录按钮就能立即返回主页面。
创建注销功能
尽管您可以成功登录,但如果还保持现状,将无法退出登录。我会创建注销功能。
要添加登出功能,需要在Spring Security的配置中设置一个用于销毁会话登录信息的URL路径。
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll(); // ログアウト機能の設定
}
}
在SpringSecurity的HttpSecurity设置中调用logout()方法可以设置注销功能。默认情况下,通过向/logout发送POST请求来丢弃登录信息并实现注销。
另外,在SpringSecurity中,默认启用了CSRF防护,要成功发送POST请求,必须发送CSRF令牌。实际上,CSRF防护是必需的,但为了与本文的主题稍有出入,我们将其禁用。
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // CSRF対策を無効化
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll();
}
}
另外,在页面的HTML中添加一个登出按钮。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample <a href="/login">ログイン</a></p>
<form id="logoutForm" action="/logout" method="POST">
<input type="submit" value="ログアウト" />
</form>
</body>
</html>
通过点击首页上的注销按钮,可以注销并确认在再次点击“登录”链接时将重定向到Slack。
API的实现
现在,我们已经实施了认证功能,接下来我们将实现一个简单的API,以便根据当前的登录状态来改变响应。
首先,需要实现API。按照以下方式,添加用于RestAPI的控制器。
package com.example;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HelloController {
@RequestMapping("/api/hello")
public Map<String, String> getMessage() {
Map<String, String> response = new HashMap<>();
response.put("message", "Hello!");
return response;
}
}
我們決定在/api以下的URL實現API。這個URL將可以無需認證就可以使用。
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/api/**").permitAll() // /api以下のパスを認証なしで利用できるようにする
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll();
}
}
请访问http://localhost:8080/api/hello,确认是否显示JSON。
{"message": "Hello"}
另外,请确保此消息显示在屏幕上。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample <a href="/login">ログイン</a></p>
<form id="logoutForm" action="/logout" method="POST">
<input type="submit" value="ログアウト" />
</form>
<p id="message"></p>
</body>
<!-- 以下を追加 -->
<script src="//code.jquery.com/jquery-2.2.0.js"></script>
<script>
$.get('/api/hello').done(function(data){
$("#message").html(data.message);
}).fail(function(data){
$("#message").html(data.responseJSON.message);
});
</script>
</html>
我要确认画面上会显示”Hello!”这个消息。
API的访问限制
接下来,我们将对/api/hello进行访问限制。Spring Security与Servlet API集成在一起,因此可以通过Servlet API获取当前的认证状态。
将HelloController进行如下修改,根据当前的登录状态返回不同的响应。
package com.example;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.common.exceptions.UnauthorizedUserException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HelloController {
@RequestMapping("/api/hello")
public Map<String, String> getMessage(HttpServletRequest request) {
if (request.isUserInRole("ROLE_USER")) {
Map<String, String> response = new HashMap<>();
response.put("message", "Hello, " + request.getRemoteUser() + "!!!");
return response;
}
throw new UnauthorizedUserException("You don't have a required role. ");
}
@ExceptionHandler(UnauthorizedUserException.class)
public void unauthorized(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.UNAUTHORIZED.value());
}
}
你可以通过HttpServletRequest获取当前登录用户的用户名和分配的角色。
访问主页(http://localhost:8080/),确认登录前后页面显示发生变化。
总结
如果使用通常的登录流程,大部分的底层功能都可以由Spring Boot完成,这样非常方便。
当然,如果要向登录用户添加附加信息或进行角色调整,则需要进行额外的实现,但似乎并不太麻烦。
请参阅以下链接。
以下是一种可能的解释:
– https://spring.io/guides/tutorials/spring-boot-oauth2/#_how_to_add_a_local_user_database: 此链接提供了如何添加本地用户数据库的指南。
– https://spring.io/guides/tutorials/spring-boot-oauth2/#_generating_a_401_in_the_server: 此链接介绍了如何在服务器上生成401错误的方法。