使用OAuth保護REST API並使用簡單的Angular客戶端

1.概述

在本教程中,咱們將使用OAuth保護REST API並從簡單的Angular客戶端使用它前端

咱們要構建的應用程序將包含四個獨立的模塊:
  • 受權服務器
  • 資源服務器
  • UI implicit - 使用implicit流的前端應用程序
  • UI密碼 - 使用密碼流的前端應用程序

在咱們開始以前 -** 一個重要的注意事項。請記住,Spring Security核心團隊正在實施新的OAuth2堆棧 - 某些方面已經完成,有些方面仍在進行中**。java

這是一個快速視頻,將爲您提供有關該工做的一些背景信息
https://youtu.be/YI4YCJoOF0knode

2.受權服務器

首先,讓咱們開始將Authorization Server設置爲一個簡單的Spring Boot應用程序。mysql

2.1。 Maven配置

咱們將設置如下依賴項集:git

<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>
</dependency>

請注意,咱們使用的是spring-jdbc和MySQL,由於咱們將使用JDBC支持的令牌存儲實現github

2.2。 @EnableAuthorizationServer

如今,讓咱們開始配置負責管理訪問令牌的受權服務器:web

@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」受權類型
  • 爲了使用「密碼」受權類型,咱們須要鏈接並使用AuthenticationManager bean

2.3。數據源配置

接下來,讓咱們配置JdbcTokenStore使用的數據源:spring

@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模式: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 bean - 咱們能夠簡單地使用schema.sql - Spring Boot默認使用它數據庫

2.4。Security配置

最後,讓咱們保護受權服務器。

當客戶端應用程序須要獲取訪問令牌時,它將在簡單的表單登陸驅動的身份驗證過程以後執行

@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而不是在Resource Server中使用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」找到
  • Authorization Server可使用任何TokenStore類型[JdbcTokenStore,JwtTokenStore,...] - 這不會影響RemoteTokenService或Resource服務器。

3.4。 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」 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 implements WebMvcConfigurer {}

4.前端 - 設置

咱們如今將查看客戶端的簡單前端Angular實現。

首先,咱們將使用Angular CLI生成和管理咱們的前端模塊

首先,咱們將安裝node和npm - 由於Angular CLI是一個npm工具

而後,咱們須要使用frontend-maven-plugin使用maven構建咱們的Angular項目

<build>
    <plugins>
        <plugin>
            <groupId>com.github.eirslett</groupId>
            <artifactId>frontend-maven-plugin</artifactId>
            <version>1.3</version>
            <configuration>
                <nodeVersion>v6.10.2</nodeVersion>
                <npmVersion>3.10.10</npmVersion>
                <workingDirectory>src/main/resources</workingDirectory>
            </configuration>
            <executions>
                <execution>
                    <id>install node and npm</id>
                    <goals>
                        <goal>install-node-and-npm</goal>
                    </goals>
                </execution>
                <execution>
                    <id>npm install</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                </execution>
                <execution>
                    <id>npm run build</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <arguments>run build</arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

最後,使用Angular CLI生成一個新模塊:

ng new oauthApp

請注意,咱們將有兩個前端模塊 - 一個用於密碼流,另外一個用於隱式流

在如下部分中,咱們將討論每一個模塊的Angular app邏輯

5.使用Angular的密碼流

咱們將在這裏使用OAuth2密碼流 - 這就是爲何這只是一個概念證實,而不是生產就緒的應用程序。您會注意到客戶端憑據已暴露給前端 - 這是咱們將在之後的文章中介紹的內容。

咱們的用例很簡單:一旦用戶提供其憑據,前端客戶端就會使用它們從受權服務器獲取訪問令牌

5.1。應用服務

讓咱們從位於app.service.ts的AppService開始 - 它包含服務器交互的邏輯:

  • obtainAccessToken():獲取給定用戶憑據的Access令牌
  • saveToken():使用ng2-cookies庫將訪問令牌保存在cookie中
  • getResource():使用其ID從服務器獲取Foo對象
  • checkCredentials():檢查用戶是否已登陸
  • logout():刪除訪問令牌cookie並將用戶註銷
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<Foo>{
    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']);
  }
}
注意:
  • 要獲取訪問令牌,咱們將POST發送到「/ oauth / token」端點
  • 咱們使用客戶端憑據和Basic Auth來命中此端點
  • 而後,咱們將發送用戶憑據以及客戶端ID和授予類型參數URL編碼
  • 獲取訪問令牌後 - 咱們將其存儲在cookie中

cookie存儲在這裏特別重要,由於咱們只是將cookie用於存儲目的而不是直接驅動身份驗證過程。這有助於防止跨站點請求僞造(CSRF)類型的攻擊和漏洞。

5.2。登陸組件

接下來,讓咱們看一下負責登陸表單的LoginComponent:

@Component({
  selector: 'login-form',
  providers: [AppService],  
  template: `<h1>Login</h1>
    <input type="text" [(ngModel)]="loginData.username" />
    <input type="password"  [(ngModel)]="loginData.password"/>
    <button (click)="login()" type="submit">Login</button>`
})
export class LoginComponent {
    public loginData = {username: "", password: ""};
 
    constructor(private _service:AppService) {}
  
    login() {
        this._service.obtainAccessToken(this.loginData);
    }

5.3。主頁組件

接下來,咱們的HomeComponent負責顯示和操做咱們的主頁:

@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `<span>Welcome !!</span>
    <a (click)="logout()" href="#">Logout</a>
    <foo-details></foo-details>`
})
  
export class HomeComponent {
    constructor(
        private _service:AppService){}
  
    ngOnInit(){
        this._service.checkCredentials();
    }
  
    logout() {
        this._service.logout();
    }
}

5.4。 Foo組件

最後,咱們的FooComponent顯示咱們的Foo細節:

@Component({
  selector: 'foo-details',
  providers: [AppService],  
  template: `<h1>Foo Details</h1>
    <label>ID</label> <span>{{foo.id}}</span>
    <label>Name</label> <span>{{foo.name}}</span>
    <button (click)="getFoo()" type="submit">New Foo</button>`
})
 
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。應用組件

咱們的簡單AppComponent充當根組件:

@Component({
    selector: 'app-root',
    template: `<router-outlet></router-outlet>`
})
 
export class AppComponent {}

以及咱們包裝全部組件,服務和路由的AppModule:

@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.隱含flow

接下來,咱們將重點關注Implicit Flow模塊。

6.1。應用服務

一樣,咱們將從咱們的服務開始,但此次咱們將使用庫angular-oauth2-oidc而不是本身獲取訪問令牌:

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

請注意,在獲取訪問令牌後,每當咱們從資源服務器中使用受保護資源時,咱們都會經過Authorization標頭使用它

6.2。主頁組件

咱們的HomeComponent處理咱們簡單的主頁:

@Component({
    selector: 'home-header',
    providers: [AppService],
  template: `
    <button *ngIf="!isLoggedIn" (click)="login()" type="submit">Login</button>
    <div *ngIf="isLoggedIn">
        <span>Welcome !!</span>
        <a (click)="logout()" href="#">Logout</a>
        <br/>
        <foo-details></foo-details>
    </div>`
})
  
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。 Foo組件

咱們的FooComponent與密碼流模塊徹底相同。

6.4。應用模塊

最後,咱們的AppModule:

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

7.運行前端

1.要運行咱們的任何前端模塊,咱們須要首先構建應用程序:
mvn clean install
2.而後咱們須要導航到咱們的Angular app目錄:
cd src/main/resources
3.最後,咱們將啓動咱們的應用程序:
npm start

默認狀況下,服務器將在端口4200上啓動,以更改任何模塊的端口更改

"start": "ng serve"

在package.json中使它在端口8086上運行,例如:

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

8.結論

在本文中,咱們學習瞭如何使用OAuth2受權咱們的應用程序。

能夠在GitHub項目中找到本教程的完整實現。

相關文章
相關標籤/搜索