Spring REST API + OAuth2 + Angular (使用 Spring Security OAuth 遺留棧)

Spring Security
Remote
1
06:23 AM · Nov 30 ,2025

1. 概述

在本教程中,我們將使用OAuth安全REST API並從簡單的Angular客户端進行消費。

我們將構建的應用程序將由四個單獨模塊組成:

  • 授權服務器
  • 資源服務器
  • UI implicit – 一個使用隱式流的客户端應用程序
  • UI password – 一個使用密碼流的客户端應用程序

注意: 本文使用 Spring OAuth 遺留項目。對於使用 Spring Security 5 堆棧版本的本文,請查看我們的文章 Spring REST API + OAuth2 + Angular。

現在,讓我們直接開始吧。

2. The Authorization Server

首先,讓我們設置一個簡單的 Spring Boot 應用程序作為授權服務器。

2.1. Maven Configuration

我們將設置以下一組依賴項:

<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 實現的 token 存儲。

2.2.

現在,讓我們配置負責管理訪問令牌的授權服務器:

@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” grant 類型的客户端
  • 我們還註冊了一個客户端並授權了“password”、“authorization_code”和“refresh_token” grant 類型
  • 為了使用“password” grant 類型,我們需要將 AuthenticationManager 注入並使用它

2.3. Data Source Configuration

接下來,讓我們配置用於 AuthorizationServer 存儲的 DataSource:

@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 – 以及 Spring Boot 默認使用的以下 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 – Spring Boot 默認會使用 schema.sql

2.4. Security Configuration

最後,讓我們安全地配置授權服務器。

當客户端應用程序需要獲取訪問令牌時,它將在一個簡單的表單登錄驅動的身份驗證流程中進行:

@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 流程並非必需 – 僅對於 Implicit 流程才需要,因此您可能能夠跳過它,具體取決於您使用的 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. 遠程令牌服務

而不是在資源服務器中使用 TokenStore,我們可以使用 RemoteTokenServices

@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 對象。
  • 可以在授權服務器 +”/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” 範圍才能訪問此資源。

    我們還需要啓用全局方法安全並配置 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>安裝 node 和 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 應用邏輯。

    5. Password Flow Using Angular

    We’re going to be using the OAuth2 Password flow here – which is why this is just a proof of concept, not a production-ready application. You’ll notice that the client credentials are exposed to the front end – which is something we’ll address in a future article.

    Our use case is simple: once a user provides their credentials, the front-end client uses them to acquire an Access Token from the Authorization Server.

    5.1. App Service

    Let’s start with our AppService – located at app.service.ts – which contains the logic for server interactions:

    • obtainAccessToken()
    • : to obtain Access token given user credentials
    • saveToken()
    • : to save our access token in a cookie using ng2-cookies library
    • getResource()
    • : to get a Foo object from server using its ID
    • checkCredentials()
    • : to check if user is logged in or not
    • logout()
    • : to delete access token cookie and log the user out
    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']);
      }
    }

    Note that:

    • To get an Access Token we send a POST to the “/oauth/token” endpoint
    • We’re using the client credentials and Basic Auth to hit this endpoint
    • We’re then sending the user credentials along with the client id and grant type parameters URL encoded
    • After we obtain the Access Token – we store it in a cookie

    The cookie storage is especially important here, because we’re only using the cookie for storage purposes and not to drive the authentication process directly. This helps protect against cross-site request forgery (CSRF) type of attacks and vulnerabilities.

    5.2. Login Component

    Next, let’s take a look at our LoginComponent which is responsible for the login form:

    @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. Home Component

    Next, our HomeComponent which is responsible for displaying and manipulating our Home Page:

    @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 Component

    Finally, our FooComponent to display our Foo details:

    @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. App Component

    Our simple AppComponent to act as the root component:

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

    And the AppModule where we wrap all our components, services and routes:

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

    接下來,我們將重點關注隱含流模塊。

    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 應用程序目錄:

    cd src/main/resources

    3. 最後,我們將啓動應用程序:

    npm start

    服務器默認將在端口 4200 上啓動,要更改任何模塊的端口,請更改

    "start": "ng serve"

    package.json中,例如將其設置為運行在端口 8086 上:

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

    8. 結論

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

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.