Secure REST API with oauth2 (翻譯)


http://blog.csdn.net/haiyan_qi/article/details/52384734

**************************************************

 

1.概述

運用AngularJS和springboot技術實現的demo:
https://github.com/qihaiyan/ng-boot-oauth

在這個教程中,我們將用oauth2對REST API進行安全控制,並在一個簡單的AngularJS客戶端程序中使用。
我們將要構建的應用包含四個獨立的模塊:

  • Authorization Server
  • Resource Server
  • UI implicit – 使用 Implicit Flow 的前端應用
  • UI password – 使用 Password Flow 的前端應用

2.認證服務

我們開始用spring Boot構建一個認證服務。

2.1 Maven配置

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 store。

2.2. @EnableAuthorizationServer

配置用於管理access tokens的認證服務:

@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());
    }
}

 

解釋:

  • 用JdbcTokenStore來存儲tokens
  • 注冊一個采用“implicit”授權方式的客戶端
  • 注冊另一個采用 “password“, “authorization_code” 和 “refresh_token”授權方式的客戶端
  • 為了使用“password”授權方式,我們需要通過spring的@Autowired注解來注入和使用AuthenticationManagerbean

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 時,我們需要初始化數據庫並創建相關的表來存儲token數據,通過使用DataSourceInitializer 和下面的語句來實現:

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)
);

 

2.4. 安全權限配置

最后,為認證服務增加安全權限控制功能。
當客戶端程序需要獲取Access Token時,會執行下面一個簡單的from-login驅動的認證過程:

@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();
    }
}

需要注意對於oauth2的Password flow模式,from-login配置不是必須的,只對Implicit flow是必須的。

3. Resource 服務

Resource 服務用於提供REST API。

3.1. Maven 配置

Resource 服務的Maven配置與前面的認證服務的Maven配置相同。

3.2. Token Store 配置

TokenStore 采用與前面的認證服務相同的數據源

@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());
}

 

為了簡化起見,雖然認證服務和Resource 服務是兩個獨立的應用程序,但是用了同一個數據庫,原因是Resource 服務需要驗證認證服務中生成的access token。

3.3. Remote Token Service

除了在Resource服務中使用TokenStore 之外,還可以使用RemoteTokeServices:

@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會使用認證服務中的CheckToken節點去驗證AccessToken並獲取 Authentication對象.
    *CheckToken節點的訪問地址為:認證服務器的URL +”/oauth/check_token“
  • 認證服務可以使用任意的TokenStore類型,包括 [JdbcTokenStore, JwtTokenStore, …] ,不會影響到RemoteTokenService 或 Resource 服務

3.4. 一個簡單的 Controller

下面用一個簡單的Controller來提供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”權限。
同時需要啟用全局安全權限控制,並且需要配置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. 客戶端應用 – 用戶密碼模式

下面來看一下用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
  • 通過 url encode 對用戶憑證、客戶端 id 和 grant type進行編碼
  • 得到Access Token后將其存放到cookie中

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. 對客戶端請求進行授權

因為Resource服務需要使用access token對客戶端請求進行授權驗證,我們用access token在http頭中增加一個簡單的authorization header:

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,將重定向到login頁面。

5. 客戶端應用 – 簡化模式

下面來看一下采用OAuth2簡化模式的客戶端程序。
這個程序是一個單獨的模塊,采用oauth2的簡化模式,從認證服務中獲取access token,然后用這個access token去訪問Resource服務。

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>

 

這兒不需要spring的oauth模塊,我們將使用AngularJS的OAuth-ng directive,以implicit grant flow方式去訪問oauth2 認證服務。

5.2. 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. Home 頁面

OAuth-ng directive需要以下參數:

  • site: 認證服務的URL
  • client-id: 客戶端應用的 client id
  • redirect-uri: 從認證服務獲取到access token后,重定向到此URI
  • scope: 從認證服務獲取到的權限
  • template: AngularJS的頁面模板
<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 directive來獲取AccessToken,這是一個簡單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 應用

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后,通過http頭的Authorization header來訪問Resrouce服務中提供的接口服務。

6. 總結

至此我們闡述了如果使用OAuth2來為應用程序提供安全權限控制功能。
本文的所有實例代碼在 the github project 中 - 這是一個eclipse項目,可以直接導入並運行。

https://github.com/Baeldung/spring-security-oauth

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM