автор: Евгений Параскив
Перепечатано из публичного аккаунта: stackgc
1 Обзор
В этом руководстве мы будем использовать OAuth для защиты REST API и продемонстрируем его с помощью простого клиента AngularJS.
Приложение, которое мы собираемся создать, будет состоять из четырех отдельных модулей:
- Сервер авторизации
- сервер ресурсов
- Неявный пользовательский интерфейс — внешнее приложение, использующее неявный поток
- Пароль пользовательского интерфейса — внешнее приложение, использующее 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 для хранения токенов.
2.2, @enable сервер авторизации
Теперь настроим сервер авторизации, отвечающий за управление 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());
}
}
Уведомление:
- Чтобы сохранить токен, мы используем
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
, схема базы данных должна быть инициализирована, поэтому мы используемDataSourceInitializer
и следующую схему SQL:
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
фасоль --Мы можем просто использовать schema.sql — по умолчанию Spring Boot.
2.4, конфигурация безопасности
Наконец, давайте сделаем сервер авторизации более безопасным.
Когда клиентскому приложению необходимо получить токен доступа, после простого процесса проверки, основанного на входе в систему, оно сделает следующее:
@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 вы используете.
3. Сервер ресурсов
Теперь давайте поговорим о серверах ресурсов, то есть о REST API, которые мы хотим использовать.
3.1, конфигурация Maven
Перед нашей конфигурацией сервера ресурсов и сервером авторизации настроено одно и то же приложение.
3.2, Конфигурация хранилища токенов
Далее будем настраивать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());
}
Обратите внимание, что для этой простой реализации, несмотря на то, что сервер авторизации и сервер ресурсов являются отдельными приложениями,Мы также делимся SQL для хранения токенов.
Причина, конечно же, в том, что ресурсный сервер должен иметь возможностьПроверьте токен доступа, выданный сервером авторизации.эффективность.
3.3. Удаленная служба токенов
нам нужно использовать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
Область (диапазон, область или разрешения) доступа к этому ресурсу.
Нам также необходимо открыть путь к защите и глобальной конфигурации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. Веб-конфигурация
Наконец, настройте очень простую веб-конфигурацию для API:
@Configuration
@EnableWebMvc
@ComponentScan({ "org.baeldung.web.controller" })
public class ResourceWebConfig extends WebMvcConfigurerAdapter {}
4. Внешний интерфейс — поток паролей
Давайте взглянем на простую клиентскую реализацию AngularJS.
Здесь мы будем использовать поток паролей OAuth2 — вот почемуЭто просто пример, а не готовое к производству приложение.. Вы заметите, что учетные данные клиента отображаются на внешнем интерфейсе — это мы обсудим в следующей статье.
Начнем с двух простых страниц — «index» и «login»; как только пользователь введет учетные данные, внешние JS-клиенты будут использовать токен доступа, который они получают от сервера авторизации.
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. Получить токен доступа
Теперь давайте посмотрим, как получить токен доступа:
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
конечная точка для получения токена доступа - Мы используем учетные данные клиента и аутентификацию Basic Auth для доступа к этой конечной точке.
- После этого мы отправляем учетные данные пользователя вместе с идентификатором клиента в кодировке URL и параметрами типа авторизации.
- После получения Access Token ставим егохранится в куки
Хранение файлов cookie здесь особенно важно, потому что мы используем файлы cookie только в качестве цели хранения, а не инициируем процесс аутентификации напрямую.Это помогает предотвратить атаки и уязвимости типа подделки межсайтовых запросов (CSRF)..
4.3 Главная страница
Вот простая главная страница:
<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. Авторизация запросов клиентов
Поскольку нам нужен токен доступа для авторизации запросов на ресурсы, мы добавим простой заголовок авторизации с токеном доступа:
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. Интерфейс — неявный грант
Теперь давайте рассмотрим клиентское приложение, использующее неявную авторизацию.
Наше клиентское приложение представляет собой автономный модуль, который пытается получить доступ к серверу ресурсов после получения маркера доступа с сервера авторизации с использованием неявного потока авторизации.
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. Веб-конфигурация
Вот простая наша веб-конфигурация:
@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
: URL сервера авторизации -
client-id
: идентификатор клиента приложения -
redirect-uri
: URI для перенаправления после получения токена доступа с сервера авторизации. -
scope
: разрешения запрашиваются с сервера авторизации -
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Команда для получения токена доступа.
Кроме того, следующее простое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
Вот наше приложение 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});
}
});
Обратите внимание, что после получения токена доступа, если на сервере ресурсов используется защищенный ресурс, мы передадимAuthorization
голова, чтобы использовать его.
В заключение
Мы узнали, как авторизовать наше приложение с помощью OAuth2.
Полную реализацию этого руководства можно найти по адресуэтот проект GitHubнайти в.