[譯] Spring REST API + OAuth2 + AngularJS

Oopsguy發表於2017-09-19

www.baeldung.com/rest-api-sp…
作者:Eugen Paraschiv
譯者oopsguy.com

1、概述

在本教程中,我們將使用 OAuth 來保護 REST API,並以一個簡單的 AngularJS 客戶端進行示範。

我們要建立的應用程式將包含了四個獨立模組:

  • 授權伺服器
  • 資源伺服器
  • UI implicit - 一個使用 Implicit Flow 的前端應用
  • UI password - 一個使用 Password Flow 的前端應用

2、授權伺服器

首先,讓我們先搭建一個簡單的 Spring Boot 應用程式作為授權伺服器。

2.1、Maven 配置

我們設定以下依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>    
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>  
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>${oauth.version}</version>
</dependency>複製程式碼

請注意,我們使用了 spring-jdbc 和 MySQL,因為我們將使用 JDBC 來實現 token 儲存。

2.2、@EnableAuthorizationServer

現在,我們來配置負責管理 Access Token(訪問令牌)的授權伺服器:

@Configuration
@EnableAuthorizationServer
public class AuthServerOAuth2Config
  extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(
      AuthorizationServerSecurityConfigurer oauthServer) 
      throws Exception {
        oauthServer
          .tokenKeyAccess("permitAll()")
          .checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) 
      throws Exception {
        clients.jdbc(dataSource())
          .withClient("sampleClientId")
          .authorizedGrantTypes("implicit")
          .scopes("read")
          .autoApprove(true)
          .and()
          .withClient("clientIdPassword")
          .secret("secret")
          .authorizedGrantTypes(
            "password","authorization_code", "refresh_token")
          .scopes("read");
    }

    @Override
    public void configure(
      AuthorizationServerEndpointsConfigurer endpoints) 
      throws Exception {

        endpoints
          .tokenStore(tokenStore())
          .authenticationManager(authenticationManager);
    }

    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource());
    }
}複製程式碼

注意:

  • 為了持久化 token,我們使用了一個 JdbcTokenStore
  • 我們為 implicit 授權型別註冊了一個客戶端
  • 我們註冊了另一個客戶端,授權了 passwordauthorization_coderefresh_token 等授權型別
  • 為了使用 password 授權型別,我們需要裝配並使用 AuthenticationManager bean

2.3、資料來源配置

接下來,讓我們配置資料來源為 JdbcTokenStore 所用:

@Value("classpath:schema.sql")
private Resource schemaScript;

@Bean
public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
    DataSourceInitializer initializer = new DataSourceInitializer();
    initializer.setDataSource(dataSource);
    initializer.setDatabasePopulator(databasePopulator());
    return initializer;
}

private DatabasePopulator databasePopulator() {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScript(schemaScript);
    return populator;
}

@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
    dataSource.setUrl(env.getProperty("jdbc.url"));
    dataSource.setUsername(env.getProperty("jdbc.user"));
    dataSource.setPassword(env.getProperty("jdbc.pass"));
    return dataSource;
}複製程式碼

請注意,由於我們使用了 JdbcTokenStore,需要初始化資料庫 schema(模式),因此我們使用了 DataSourceInitializer - 和以下 SQL schema:

drop table if exists oauth_client_details;
create table oauth_client_details (
  client_id VARCHAR(255) PRIMARY KEY,
  resource_ids VARCHAR(255),
  client_secret VARCHAR(255),
  scope VARCHAR(255),
  authorized_grant_types VARCHAR(255),
  web_server_redirect_uri VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(255)
);

drop table if exists oauth_client_token;
create table oauth_client_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255)
);

drop table if exists oauth_access_token;
create table oauth_access_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication LONG VARBINARY,
  refresh_token VARCHAR(255)
);

drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
  token_id VARCHAR(255),
  token LONG VARBINARY,
  authentication LONG VARBINARY
);

drop table if exists oauth_code;
create table oauth_code (
  code VARCHAR(255), authentication LONG VARBINARY
);

drop table if exists oauth_approvals;
create table oauth_approvals (
    userId VARCHAR(255),
    clientId VARCHAR(255),
    scope VARCHAR(255),
    status VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP
);

drop table if exists ClientDetails;
create table ClientDetails (
  appId VARCHAR(255) PRIMARY KEY,
  resourceIds VARCHAR(255),
  appSecret VARCHAR(255),
  scope VARCHAR(255),
  grantTypes VARCHAR(255),
  redirectUrl VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(255)
);複製程式碼

需要注意的是,我們不一定需要顯式宣告 DatabasePopulator bean - *我們可以簡單地使用一個 schema.sql - Spring Boot 預設使用

2.4、安全配置

最後,讓我們將授權伺服器變得更加安全。

當客戶端應用程式需要獲取一個 Access Token 時,在一個簡單的表單登入驅動驗證處理之後,它將執行此操作:

@Configuration
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) 
      throws Exception {
        auth.inMemoryAuthentication()
          .withUser("john").password("123").roles("USER");
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() 
      throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll();
    }
}複製程式碼

這裡的需要提及的是,Password flow 不需要表單登入配置 - 僅限於 Implicit flow,因此您可以根據您使用的 OAuth2 flow 跳過它。

3、資源伺服器

現在,我們來討論一下資源伺服器;本質上就是我們想要消費的 REST API。

3.1、Maven 配置

我們的資源伺服器配置與之前的授權伺服器應用程式配置相同。

3.2、Token 儲存配置

接下來,我們將配置我們的 TokenStore 來訪問與授權伺服器用於儲存 Access Token 相同的資料庫:

@Autowired
private Environment env;

@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
    dataSource.setUrl(env.getProperty("jdbc.url"));
    dataSource.setUsername(env.getProperty("jdbc.user"));
    dataSource.setPassword(env.getProperty("jdbc.pass"));
    return dataSource;
}

@Bean
public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource());
}複製程式碼

請注意,針對這個簡單的實現,即使授權伺服器與資源伺服器是單獨的應用,我們也共享著 token 儲存的 SQL

原因當然是資源伺服器需要能夠驗證授權伺服器發出的 Access Token 的有效性。

3.3、遠端 Token 服務

我們可以使用 RemoteTokeServices,而不是在資源伺服器中使用一個 TokenStore

@Primary
@Bean
public RemoteTokenServices tokenService() {
    RemoteTokenServices tokenService = new RemoteTokenServices();
    tokenService.setCheckTokenEndpointUrl(
      "http://localhost:8080/spring-security-oauth-server/oauth/check_token");
    tokenService.setClientId("fooClientIdPassword");
    tokenService.setClientSecret("secret");
    return tokenService;
}複製程式碼

注意:

  • RemoteTokenService 將使用授權伺服器上的 CheckTokenEndPoint 來驗證 AccessToken 並從中獲取 Authentication 物件。
  • 可以在 AuthorizationServerBaseURL + /oauth/check_token 找到
  • 授權伺服器可以使用任何 TokenStore 型別 [JdbcTokenStoreJwtTokenStore、……] - 這不會影響到 RemoteTokenService 或者資源伺服器。

3.4、一個簡單的控制器

接下來,讓我們來實現一個簡單控制器以暴露一個 Foo 資源:

@Controller
public class FooController {

    @PreAuthorize("#oauth2.hasScope('read')")
    @RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")
    @ResponseBody
    public Foo findById(@PathVariable long id) {
        return
          new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
    }
}複製程式碼

請注意客戶端需要需要 read scope(範圍、作用域或許可權)訪問此資源。

我們還需要開啟全域性方法安全性並配置 MethodSecurityExpressionHandler

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig 
  extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}複製程式碼

以下是我們基礎的 Foo 資源:

public class Foo {
    private long id;
    private String name;
}複製程式碼

3.5、Web 配置

最後,讓我們為 API 設定一個非常基本的 web 配置:

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig extends WebMvcConfigurerAdapter {}複製程式碼

4、前端 - Password Flow

我們現在來看看一個簡單的前端 AngularJS 客戶端實現。

我們將在這裡使用 OAuth2 Password flow - 這就是為什麼這只是一個示例,而不是一個可用於生產的應用。您會注意到,客戶端憑據被暴露在前端 - 這也是我們將來在以後的文章中要討論的。

我們從兩個簡單的頁面開始 - “index” 和 “login”;一旦使用者提供憑據,前端 JS 客戶端將使用它們從授權伺服器獲取的一個 Access Token。

4.1、登入頁面

以下是一個簡單的登入頁面:

<body ng-app="myApp" ng-controller="mainCtrl">
<h1>Login</h1>
<label>Username</label><input ng-model="data.username"/>
<label>Password</label><input type="password" ng-model="data.password"/>
<a href="#" ng-click="login()">Login</a>
</body>複製程式碼

4.2、獲取 Access Token

現在,讓我們來看看如何獲取 Access Token:

var app = angular.module('myApp', ["ngResource","ngRoute","ngCookies"]);
app.controller('mainCtrl', 
  function($scope, $resource, $http, $httpParamSerializer, $cookies) {

    $scope.data = {
        grant_type:"password", 
        username: "", 
        password: "", 
        client_id: "clientIdPassword"
    };
    $scope.encoded = btoa("clientIdPassword:secret");

    $scope.login = function() {   
        var req = {
            method: 'POST',
            url: "http://localhost:8080/spring-security-oauth-server/oauth/token",
            headers: {
                "Authorization": "Basic " + $scope.encoded,
                "Content-type": "application/x-www-form-urlencoded; charset=utf-8"
            },
            data: $httpParamSerializer($scope.data)
        }
        $http(req).then(function(data){
            $http.defaults.headers.common.Authorization = 
              'Bearer ' + data.data.access_token;
            $cookies.put("access_token", data.data.access_token);
            window.location.href="index";
        });   
   }    
});複製程式碼

注意:

  • 我們傳送一個 POST 到 /oauth/token 端點以獲取一個 Access Token
  • 我們使用客戶端憑據和 Basic Auth 驗證來訪問此端點
  • 之後我們傳送使用者憑證以及客戶端 id 和授權型別引數的 URL 編碼
  • 獲取 Access Token 後,我們將其儲存在一個 cookie 中

cookie 儲存在這裡特別重要,因為我們只使用 cookie 作為儲存目標,而不是直接發動身份驗證過程。這有助於防止跨站點請求偽造(CSRF)型別的攻擊和漏洞

4.3、索引(index)頁面

以下是一個簡單的索引頁面:

<body ng-app="myApp" ng-controller="mainCtrl">
<h1>Foo Details</h1>
<label>ID</label><span>{{foo.id}}</span>
<label>Name</label><span>{{foo.name}}</span>
<a href="#" ng-click="getFoo()">New Foo</a>
</body>複製程式碼

4.4、授權客戶端請求

由於我們需要 Access Token 為對資源的請求進行授權,我們將追加一個帶有 Access Token 的簡單授權頭:

var isLoginPage = window.location.href.indexOf("login") != -1;
if(isLoginPage){
    if($cookies.get("access_token")){
        window.location.href = "index";
    }
} else{
    if($cookies.get("access_token")){
        $http.defaults.headers.common.Authorization = 
          'Bearer ' + $cookies.get("access_token");
    } else{
        window.location.href = "login";
    }
}複製程式碼

沒有沒有找到 cookie,使用者將跳轉到登入頁面。

5.前端 - 隱式授權(Implicit Grant)

現在,我們來看看使用了隱式授權的客戶端應用。

我們的客戶端應用是一個獨立的模組,嘗試使用隱式授權流程從授權伺服器獲取 Access Token 後訪問資源伺服器。

5.1、Maven 配置

這裡是 pom.xml 依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>複製程式碼

注意:我們不需要 OAuth 依賴,因為我們將使用 AngularJS 的 OAuth-ng 指令來處理,其可以使用隱式授權流程連線到 OAuth2 伺服器。

5.2、Web 配置

以下是我們的一個簡單的 web 配置:

@Configuration
@EnableWebMvc
public class UiWebConfig extends WebMvcConfigurerAdapter {
    @Bean
    public static PropertySourcesPlaceholderConfigurer 
      propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Override
    public void configureDefaultServletHandling(
      DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        super.addViewControllers(registry);
        registry.addViewController("/index");
        registry.addViewController("/oauthTemplate");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
          .addResourceLocations("/resources/");
    }
}複製程式碼

5.3、主頁

接下來,這裡是我們的主頁:

OAuth-ng 指令需要:

  • site:授權伺服器 URL
  • client-id:應用程式客戶端 id
  • redirect-uri:從授權伺服器獲 Access Token 後,要重定向到的 URI
  • scope:從授權伺服器請求的許可權
  • template:渲染自定義 HTML 模板
<body ng-app="myApp" ng-controller="mainCtrl">
    <oauth
      site="http://localhost:8080/spring-security-oauth-server"
      client-id="clientId"
      redirect-uri="http://localhost:8080/spring-security-oauth-ui-implicit/index"
      scope="read"
      template="oauthTemplate">
    </oauth>

<h1>Foo Details</h1>
<label >ID</label><span>{{foo.id}}</span>
<label>Name</label><span>{{foo.name}}</span>
</div>
<a href="#" ng-click="getFoo()">New Foo</a>

<script
  src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js">
</script>
<script
  src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-resource.min.js">
</script>
<script
  src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-route.min.js">
</script>
<script
  src="https://cdnjs.cloudflare.com/ajax/libs/ngStorage/0.3.9/ngStorage.min.js">
</script>
<script th:src="@{/resources/oauth-ng.js}"></script>
</body>複製程式碼

請注意我們如何使用 OAuth-ng 指令來獲取 Access Token。

另外,以下是一個簡單的 oauthTemplate.html

<div>
  <a href="#" ng-show="show=='logged-out'" ng-click="login()">Login</a>
  <a href="#" ng-show="show=='denied'" ng-click="login()">Access denied. Try again.</a>
</div>複製程式碼

5.4、AngularJS App

這是我們的 AngularJS app:

var app = angular.module('myApp', ["ngResource","ngRoute","oauth"]);
app.config(function($locationProvider) {
  $locationProvider.html5Mode({
      enabled: true,
      requireBase: false
    }).hashPrefix('!');
});

app.controller('mainCtrl', function($scope,$resource,$http) {
    $scope.$on('oauth:login', function(event, token) {
        $http.defaults.headers.common.Authorization= 'Bearer ' + token.access_token;
    });

    $scope.foo = {id:0 , name:"sample foo"};
    $scope.foos = $resource(
      "http://localhost:8080/spring-security-oauth-resource/foos/:fooId", 
      {fooId:'@id'});
    $scope.getFoo = function(){
        $scope.foo = $scope.foos.get({fooId:$scope.foo.id});
    } 
});複製程式碼

請注意,在獲取 Access Token 後,如果在資源伺服器中使用到了受保護的資源,我們將通過 Authorization 頭來使用它。

結論

我們已經學習瞭如何使用 OAuth2 授權我們的應用程式。

本教程的完整實現可以在此 GitHub 專案中找到 - 這是一個基於 Eclipse 的專案,所以應該很容易匯入執行。

原文示例程式碼

github.com/eugenp/spri…

相關文章