使用OAuth保护REST API并使用简单的Angular客户端
1.概述
在本教程中,我们将使用OAuth保护REST API并从简单的Angular客户端使用它。
我们要构建的应用程序将包含四个独立的模块:
- 授权服务器
- 资源服务器
- UI implicit - 使用implicit流的前端应用程序
- UI密码 - 使用密码流的前端应用程序
在我们开始之前 -** 一个重要的注意事项。请记住,Spring Security核心团队正在实施新的OAuth2堆栈 - 某些方面已经完成,有些方面仍在进行中**。
这是一个快速视频,将为您提供有关该工作的一些背景信息:
https://youtu.be/YI4YCJoOF0k
2.授权服务器
首先,让我们开始将Authorization Server设置为一个简单的Spring Boot应用程序。
2.1。 Maven配置
我们将设置以下依赖项集:
<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支持的令牌存储实现。
2.2。 @EnableAuthorizationServer
现在,让我们开始配置负责管理访问令牌的授权服务器:
@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使用的数据源:
@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模式:
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项目中找到本教程的完整实现。
使用OAuth保护REST API并使用简单的Angular客户端的更多相关文章
- ASP.NET Web API与Owin OAuth:使用Access Toke调用受保护的API
在前一篇博文中,我们使用OAuth的Client Credential Grant授权方式,在服务端通过CNBlogsAuthorizationServerProvider(Authorization ...
- Access Toke调用受保护的API
ASP.NET Web API与Owin OAuth:使用Access Toke调用受保护的API 在前一篇博文中,我们使用OAuth的Client Credential Grant授权方式,在服务端 ...
- CI中如何保护RESTful API
步骤5 保护RESTful API 为了保护RESTful API,可以在application/config/rest.php中设置安全保护级别,如下所示: $config['rest_auth'] ...
- [angularjs] MVC + Web API + AngularJs 搭建简单的 CURD 框架
MVC + Web API + AngularJs 搭建简单的 CURD 框架 GitHub 地址:https://github.com/liqingwen2015/Wen.MvcSinglePage ...
- python操作三大主流数据库(12)python操作redis的api框架redis-py简单使用
python操作三大主流数据库(12)python操作redis的api框架redis-py简单使用 redispy安装安装及简单使用:https://github.com/andymccurdy/r ...
- 阿里云api调用做简单的cmdb
阿里云api调用做简单的cmdb 1 步骤 事实上就是调用阿里api.获取可用区,比方cn-hangzhou啊等等.然后在每一个区调用api 取ecs的状态信息,最好写到一个excel里面去.方便排序 ...
- C#中缓存的使用 ajax请求基于restFul的WebApi(post、get、delete、put) 让 .NET 更方便的导入导出 Excel .net core api +swagger(一个简单的入门demo 使用codefirst+mysql) C# 位运算详解 c# 交错数组 c# 数组协变 C# 添加Excel表单控件(Form Controls) C#串口通信程序
C#中缓存的使用 缓存的概念及优缺点在这里就不多做介绍,主要介绍一下使用的方法. 1.在ASP.NET中页面缓存的使用方法简单,只需要在aspx页的顶部加上一句声明即可: <%@ Outp ...
- 微服务(入门四):identityServer的简单使用(客户端授权)
IdentityServer简介(摘自Identity官网) IdentityServer是将符合规范的OpenID Connect和OAuth 2.0端点添加到任意ASP.NET核心应用程序的中间件 ...
- 运用socket实现简单的服务器客户端交互
Socket解释: 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket. Socket的英文原义是“孔”或“插座”.作为BSD UNIX的进程通信机制,取后一种意 ...
随机推荐
- SSH中的Hibernate
SSH中的Hibernate 就是DAO连接数据库对数据进行实际操作,做了架构简化,对数据库的操作.
- Java 并发 —— Java 标准库对并发的支持及 java.util.concurrent 包
0. Collections.synchronizedXxx() Java 中常用的集合框架中的实现类:HashSet/TreeSet.ArrayList/LinkedList.HashMap/Tre ...
- 关于Django ORM filter方法小结
django filter是一个过滤器,相当于SQL的select * from where. filter返回一个QuerySet对象,还可以在该对象上继续进行django orm 该有的操作. 有 ...
- poj1742硬币——多重背包可行性
题目:http://poj.org/problem?id=1742 贪心地想,1.如果一种面值已经可以被组成,则不再对它更新: 2.对于同一种面值的硬币,尽量用较少硬币(一个)更新,使后面可以用更多此 ...
- I/O:Unit1
编程,从键盘读入学生成绩(0~100分),共15名学生,计算并显示总分.平均成绩.单的学生成绩 ; sum: avg: DATA1 SEGMENT STU DB ,,,,,,,,,,,,,, SUM ...
- POJ1287(最小生成树入门题)
Networking Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 7753 Accepted: 4247 Descri ...
- [hiho第92周]Miller-Rabin素性测试的c++实现
证明: 如果n是素数,整数$a$ 与$n$ 互素,即$n$ 不整除$a$ ,则${a^{n - 1}} \equiv 1(\bmod n)$ ,如果能找到一个与$n$ 互素的整数$a$ ,是的上式不成 ...
- Ajax的属性
1.属性列表 url: (默认: 当前页地址) 发送请求的地址. type: (默认: "GET") 请求方式 ("POST" 或 "GET ...
- SSM之全局异常处理器
1. 异常处理思路 首先来看一下在springmvc中,异常处理的思路: 如上图所示,系统的dao.service.controller出现异常都通过throws Exception向上抛出,最后 ...
- Hibernate注解详细介绍
引自http://blog.csdn.net/lin_yongrui/article/details/6855394 声明实体Bean @Entity public class Flig ...