API REST do Spring + OAuth2 + Angular

API REST do Spring + OAuth2 + Angular

1. Visão geral

Neste tutorial, protegeremos uma API REST com OAuth e a consumiremos de um cliente Angular simples.

O aplicativo que criaremos consistirá em quatro módulos separados:

  • Servidor de Autorização

  • Servidor de Recursos

  • UI implícita - um aplicativo front-end usando o fluxo implícito

  • Senha da interface do usuário - um aplicativo front-end usando o fluxo de senhas

Leitura adicional:

Usando JWT com Spring Security OAuth

Um guia para usar JSON Web Tokens com assinatura simétrica e assimétrica no Spring Security OAuth.

Read more

OAuth2.0 and Dynamic Client Registration

Aprenda a definir clientes dinamicamente com Spring Security e OAuth2.

Read more

 

Antes de começarmos - uma observação importante. Lembre-se de quethe Spring Security core team is in the process of implementing a new OAuth2 stack - com alguns aspectos já destacados e outros ainda em andamento.

Aqui está um vídeo rápido que lhe dará algum contexto sobre esse esforço:

 

Tudo bem, vamos começar.

2. O Servidor de Autorização

Primeiro, vamos começar a configurar um servidor de autorização como um aplicativo simples de inicialização da primavera.

2.1. Configuração do Maven

Vamos configurar o seguinte conjunto de dependências:


    org.springframework.boot
    spring-boot-starter-web


    org.springframework
    spring-jdbc


    mysql
    mysql-connector-java
    runtime


    org.springframework.security.oauth
    spring-security-oauth2

Observe que estamos usando spring-jdbc e MySQL porque usaremos uma implementação suportada por JDBC do repositório de tokens.

2.2. @EnableAuthorizationServer

Agora, vamos começar a configurar o servidor de autorização responsável pelo gerenciamento de tokens de acesso:

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

Observe que:

  • Para manter os tokens, usamos umJdbcTokenStore

  • Registramos um cliente para o tipo de concessão “implicit

  • Registramos outro cliente e autorizamos os tipos de concessão “password“, “authorization_code” e “refresh_token

  • Para usar o tipo de concessão “password”, precisamos conectar e usar o beanAuthenticationManager

2.3. Configuração da fonte de dados

A seguir, vamos configurar nossa fonte de dados para ser usada porJdbcTokenStore:

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

Observe que, como estamos usandoJdbcTokenStore, precisamos inicializar o esquema do banco de dados, então usamosDataSourceInitializer - e o seguinte esquema 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)
);

Observe que não precisamos necessariamente do beanDatabasePopulator explícito -we could simply use a schema.sql – which Spring Boot makes use of by default.

2.4. Configuração de segurança

Por fim, vamos proteger o servidor de autorização.

Quando o aplicativo cliente precisar adquirir um token de acesso, ele o fará após um simples processo de autenticação orientado a login por formulário:

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

Uma observação rápida aqui é quethe form login configuration isn’t necessary for the Password flow - apenas para o fluxo implícito - então você pode ignorá-lo dependendo de qual fluxo OAuth2 estiver usando.

3. O Servidor de Recursos

Agora, vamos discutir o servidor de recursos; essa é essencialmente a API REST que, em última análise, queremos consumir.

3.1. Configuração do Maven

Nossa configuração do Resource Server é igual à configuração do aplicativo Authorization Server anterior.

3.2. Configuração de Token Store

A seguir, configuraremos nossoTokenStore para acessar o mesmo banco de dados que o servidor de autorização usa para armazenar tokens de acesso:

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

Observe que, para esta implementação simples,we’re sharing the SQL backed token store, embora os servidores de autorização e recursos sejam aplicativos separados.

O motivo, obviamente, é que o Servidor de Recursos precisa ser capaz decheck the validity of the access tokens emitidos pelo Servidor de Autorização.

3.3. Serviço de Token Remoto

Em vez de usarTokenStore em nosso servidor de recursos, podemos usarRemoteTokeServices:

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

Observe que:

  • EsteRemoteTokenService usaráCheckTokenEndPoint no Authorization Server para validar o AccessToken e obter o objetoAuthentication dele.

  • O pode ser encontrado em AuthorizationServerBaseURL + ”/oauth/check_token

  • O servidor de autorização pode usar qualquer tipo de TokenStore [JdbcTokenStore,JwtTokenStore, ...] - isso não afetará oRemoteTokenService ou servidor de recursos.

3.4. Um controlador de amostra

A seguir, vamos implementar um controlador simples expondo um recursoFoo:

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

Observe como o cliente precisa do escopo“read” para acessar este Recurso.

Também precisamos habilitar a segurança do método global e configurarMethodSecurityExpressionHandler:

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

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}

E aqui está nossoFoo Recurso básico:

public class Foo {
    private long id;
    private String name;
}

3.5. Configuração da Web

Por fim, vamos definir uma configuração da Web muito básica para a API:

@Configuration
@EnableWebMvc
@ComponentScan({ "org.example.web.controller" })
public class ResourceWebConfig implements WebMvcConfigurer {}

4. Front End - Configuração

Agora, veremos uma implementação Angular de front-end simples para o cliente.

Primeiro, usaremosAngular CLI para gerar e gerenciar nossos módulos front-end.

First, we’ll install node and npm - pois o Angular CLI é uma ferramenta npm.

Então, precisamos usar ofrontend-maven-plugin para construir nosso projeto Angular usando o maven:


    
        
            com.github.eirslett
            frontend-maven-plugin
            1.3
            
                v6.10.2
                3.10.10
                src/main/resources
            
            
                
                    install node and npm
                    
                        install-node-and-npm
                    
                
                
                    npm install
                    
                        npm
                    
                
                
                    npm run build
                    
                        npm
                    
                    
                        run build
                    
                
            
        
    

E finalmente,generate a new Module using Angular CLI:

ng new oauthApp

Observe que teremos dois módulos front-end - um para fluxo de senha e outro para fluxo implícito.

Nas seções a seguir, discutiremos a lógica do aplicativo Angular para cada módulo.

5. Fluxo de senha usando Angular

Usaremos o fluxo de senha OAuth2 aqui - é por isso quethis is just a proof of concept, not a production ready application. Você notará que as credenciais do cliente estão expostas ao front end - algo que abordaremos em um artigo futuro.

Nosso caso de uso é simples: uma vez que um usuário fornece suas credenciais, o cliente front-end os utiliza para adquirir um token de acesso do servidor de autorização.

5.1. Serviço de aplicativo

Vamos começar com nossoAppService - localizado emapp.service.ts - que contém a lógica para interações com o servidor:

  • obtainAccessToken(): para obter o token de acesso com as credenciais do usuário

  • saveToken(): para salvar nosso token de acesso em um cookie usando a biblioteca ng2-cookies

  • getResource(): para obter um objeto Foo do servidor usando seu ID

  • checkCredentials(): para verificar se o usuário está logado ou não

  • logout(): para excluir o cookie do token de acesso e desconectar o usuário

export class Foo {
  constructor(
    public id: number,
    public name: string) { }
}

@Injectable()
export class AppService {
  constructor(
    private _router: Router, private _http: Http){}

  obtainAccessToken(loginData){
    let params = new URLSearchParams();
    params.append('username',loginData.username);
    params.append('password',loginData.password);
    params.append('grant_type','password');
    params.append('client_id','fooClientIdPassword');
    let headers =
      new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Basic '+btoa("fooClientIdPassword:secret")});
    let options = new RequestOptions({ headers: headers });

    this._http.post('http://localhost:8081/spring-security-oauth-server/oauth/token',
      params.toString(), options)
      .map(res => res.json())
      .subscribe(
        data => this.saveToken(data),
        err => alert('Invalid Credentials'));
  }

  saveToken(token){
    var expireDate = new Date().getTime() + (1000 * token.expires_in);
    Cookie.set("access_token", token.access_token, expireDate);
    this._router.navigate(['/']);
  }

  getResource(resourceUrl) : Observable{
    var headers =
      new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
      'Authorization': 'Bearer '+Cookie.get('access_token')});
    var options = new RequestOptions({ headers: headers });
    return this._http.get(resourceUrl, options)
                   .map((res:Response) => res.json())
                   .catch((error:any) =>
                     Observable.throw(error.json().error || 'Server error'));
  }

  checkCredentials(){
    if (!Cookie.check('access_token')){
        this._router.navigate(['/login']);
    }
  }

  logout() {
    Cookie.delete('access_token');
    this._router.navigate(['/login']);
  }
}

Observe que:

  • Para obter um token de acesso, enviamos umPOST para o endpoint “/oauth/token

  • Estamos usando as credenciais do cliente e a autenticação básica para atingir este endpoint

  • Em seguida, estamos enviando as credenciais do usuário junto com o ID do cliente e parâmetros de tipo de concessão codificados por URL

  • Depois de obtermos o token de acesso -we store it in a cookie

O armazenamento de cookies é especialmente importante aqui, porque estamos usando o cookie apenas para fins de armazenamento e não para impulsionar o processo de autenticação diretamente. This helps protect against cross-site request forgery (CSRF) type of attacks and vulnerabilities.

5.2. Componente de Login

A seguir, vamos dar uma olhada em nossoLoginComponent, que é responsável pelo formulário de login:

@Component({
  selector: 'login-form',
  providers: [AppService],
  template: `

Login

` }) export class LoginComponent { public loginData = {username: "", password: ""}; constructor(private _service:AppService) {} login() { this._service.obtainAccessToken(this.loginData); }

5.3. Componente residencial

A seguir, nossoHomeComponent, que é responsável por exibir e manipular nossa página inicial:

@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `Welcome !!
    Logout
    `
})

export class HomeComponent {
    constructor(
        private _service:AppService){}

    ngOnInit(){
        this._service.checkCredentials();
    }

    logout() {
        this._service.logout();
    }
}

5.4. Componente Foo

Finalmente, nossoFooComponent para exibir nossos detalhes Foo:

@Component({
  selector: 'foo-details',
  providers: [AppService],
  template: `

Foo Details

{{foo.id}} {{foo.name}} ` }) export class FooComponent { public foo = new Foo(1,'sample foo'); private foosUrl = 'http://localhost:8082/spring-security-oauth-resource/foos/'; constructor(private _service:AppService) {} getFoo(){ this._service.getResource(this.foosUrl+this.foo.id) .subscribe( data => this.foo = data, error => this.foo.name = 'Error'); } }

5.5. Componente de aplicativo

NossoAppComponent simples para atuar como o componente raiz:

@Component({
    selector: 'app-root',
    template: ``
})

export class AppComponent {}

E oAppModule onde envolvemos todos os nossos componentes, serviços e rotas:

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    LoginComponent,
    FooComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot([
     { path: '', component: HomeComponent },
    { path: 'login', component: LoginComponent }])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

6. Fluxo Implícito

A seguir, focaremos no módulo Implicit Flow.

6.1. Serviço de aplicativo

Da mesma forma, começaremos com nosso serviço, mas desta vez usaremos a bibliotecaangular-oauth2-oidc em vez de obtermos nós mesmos o token de acesso:

@Injectable()
export class AppService {

  constructor(
    private _router: Router, private _http: Http, private oauthService: OAuthService){
        this.oauthService.loginUrl =
          'http://localhost:8081/spring-security-oauth-server/oauth/authorize';
        this.oauthService.redirectUri = 'http://localhost:8086/';
        this.oauthService.clientId = "sampleClientId";
        this.oauthService.scope = "read write foo bar";
        this.oauthService.setStorage(sessionStorage);
        this.oauthService.tryLogin({});
    }

  obtainAccessToken(){
      this.oauthService.initImplicitFlow();
  }

  getResource(resourceUrl) : Observable{
    var headers =
      new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8',
     'Authorization': 'Bearer '+this.oauthService.getAccessToken()});
    var options = new RequestOptions({ headers: headers });
    return this._http.get(resourceUrl, options)
      .map((res:Response) => res.json())
      .catch((error:any) => Observable.throw(error.json().error || 'Server error'));
  }

  isLoggedIn(){
    if (this.oauthService.getAccessToken() === null){
       return false;
    }
    return true;
  }

  logout() {
      this.oauthService.logOut();
      location.reload();
  }
}

Observe como, após obter o token de acesso, o estamos usando por meio do cabeçalhoAuthorization sempre que consumimos recursos protegidos de dentro do servidor de recursos.

6.2. Componente residencial

NossoHomeComponent para lidar com nossa página inicial simples:

@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `
    
    
Welcome !! Logout
` }) export class HomeComponent { public isLoggedIn = false; constructor( private _service:AppService){} ngOnInit(){ this.isLoggedIn = this._service.isLoggedIn(); } login() { this._service.obtainAccessToken(); } logout() { this._service.logout(); } }

6.3. Componente Foo

NossoFooComponent é exatamente o mesmo que no módulo de fluxo de senha.

6.4. Módulo App

Finalmente, nossoAppModule:

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    FooComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    OAuthModule.forRoot(),
    RouterModule.forRoot([
     { path: '', component: HomeComponent }])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

7. Execute o front end

1. Para executar qualquer um dos nossos módulos front-end, precisamos criar o aplicativo primeiro:

mvn clean install

2. Em seguida, precisamos navegar para o diretório de aplicativos Angular:

cd src/main/resources

3. Por fim, iniciaremos nosso aplicativo:

npm start

O servidor iniciará por padrão na porta 4200, para alterar a porta de qualquer módulo, altere o

"start": "ng serve"

empackage.json para que seja executado na porta 8086, por exemplo:

"start": "ng serve --port 8086"

8. Conclusão

Neste artigo, aprendemos como autorizar nosso aplicativo usando o OAuth2.

A implementação completa deste tutorial pode ser encontrada emthe GitHub project.