www.baeldung.com/rest-api-sp…
作者:Eugen Paraschiv
譯者:oopsguy.com
1、概述
在本教程中,我們將使用 OAuth 來保護 REST API,並以一個簡單的 AngularJS 客戶端進行示範。
我們要建立的應用程式將包含了四個獨立模組:
- 授權伺服器
- 資源伺服器
- UI implicit - 一個使用 Implicit Flow 的前端應用
- UI password - 一個使用 Password Flow 的前端應用
2、授權伺服器
首先,讓我們先搭建一個簡單的 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>
<version>${oauth.version}</version>
</dependency>複製程式碼
請注意,我們使用了 spring-jdbc 和 MySQL,因為我們將使用 JDBC 來實現 token 儲存。
2.2、@EnableAuthorizationServer
現在,我們來配置負責管理 Access Token(訪問令牌)的授權伺服器:
@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
- 我們為
implicit
授權型別註冊了一個客戶端 - 我們註冊了另一個客戶端,授權了
password
、authorization_code
和refresh_token
等授權型別 - 為了使用
password
授權型別,我們需要裝配並使用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
,需要初始化資料庫 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 預設使用。
2.4、安全配置
最後,讓我們將授權伺服器變得更加安全。
當客戶端應用程式需要獲取一個 Access Token 時,在一個簡單的表單登入驅動驗證處理之後,它將執行此操作:
@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,因此您可以根據您使用的 OAuth2 flow 跳過它。
3、資源伺服器
現在,我們來討論一下資源伺服器;本質上就是我們想要消費的 REST API。
3.1、Maven 配置
我們的資源伺服器配置與之前的授權伺服器應用程式配置相同。
3.2、Token 儲存配置
接下來,我們將配置我們的 TokenStore
來訪問與授權伺服器用於儲存 Access Token 相同的資料庫:
@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());
}複製程式碼
請注意,針對這個簡單的實現,即使授權伺服器與資源伺服器是單獨的應用,我們也共享著 token 儲存的 SQL。
原因當然是資源伺服器需要能夠驗證授權伺服器發出的 Access Token 的有效性。
3.3、遠端 Token 服務
我們可以使用 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
將使用授權伺服器上的CheckTokenEndPoint
來驗證 AccessToken 並從中獲取Authentication
物件。 - 可以在 AuthorizationServerBaseURL +
/oauth/check_token
找到 - 授權伺服器可以使用任何 TokenStore 型別 [
JdbcTokenStore
、JwtTokenStore
、……] - 這不會影響到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
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 extends WebMvcConfigurerAdapter {}複製程式碼
4、前端 - Password Flow
我們現在來看看一個簡單的前端 AngularJS 客戶端實現。
我們將在這裡使用 OAuth2 Password flow - 這就是為什麼這只是一個示例,而不是一個可用於生產的應用。您會注意到,客戶端憑據被暴露在前端 - 這也是我們將來在以後的文章中要討論的。
我們從兩個簡單的頁面開始 - “index” 和 “login”;一旦使用者提供憑據,前端 JS 客戶端將使用它們從授權伺服器獲取的一個 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
現在,讓我們來看看如何獲取 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";
});
}
});複製程式碼
注意:
- 我們傳送一個 POST 到
/oauth/token
端點以獲取一個 Access Token - 我們使用客戶端憑據和 Basic Auth 驗證來訪問此端點
- 之後我們傳送使用者憑證以及客戶端 id 和授權型別引數的 URL 編碼
- 獲取 Access Token 後,我們將其儲存在一個 cookie 中
cookie 儲存在這裡特別重要,因為我們只使用 cookie 作為儲存目標,而不是直接發動身份驗證過程。這有助於防止跨站點請求偽造(CSRF)型別的攻擊和漏洞。
4.3、索引(index)頁面
以下是一個簡單的索引頁面:
<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、授權客戶端請求
由於我們需要 Access Token 為對資源的請求進行授權,我們將追加一個帶有 Access Token 的簡單授權頭:
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,使用者將跳轉到登入頁面。
5.前端 - 隱式授權(Implicit Grant)
現在,我們來看看使用了隱式授權的客戶端應用。
我們的客戶端應用是一個獨立的模組,嘗試使用隱式授權流程從授權伺服器獲取 Access Token 後訪問資源伺服器。
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 指令來處理,其可以使用隱式授權流程連線到 OAuth2 伺服器。
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
:授權伺服器 URLclient-id
:應用程式客戶端 idredirect-uri
:從授權伺服器獲 Access Token 後,要重定向到的 URIscope
:從授權伺服器請求的許可權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>複製程式碼
請注意我們如何使用 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 後,如果在資源伺服器中使用到了受保護的資源,我們將通過 Authorization
頭來使用它。
結論
我們已經學習瞭如何使用 OAuth2 授權我們的應用程式。
本教程的完整實現可以在此 GitHub 專案中找到 - 這是一個基於 Eclipse 的專案,所以應該很容易匯入執行。