使用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错误的方法。

如果您使用Mac或Linux系统,使用sdkman非常方便。但在本次的使用中,不仅可以使用类似OAuth Consumer的Spring Security OAuth,还可以实现本文介绍的Slack等OAuth提供者。请参考OAuth2SsoProperties(JavaDoc)。您可以通过将路径设置为/application.yml中的security.oauth.sso.login-path/sessions/new等来更改该路径。似乎无法在Spring Security的form-login中进行设置。
广告
将在 10 秒后关闭
bannerAds