http://blog.csdn.net/haiyan_qi/article/details/52384734
**************************************************
1.概述
運用AngularJS和springboot技術實現的demo:
https://github.com/qihaiyan/ng-boot-oauth
在這個教程中,我們將用oauth2對REST API進行安全控制,並在一個簡單的AngularJS客戶端程序中使用。
我們將要構建的應用包含四個獨立的模塊:
- Authorization Server
- Resource Server
- UI implicit – 使用 Implicit Flow 的前端應用
- UI password – 使用 Password Flow 的前端應用
2.認證服務
我們開始用spring Boot構建一個認證服務。
2.1 Maven配置
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 store。
2.2. @EnableAuthorizationServer
配置用於管理access tokens的認證服務:
@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來存儲tokens
- 注冊一個采用“implicit”授權方式的客戶端
- 注冊另一個采用 “password“, “authorization_code” 和 “refresh_token”授權方式的客戶端
- 為了使用“password”授權方式,我們需要通過spring的@Autowired注解來注入和使用AuthenticationManagerbean
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 時,我們需要初始化數據庫並創建相關的表來存儲token數據,通過使用DataSourceInitializer 和下面的語句來實現:
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) );
2.4. 安全權限配置
最后,為認證服務增加安全權限控制功能。
當客戶端程序需要獲取Access Token時,會執行下面一個簡單的from-login驅動的認證過程:
@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的Password flow模式,from-login配置不是必須的,只對Implicit flow是必須的。
3. Resource 服務
Resource 服務用於提供REST API。
3.1. Maven 配置
Resource 服務的Maven配置與前面的認證服務的Maven配置相同。
3.2. Token Store 配置
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()); }
為了簡化起見,雖然認證服務和Resource 服務是兩個獨立的應用程序,但是用了同一個數據庫,原因是Resource 服務需要驗證認證服務中生成的access token。
3.3. Remote Token Service
除了在Resource服務中使用TokenStore 之外,還可以使用RemoteTokeServices:
@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會使用認證服務中的CheckToken節點去驗證AccessToken並獲取 Authentication對象.
*CheckToken節點的訪問地址為:認證服務器的URL +”/oauth/check_token“ - 認證服務可以使用任意的TokenStore類型,包括 [JdbcTokenStore, JwtTokenStore, …] ,不會影響到RemoteTokenService 或 Resource 服務
3.4. 一個簡單的 Controller
下面用一個簡單的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”權限。
同時需要啟用全局安全權限控制,並且需要配置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. 客戶端應用 – 用戶密碼模式
下面來看一下用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
- 通過 url encode 對用戶憑證、客戶端 id 和 grant type進行編碼
- 得到Access Token后將其存放到cookie中
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. 對客戶端請求進行授權
因為Resource服務需要使用access token對客戶端請求進行授權驗證,我們用access token在http頭中增加一個簡單的authorization header:
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,將重定向到login頁面。
5. 客戶端應用 – 簡化模式
下面來看一下采用OAuth2簡化模式的客戶端程序。
這個程序是一個單獨的模塊,采用oauth2的簡化模式,從認證服務中獲取access token,然后用這個access token去訪問Resource服務。
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>
這兒不需要spring的oauth模塊,我們將使用AngularJS的OAuth-ng directive,以implicit grant flow方式去訪問oauth2 認證服務。
5.2. 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. Home 頁面
OAuth-ng directive需要以下參數:
- site: 認證服務的URL
- client-id: 客戶端應用的 client id
- redirect-uri: 從認證服務獲取到access token后,重定向到此URI
- scope: 從認證服務獲取到的權限
- template: AngularJS的頁面模板
<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 directive來獲取AccessToken,這是一個簡單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 應用
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后,通過http頭的Authorization header來訪問Resrouce服務中提供的接口服務。
6. 總結
至此我們闡述了如果使用OAuth2來為應用程序提供安全權限控制功能。
本文的所有實例代碼在 the github project 中 - 這是一個eclipse項目,可以直接導入並運行。
https://github.com/Baeldung/spring-security-oauth