http://www.baeldung.com/rest-api-spring-oauth2-angularjs

作者:Eugen Paraschiv

译者http://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 的项目,所以应该很容易导入运行。

原文示例代码

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

Spring REST API + OAuth2 + AngularJS的更多相关文章

  1. Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器

    概要 之前的两篇文章,讲述了Spring Security 结合 OAuth2 .JWT 的使用,这一节要求对 OAuth2.JWT 有了解,若不清楚,先移步到下面两篇提前了解下. Spring Bo ...

  2. Setting Up Swagger 2 with a Spring REST API

    Last modified: August 30, 2016 REST, SPRING by baeldung If you're new here, join the next webinar: & ...

  3. 通过Spring Mail Api发送邮件

    使用Java Mail API来发送邮件也很容易实现,但是最近公司一个同事封装的邮件API实在让我无法接受,于是便打算改用Spring Mail API来发送邮件,顺便记录下这篇文章. [Spring ...

  4. Spring Boot 与 OAuth2 官方最详细教程

    https://mp.weixin.qq.com/s?__biz=MzU0MDEwMjgwNA==&mid=2247484357&idx=1&sn=73e501de8591e6 ...

  5. Spring Security 与 OAuth2 介绍

    个人 OAuth2 全部文章 Spring Security 与 OAuth2(介绍):https://www.jianshu.com/p/68f22f9a00ee Spring Security 与 ...

  6. 使用Spring Cloud Security OAuth2搭建授权服务

    阅读数:84139 前言: 本文意在抛砖引玉,帮大家将基本的环境搭起来,具体实战方案还要根据自己的业务需求进行制定.我们最终没有使用Spring Security OAuth2来搭建授权服务,而是完全 ...

  7. Secure Spring REST API using Basic Authentication

    What is Basic Authentication? Traditional authentication approaches like login pages or session iden ...

  8. Spring学习十五----------Spring AOP API的Pointcut、advice及 ProxyFactoryBean相关内容

    © 版权声明:本文为博主原创文章,转载请注明出处 实例: 1.项目结构 2.pom.xml <project xmlns="http://maven.apache.org/POM/4. ...

  9. Spring Security 与 OAuth2(介绍)

    https://www.jianshu.com/p/68f22f9a00ee Spring Security 与 OAuth2(介绍) 林塬 2018.01.23 11:14* 字数 3097 阅读 ...

随机推荐

  1. Github 开源:使用 .NET WinForm 开发所见即所得的 IDE 开发环境(Sheng.Winform.IDE)【2.源代码简要说明】

    GitHub:https://github.com/iccb1013/Sheng.Winform.IDE 在上一篇文章中,简要的介绍了  Sheng.Winform.IDE 的基本功能和要实现的目标: ...

  2. select、poll、epoll简介

    epoll跟select都能提供多路I/O复用的解决方案.在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现 sele ...

  3. MySQL Linux压缩版安装方法

    在诸多开源数据库中,MySQL是目前应用行业,特别是互联网行业发展最好的一个.借助灵活的架构特点和适应不同应用系统场景的Storage Engine,MySQL在很多方面已经有不次于传统商用数据库的表 ...

  4. 介绍maven构建的生命周期

    介绍maven构建的生命周期 这篇是 https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html 的 ...

  5. SpringMVC(三)-- 视图和视图解析器、数据格式化标签、数据类型转换、SpringMVC处理JSON数据、文件上传

    1.视图和视图解析器 请求处理方法执行完成后,最终返回一个 ModelAndView 对象 对于那些返回 String,View 或 ModeMap 等类型的处理方法,SpringMVC 也会在内部将 ...

  6. Capacitor电容

    capacitor无正负极性. cap electrolit有极性,实际中不能接反,否则电容会烧毁或爆炸. MULTISIM仿真中接反会有漏阻存在,但不会烧毁. 电容的分类 按结构可分为:固定电容,可 ...

  7. MongoDB副本集模式安装

    设备: 三个1G.20G.1核的虚拟机,系统是SentOS7 min 设置目录: Server1: mkdir -p /home/mongoshard/data/shard11 /home/mongo ...

  8. JAVA线程池应用的DEMO

    在做很多高并发应用的时候,单线程的瓶颈已经满足不了我们的需求,此时使用多线程来提高处理速度已经是比较常规的方案了.在使用多线程的时候,我们可以使用线程池来管理我们的线程,至于使用线程池的优点就不多说了 ...

  9. ORACLE SEQUENCE的简单介绍

    先假设有这么一个表: create table S_Depart ( DepartId INT not null, DepartName NVARCHAR2() not null, DepartOrd ...

  10. docker+tomcat 启动时非常慢原因之JRE /dev/random阻塞

    docker+tomcat 启动时非常慢,一般正常启动几十秒的,发现docker+tomcat启动竟需要几分钟,不可思议 根本原因是 SecureRandom 这个 jre 的工具类的问题.那为什么 ...