[Перевод] Spring REST API + OAuth2 + AngularJS

Java Spring API

Woohoo. Возьми арлингтон-терьера.com/rest-api-tickets…

автор: Евгений Параскив

Перепечатано из публичного аккаунта: stackgc

1 Обзор

В этом руководстве мы будем использовать OAuth для защиты REST API и продемонстрируем его с помощью простого клиента AngularJS.

Приложение, которое мы собираемся создать, будет состоять из четырех отдельных модулей:

  • Сервер авторизации
  • сервер ресурсов
  • Неявный пользовательский интерфейс — внешнее приложение, использующее неявный поток
  • Пароль пользовательского интерфейса — внешнее приложение, использующее 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 для хранения токенов.

2.2, @enable сервер авторизации

Теперь настроим сервер авторизации, отвечающий за управление 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());
    }
}

Уведомление:

  • Чтобы сохранить токен, мы используемJdbcTokenStore
  • мыimplicitТип авторизации регистрирует клиента
  • Мы зарегистрировали другого клиента, авторизованногоpassword,authorization_codeа такжеrefresh_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, схема базы данных должна быть инициализирована, поэтому мы используемDataSourceInitializerи следующую схему SQL:

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фасоль --Мы можем просто использовать schema.sql — по умолчанию Spring Boot.

2.4, конфигурация безопасности

Наконец, давайте сделаем сервер авторизации более безопасным.

Когда клиентскому приложению необходимо получить токен доступа, после простого процесса проверки, основанного на входе в систему, оно сделает следующее:

@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 вы используете.

3. Сервер ресурсов

Теперь давайте поговорим о серверах ресурсов, то есть о REST API, которые мы хотим использовать.

3.1, конфигурация Maven

Перед нашей конфигурацией сервера ресурсов и сервером авторизации настроено одно и то же приложение.

3.2, Конфигурация хранилища токенов

Далее будем настраивать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());
}

Обратите внимание, что для этой простой реализации, несмотря на то, что сервер авторизации и сервер ресурсов являются отдельными приложениями,Мы также делимся SQL для хранения токенов.

Причина, конечно же, в том, что ресурсный сервер должен иметь возможностьПроверьте токен доступа, выданный сервером авторизации.эффективность.

3.3. Удаленная служба токенов

нам нужно использовать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 [JdbcTokenStore,JwtTokenStore, ...] - это не повлияет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Область (диапазон, область или разрешения) доступа к этому ресурсу.

Нам также необходимо открыть путь к защите и глобальной конфигурации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. Веб-конфигурация

Наконец, настройте очень простую веб-конфигурацию для API:

@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig extends WebMvcConfigurerAdapter {}

4. Внешний интерфейс — поток паролей

Давайте взглянем на простую клиентскую реализацию AngularJS.

Здесь мы будем использовать поток паролей OAuth2 — вот почемуЭто просто пример, а не готовое к производству приложение.. Вы заметите, что учетные данные клиента отображаются на внешнем интерфейсе — это мы обсудим в следующей статье.

Начнем с двух простых страниц — «index» и «login»; как только пользователь введет учетные данные, внешние JS-клиенты будут использовать токен доступа, который они получают от сервера авторизации.

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. Получить токен доступа

Теперь давайте посмотрим, как получить токен доступа:

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конечная точка для получения токена доступа
  • Мы используем учетные данные клиента и аутентификацию Basic Auth для доступа к этой конечной точке.
  • После этого мы отправляем учетные данные пользователя вместе с идентификатором клиента в кодировке URL и параметрами типа авторизации.
  • После получения Access Token ставим егохранится в куки

Хранение файлов cookie здесь особенно важно, потому что мы используем файлы cookie только в качестве цели хранения, а не инициируем процесс аутентификации напрямую.Это помогает предотвратить атаки и уязвимости типа подделки межсайтовых запросов (CSRF)..

4.3 Главная страница

Вот простая главная страница:

<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. Авторизация запросов клиентов

Поскольку нам нужен токен доступа для авторизации запросов на ресурсы, мы добавим простой заголовок авторизации с токеном доступа:

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. Интерфейс — неявный грант

Теперь давайте рассмотрим клиентское приложение, использующее неявную авторизацию.

Наше клиентское приложение представляет собой автономный модуль, который пытается получить доступ к серверу ресурсов после получения маркера доступа с сервера авторизации с использованием неявного потока авторизации.

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. Веб-конфигурация

Вот простая наша веб-конфигурация:

@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: идентификатор клиента приложения
  • redirect-uri: 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Команда для получения токена доступа.

Кроме того, следующее простое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

Вот наше приложение 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});
    } 
});

Обратите внимание, что после получения токена доступа, если на сервере ресурсов используется защищенный ресурс, мы передадимAuthorizationголова, чтобы использовать его.

В заключение

Мы узнали, как авторизовать наше приложение с помощью OAuth2.

Полную реализацию этого руководства можно найти по адресуэтот проект GitHubнайти в.

Оригинальный пример кода

GitHub.com/EU&P/Судный день…