在本教程中,咱們將使用OAuth保護REST API並從簡單的Angular客戶端使用它。前端
在咱們開始以前 -** 一個重要的注意事項。請記住,Spring Security核心團隊正在實施新的OAuth2堆棧 - 某些方面已經完成,有些方面仍在進行中**。java
這是一個快速視頻,將爲您提供有關該工做的一些背景信息:
https://youtu.be/YI4YCJoOF0knode
首先,讓咱們開始將Authorization Server設置爲一個簡單的Spring Boot應用程序。mysql
咱們將設置如下依賴項集: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
如今,讓咱們開始配置負責管理訪問令牌的受權服務器: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使用的數據源: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默認使用它。數據庫
最後,讓咱們保護受權服務器。
當客戶端應用程序須要獲取訪問令牌時,它將在簡單的表單登陸驅動的身份驗證過程以後執行:
@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流來跳過它。
如今,讓咱們討論資源服務器;這本質上是咱們最終但願可以使用的REST API。
咱們的資源服務器配置與先前的受權服務器應用程序配置相同。
接下來,咱們將配置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支持的令牌存儲,即便受權和資源服務器是單獨的應用程序。
固然,緣由是資源服務器須要可以檢查受權服務器發出的訪問令牌的有效性。
咱們可使用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; }
接下來,讓咱們實現一個公開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; }
最後,讓咱們爲API設置一個很是基本的Web配置:
@Configuration @EnableWebMvc @ComponentScan({ "org.baeldung.web.controller" }) public class ResourceWebConfig implements WebMvcConfigurer {}
咱們如今將查看客戶端的簡單前端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邏輯。
咱們將在這裏使用OAuth2密碼流 - 這就是爲何這只是一個概念證實,而不是生產就緒的應用程序。您會注意到客戶端憑據已暴露給前端 - 這是咱們將在之後的文章中介紹的內容。
咱們的用例很簡單:一旦用戶提供其憑據,前端客戶端就會使用它們從受權服務器獲取訪問令牌。
讓咱們從位於app.service.ts的AppService開始 - 它包含服務器交互的邏輯:
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']); } }
cookie存儲在這裏特別重要,由於咱們只是將cookie用於存儲目的而不是直接驅動身份驗證過程。這有助於防止跨站點請求僞造(CSRF)類型的攻擊和漏洞。
接下來,讓咱們看一下負責登陸表單的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); }
接下來,咱們的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(); } }
最後,咱們的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'); } }
咱們的簡單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 { }
接下來,咱們將重點關注Implicit Flow模塊。
一樣,咱們將從咱們的服務開始,但此次咱們將使用庫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標頭使用它。
咱們的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(); } }
咱們的FooComponent與密碼流模塊徹底相同。
最後,咱們的AppModule:
@NgModule({ declarations: [ AppComponent, HomeComponent, FooComponent ], imports: [ BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot(), RouterModule.forRoot([ { path: '', component: HomeComponent }]) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
mvn clean install
cd src/main/resources
npm start
默認狀況下,服務器將在端口4200上啓動,以更改任何模塊的端口更改
"start": "ng serve"
在package.json中使它在端口8086上運行,例如:
"start": "ng serve --port 8086"
在本文中,咱們學習瞭如何使用OAuth2受權咱們的應用程序。
能夠在GitHub項目中找到本教程的完整實現。