http://www.baeldung.com/rest-api-spring-oauth2-angularjs
做者:Eugen Paraschiv
譯者:http://oopsguy.comhtml
在本教程中,咱們將使用 OAuth 來保護 REST API,並以一個簡單的 AngularJS 客戶端進行示範。前端
咱們要創建的應用程序將包含了四個獨立模塊:html5
首先,讓咱們先搭建一個簡單的 Spring Boot 應用程序做爲受權服務器。java
咱們設置如下依賴:mysql
<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 存儲。git
如今,咱們來配置負責管理 Access Token(訪問令牌)的受權服務器:angularjs
@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()); } }
注意:github
JdbcTokenStore
implicit
受權類型註冊了一個客戶端password
、authorization_code
和 refresh_token
等受權類型password
受權類型,咱們須要裝配並使用 AuthenticationManager
bean接下來,讓咱們配置數據源爲 JdbcTokenStore
所用:web
@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:ajax
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 項目中找到 - 這是一個基於 Eclipse 的項目,因此應該很容易導入運行。