• Spring REST API + OAuth2 + AngularJS


    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/

  • 相关阅读:
    关于职业生涯的思考 吴丹阳
    2022GPLT团体程序设计天梯赛 结果记录
    vuecli中使用npm link后报错
    JavaScript の querySelector 使用说明
    改善图形神经网络,提升GNN性能的三个技巧
    在预测中使用LSTM架构的最新5篇论文推荐
    HIST:微软最新发布的基于图的可以挖掘面向概念分类的共享信息的股票趋势预测框架
    检测和处理异常值的极简指南
    SRCNN:基于深度学习的超分辨率开山之作回顾
    5篇关于将强化学习与马尔可夫决策过程结合使用的论文推荐
  • 原文地址:https://www.cnblogs.com/oopsguy/p/7550399.html
Copyright © 2020-2023  润新知