http://www.baeldung.com/rest-api-spring-oauth2-angularjs
作者:Eugen Paraschiv
譯者:http://oopsguy.com
1、概述
在本教程中,我們將使用 OAuth 來保護(hù) REST API,并通過一個簡單的 AngularJS 客戶端進(jìn)行示范。
我們要建立的應(yīng)用將包含了四個獨(dú)立模塊:
- 授權(quán)服務(wù)器
- 資源服務(wù)器
- UI implicit —— 一個使用 Implicit Flow 的前端應(yīng)用
- UI password —— 一個使用 Password Flow 的前端應(yīng)用
2、授權(quán)服務(wù)器
首先,讓我們先搭建一個簡單的 Spring Boot 應(yīng)用作為授權(quán)服務(wù)器。
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>
<version>${oauth.version}</version>
</dependency>
上面使用了 spring-jdbc 和 MySQL,因為我們將使用 JDBC 來實(shí)現(xiàn) token 存儲。
2.2、@EnableAuthorizationServer
現(xiàn)在,我們來配置負(fù)責(zé)管理 Access Token(訪問令牌)的授權(quán)服務(wù)器:
@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());
}
}
注意:
- 為了持久化 token,我們使用了一個
JdbcTokenStore
- 我們?yōu)?
implicit
授權(quán)類型注冊了一個客戶端 - 我們注冊了另一個客戶端,授權(quán)了
password
、authorization_code
和refresh_token
等授權(quán)類型 - 為了使用
password
授權(quán)類型,我們需要裝配并使用AuthenticationManager
bean
2.3、數(shù)據(jù)源配置
接下來,讓我們?yōu)?JdbcTokenStore
配置數(shù)據(jù)源:
@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
,需要初始化數(shù)據(jù)庫 schema(模式),因此我們使用了 DataSourceInitializer
,和以下 SQL schema:
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 默認(rèn)。
2.4、安全配置
最后,讓我們將授權(quán)服務(wù)器變得更加安全。
當(dāng)客戶端應(yīng)用需要獲取一個 Access Token 時,在一個簡單的表單登錄驅(qū)動驗證處理之后,它將執(zhí)行此操作:
@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 flow 不需要表單登錄配置 —— 僅限于 Implicit flow,因此你可以根據(jù)你使用的 OAuth2 flow 跳過它。
3、資源服務(wù)器
現(xiàn)在,我們來討論一下資源服務(wù)器;本質(zhì)上就是我們想要消費(fèi)的 REST API。
3.1、Maven 配置
我們的資源服務(wù)器配置與之前的授權(quán)服務(wù)器應(yīng)用配置相同。
3.2、Token 存儲配置
接下來,我們將配置 TokenStore
來訪問與授權(quán)服務(wù)器用于存儲 Access Token 相同的數(shù)據(jù)庫:
@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());
}
請注意,針對這個簡單的實(shí)現(xiàn),即使授權(quán)服務(wù)器與資源服務(wù)器是單獨(dú)的應(yīng)用,我們也共享著 token 存儲的 SQL。
原因當(dāng)然是資源服務(wù)器需要能夠驗證授權(quán)服務(wù)器發(fā)出的 Access Token 的有效性。
3.3、遠(yuǎn)程 Token 服務(wù)
我們需要使用 RemoteTokeServices
,而不是 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
將使用授權(quán)服務(wù)器上的CheckTokenEndPoint
來驗證 AccessToken 并從中獲取Authentication
對象。 - 可以在 AuthorizationServerBaseURL +
/oauth/check_token
找到 - 授權(quán)服務(wù)器可以使用任何 TokenStore 類型 [
JdbcTokenStore
、JwtTokenStore
、……] —— 這不會影響到RemoteTokenService
或者資源服務(wù)器。
3.4、一個簡單的控制器
接下來實(shí)現(xiàn)一個簡單控制器以暴露一個 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(范圍、作用域或權(quán)限)訪問此資源。
我們還需要開啟全局方法保護(hù)并配置 MethodSecurityExpressionHandler
:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig
extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
以下是基礎(chǔ)的 Foo
資源:
public class Foo {
private long id;
private String name;
}
3.5、Web 配置
最后,為 API 設(shè)置一個非常基本的 Web 配置:
@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig extends WebMvcConfigurerAdapter {}
4、前端 - Password Flow
來看看一個簡單的前端 AngularJS 客戶端實(shí)現(xiàn)。
我們將在這里使用 OAuth2 Password flow —— 這就是為什么這只是一個示例,而不是一個可用于生產(chǎn)的應(yīng)用。你會注意到,客戶端憑據(jù)被暴露在前端 —— 這也是我們將來在以后的文章中要討論的。
我們從兩個簡單的頁面開始 - “index” 和 “l(fā)ogin”;一旦用戶提供憑據(jù),前端 JS 客戶端將使用它們從授權(quán)服務(wù)器獲取的一個 Access Token。
4.1、登錄頁面
以下是一個簡單的登錄頁面:
<body ng-app="myApp" ng-controller="mainCtrl">
<h1>Login</h1>
<label>Username</label><input ng-model="data.username"/>
<label>Password</label><input type="password" ng-model="data.password"/>
<a href="#" ng-click="login()">Login</a>
</body>
4.2、獲取 Access Token
現(xiàn)在,讓我們來看看如何獲取 Access Token:
var app = angular.module('myApp', ["ngResource","ngRoute","ngCookies"]);
app.controller('mainCtrl',
function($scope, $resource, $http, $httpParamSerializer, $cookies) {
$scope.data = {
grant_type:"password",
username: "",
password: "",
client_id: "clientIdPassword"
};
$scope.encoded = btoa("clientIdPassword:secret");
$scope.login = function() {
var req = {
method: 'POST',
url: "http://localhost:8080/spring-security-oauth-server/oauth/token",
headers: {
"Authorization": "Basic " + $scope.encoded,
"Content-type": "application/x-www-form-urlencoded; charset=utf-8"
},
data: $httpParamSerializer($scope.data)
}
$http(req).then(function(data){
$http.defaults.headers.common.Authorization =
'Bearer ' + data.data.access_token;
$cookies.put("access_token", data.data.access_token);
window.location.href="index";
});
}
});
注意:
- 我們發(fā)送一個 POST 到
/oauth/token
端點(diǎn)以獲取一個 Access Token - 我們使用客戶端憑據(jù)和 Basic Auth 驗證來訪問此端點(diǎn)
- 之后我們發(fā)送用戶憑證以及客戶端 id 和授權(quán)類型參數(shù)的 URL 編碼
- 獲取 Access Token 后,我們將其存儲在一個 cookie 中
cookie 存儲在這里特別重要,因為我們只使用 cookie 作為存儲目標(biāo),而不是直接發(fā)動身份驗證過程。這有助于防止跨站點(diǎn)請求偽造(CSRF)類型的攻擊和漏洞。
4.3、主頁面
以下是一個簡單的主頁面:
<body ng-app="myApp" ng-controller="mainCtrl">
<h1>Foo Details</h1>
<label>ID</label><span>{{foo.id}}</span>
<label>Name</label><span>{{foo.name}}</span>
<a href="#" ng-click="getFoo()">New Foo</a>
</body>
4.4、授權(quán)客戶端請求
由于我們需要 Access Token 為對資源的請求進(jìn)行授權(quán),我們將追加一個帶有 Access Token 的簡單授權(quán)頭:
var isLoginPage = window.location.href.indexOf("login") != -1;
if(isLoginPage){
if($cookies.get("access_token")){
window.location.href = "index";
}
} else{
if($cookies.get("access_token")){
$http.defaults.headers.common.Authorization =
'Bearer ' + $cookies.get("access_token");
} else{
window.location.href = "login";
}
}
沒有找到 cookie,用戶將跳轉(zhuǎn)到登錄頁面。
5.前端 —— 隱式授權(quán)(Implicit Grant)
現(xiàn)在,我們來看看使用了隱式授權(quán)的客戶端應(yīng)用。
我們的客戶端應(yīng)用是一個獨(dú)立的模塊,嘗試使用隱式授權(quán)流程從授權(quán)服務(wù)器獲取 Access Token 后訪問資源服務(wù)器。
5.1、Maven 配置
這里是 pom.xml
依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
注意:我們不需要 OAuth 依賴,因為我們將使用 AngularJS 的 OAuth-ng 指令來處理,其可以使用隱式授權(quán)流程連接到 OAuth2 服務(wù)器。
5.2、Web 配置
以下是我們的一個簡單的 Web 配置:
@Configuration
@EnableWebMvc
public class UiWebConfig extends WebMvcConfigurerAdapter {
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
super.addViewControllers(registry);
registry.addViewController("/index");
registry.addViewController("/oauthTemplate");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/resources/");
}
}
5.3、主頁
接下來,這里是我們的主頁:
OAuth-ng 指令需要:
-
site
:授權(quán)服務(wù)器 URL -
client-id
:應(yīng)用客戶端 id -
redirect-uri
:從授權(quán)服務(wù)器獲 Access Token 后,要重定向到的 URI -
scope
:從授權(quán)服務(wù)器請求的權(quán)限 -
template
:渲染自定義 HTML 模板
<body ng-app="myApp" ng-controller="mainCtrl">
<oauth
site="http://localhost:8080/spring-security-oauth-server"
client-id="clientId"
redirect-uri="http://localhost:8080/spring-security-oauth-ui-implicit/index"
scope="read"
template="oauthTemplate">
</oauth>
<h1>Foo Details</h1>
<label >ID</label><span>{{foo.id}}</span>
<label>Name</label><span>{{foo.name}}</span>
</div>
<a href="#" ng-click="getFoo()">New Foo</a>
<script
src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js">
</script>
<script
src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-resource.min.js">
</script>
<script
src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-route.min.js">
</script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/ngStorage/0.3.9/ngStorage.min.js">
</script>
<script th:src="@{/resources/oauth-ng.js}"></script>
</body>
請注意我們?nèi)绾问褂?OAuth-ng 指令來獲取 Access Token。
另外,以下是一個簡單的 oauthTemplate.html
:
<div>
<a href="#" ng-show="show=='logged-out'" ng-click="login()">Login</a>
<a href="#" ng-show="show=='denied'" ng-click="login()">Access denied. Try again.</a>
</div>
5.4、AngularJS App
這是我們的 AngularJS app:
var app = angular.module('myApp', ["ngResource","ngRoute","oauth"]);
app.config(function($locationProvider) {
$locationProvider.html5Mode({
enabled: true,
requireBase: false
}).hashPrefix('!');
});
app.controller('mainCtrl', function($scope,$resource,$http) {
$scope.$on('oauth:login', function(event, token) {
$http.defaults.headers.common.Authorization= 'Bearer ' + token.access_token;
});
$scope.foo = {id:0 , name:"sample foo"};
$scope.foos = $resource(
"http://localhost:8080/spring-security-oauth-resource/foos/:fooId",
{fooId:'@id'});
$scope.getFoo = function(){
$scope.foo = $scope.foos.get({fooId:$scope.foo.id});
}
});
請注意,在獲取 Access Token 后,如果在資源服務(wù)器中使用到了受保護(hù)的資源,我們將通過 Authorization
頭來使用它。
結(jié)論
我們已經(jīng)學(xué)習(xí)了如何使用 OAuth2 授權(quán)我們的應(yīng)用。
本教程的完整實(shí)現(xiàn)可以在此 GitHub 項目中找到。