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 的项目,因此应该很容易导入运行。