在本教程中,咱们将使用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项目中找到本教程的完整实现。