做者:Eugen Paraschiv前端
轉載自公衆號:stackgchtml5
在本教程中,咱們將使用 OAuth 來保護 REST API,並經過一個簡單的 AngularJS 客戶端進行示範。java
咱們要創建的應用將包含了四個獨立模塊:mysql
首先,讓咱們先搭建一個簡單的 Spring Boot 應用做爲受權服務器。git
添加如下依賴:angularjs
<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 存儲。github
如今,咱們來配置負責管理 Access Token(訪問令牌)的受權服務器: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());
}
}
複製代碼
注意:ajax
JdbcTokenStore
implicit
受權類型註冊了一個客戶端password
、authorization_code
和 refresh_token
等受權類型password
受權類型,咱們須要裝配並使用 AuthenticationManager
bean接下來,讓咱們爲 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 默認。
最後,讓咱們將受權服務器變得更加安全。
當客戶端應用須要獲取一個 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 跳過它。
如今,咱們來討論一下資源服務器;本質上就是咱們想要消費的 REST API。
咱們的資源服務器配置與以前的受權服務器應用配置相同。
接下來,咱們將配置 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 的有效性。
咱們須要使用 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
對象。/oauth/check_token
找到JdbcTokenStore
、JwtTokenStore
、……] —— 這不會影響到 RemoteTokenService
或者資源服務器。接下來實現一個簡單控制器以暴露一個 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 extends WebMvcConfigurerAdapter {}
複製代碼
來看看一個簡單的前端 AngularJS 客戶端實現。
咱們將在這裏使用 OAuth2 Password flow —— 這就是爲何這只是一個示例,而不是一個可用於生產的應用。你會注意到,客戶端憑據被暴露在前端 —— 這也是咱們未來在之後的文章中要討論的。
咱們從兩個簡單的頁面開始 - 「index」 和 「login」;一旦用戶提供憑據,前端 JS 客戶端將使用它們從受權服務器獲取的一個 Access Token。
如下是一個簡單的登陸頁面:
<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>
複製代碼
如今,讓咱們來看看如何獲取 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";
});
}
});
複製代碼
注意:
/oauth/token
端點以獲取一個 Access Tokencookie 存儲在這裏特別重要,由於咱們只使用 cookie 做爲存儲目標,而不是直接發動身份驗證過程。這有助於防止跨站點請求僞造(CSRF)類型的攻擊和漏洞。
如下是一個簡單的主頁面:
<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>
複製代碼
因爲咱們須要 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,用戶將跳轉到登陸頁面。
如今,咱們來看看使用了隱式受權的客戶端應用。
咱們的客戶端應用是一個獨立的模塊,嘗試使用隱式受權流程從受權服務器獲取 Access Token 後訪問資源服務器。
這裏是 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 服務器。
如下是咱們的一個簡單的 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/");
}
}
複製代碼
接下來,這裏是咱們的主頁:
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>
複製代碼
這是咱們的 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 項目中找到。