Весенний API REST – OAuth2 – Angular (с использованием стека наследия Spring Security OAuth)
1. Обзор
В этом учебнике мы защитим API REST с OAuth и будем потреблять его у простого клиента Angular.
Приложение, которое мы собираемся создать, будет состоять из четырех отдельных модулей:
- Сервер авторизации
- Ресурсный сервер
- Пользовательский интерфейс неявный – передний конец приложения с использованием неявного потока
- Пароль пользовательского интерфейса — переднее приложение с использованием потока паролей
Примечание : в этой статье используются Весенний проект наследия OAuth . Для версии этой статьи с помощью нового стека Spring Security 5, посмотрите на нашу статью Весенний REST API – OAuth2 – Angular .
Хорошо, давайте запрыгнем прямо с этого.
2. Сервер авторизации
Во-первых, давайте начнем настройку сервера авторизации в качестве простого приложения Spring Boot.
2.1. Конфигурация Maven
Мы наберем следующий набор зависимостей:
org.springframework.boot spring-boot-starter-web org.springframework spring-jdbc mysql mysql-connector-java runtime org.springframework.security.oauth spring-security-oauth2
Обратите внимание, что мы используем spring-jdbc и MyS’L, потому что мы собираемся использовать поддержку JDBC реализации магазина токенов.
2.2. @EnableAuthorizationServer
Теперь давайте начнем настраивать сервер авторизации, отвечающий за управление токенами доступа:
@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
- Мы зарегистрировали клиента для « неявное ” тип гранта
- Мы зарегистрировали другого клиента и уполномочили « пароль “, ” authorization_code ” и ” refresh_token ” Типы грантов
- Для того, чтобы использовать ” пароль ” грант типа мы должны провода и использовать АутентификацияМенагер боб
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 – и следующая схема S’L:
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) );
Обратите внимание, что нам не обязательно нужна явная База данныхПопулятор фасоль – мы могли бы просто использовать схема.sql – который Весенняя загрузка использует по умолчанию .
2.4. Конфигурация безопасности
Наконец, давайте защитим сервер авторизации.
Когда клиентскому приложению необходимо приобрести токен Access Token, оно сделает это после простого процесса, управляемого системой auth:
@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. Ресурсный сервер
Теперь давайте обсудим ресурсный сервер; по сути, это API REST, который мы в конечном счете хотим иметь возможность потреблять.
3.1. Конфигурация Maven
Наша конфигурация Resource Server такая же, как и предыдущая конфигурация приложения Authorization Server.
3.2. Конфигурация магазина токенов
Далее мы настроили наши ТокенСтор для доступа к той же базе данных, которую сервер авторизации использует для хранения токенов доступа:
@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()); }
Обратите внимание, что для этой простой реализации мы делимся магазином токенов, поддерживаемым S’L несмотря на то, что серверы авторизации и ресурсов являются отдельными приложениями.
Причина, конечно, в том, что ресурсный сервер должен быть в состоянии проверить достоверность токенов доступа выдается сервером авторизации.
3.3. Дистанционное обслуживание токенов
Вместо того, чтобы использовать ТокенСтор в нашем ресурсном сервере мы можем использовать 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; }
Обратите внимание, что:
- Этот ДистанционноеТокенСервис будет использовать CheckTokenEndPoint на сервере авторизации для проверки AccessToken и получения Проверка объект из него.
- С их авторами можно усмотреть по телефону AuthorizationServerBaseURL /oauth/check_token “
- Сервер авторизации может использовать любой тип TokenStore и JdbcTokenStore , JwtTokenStore , …] – это не повлияет на ДистанционноеТокенСервис или Ресурсный сервер.
3.4. Контроллер образца
Далее давайте реализуем простой контроллер, разоблачающий Фу ресурс:
@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)); } }
Обратите внимание, как клиент нуждается в “Читать” для доступа к этому ресурсу.
Нам также необходимо обеспечить глобальную безопасность методов и настроить МетодSecurityExpressionHandler :
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class OAuth2ResourceServerConfig extends GlobalMethodSecurityConfiguration { @Override protected MethodSecurityExpressionHandler createExpressionHandler() { return new OAuth2MethodSecurityExpressionHandler(); } }
И вот наш основной Фу ресурс:
public class Foo { private long id; private String name; }
3.5. Веб-конфигурация
Наконец, давайте наимем очень базовую конфигурацию веб-страниц для API:
@Configuration @EnableWebMvc @ComponentScan({ "org.baeldung.web.controller" }) public class ResourceWebConfig implements WebMvcConfigurer {}
4. Передний конец – Настройка
Теперь мы будем смотреть на простой передний конец Angular реализации для клиента.
Во-первых, мы будем использовать Угловый КЛИ для создания и управления нашими передними модулями.
Во-первых, мы установим узел и npm – как угловой CLI является npm инструмент.
Тогда нам нужно использовать интерфейс-maven-plugin построить наш угловой проект с использованием maven:
com.github.eirslett frontend-maven-plugin 1.3 v6.10.2 3.10.10 src/main/resources install node and npm install-node-and-npm npm install npm npm run build npm run build
И, наконец, создать новый модуль с использованием углового CLI:
ng new oauthApp
Обратите внимание, что у нас будет два передних модуля – один для потока паролей, а другой для неявного потока.
В следующих разделах мы обсудим логику приложения Angular для каждого модуля.
5. Поток паролей с помощью углового
Мы будем использовать OAuth2 Пароль поток здесь – именно поэтому это всего лишь доказательство концепции, а не готовое к производству приложение . Вы заметите, что учетные данные клиентов подвергаются передней части – это то, что мы будем рассматривать в будущей статье.
Наш пример использования прост: как только пользователь предоставляет свои учетные данные, интерфейс клиент использует их для приобретения токена доступа с сервера авторизации.
5.1. Служба приложений
Начнем с наших AppService – расположен в app.service.ts – который содержит логику взаимодействия с сервером:
- получитьAccessToken () : для получения токена Access с учетом учетных данных пользователей
- сохранитьТокен () : для сохранения маркера доступа в файле cookie с помощью библиотеки ng2-cookies
- getResource () : чтобы получить объект Foo с сервера, используя его ID
- checkCredentials () : проверить, вошел пользователь или нет
- логоут () : удалить маркер доступа cookie и выйти из системы пользователя
export class Foo { constructor( public id: number, public name: string) { } } @Injectable() export class AppService { constructor( private _router: Router, private _http: Http){} obtainAccessToken(loginData){ let params = new URLSearchParams(); params.append('username',loginData.username); params.append('password',loginData.password); params.append('grant_type','password'); params.append('client_id','fooClientIdPassword'); let headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': 'Basic '+btoa("fooClientIdPassword:secret")}); let options = new RequestOptions({ headers: headers }); this._http.post('http://localhost:8081/spring-security-oauth-server/oauth/token', params.toString(), options) .map(res => res.json()) .subscribe( data => this.saveToken(data), err => alert('Invalid Credentials')); } saveToken(token){ var expireDate = new Date().getTime() + (1000 * token.expires_in); Cookie.set("access_token", token.access_token, expireDate); this._router.navigate(['/']); } getResource(resourceUrl) : Observable{ var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': 'Bearer '+Cookie.get('access_token')}); var options = new RequestOptions({ headers: headers }); return this._http.get(resourceUrl, options) .map((res:Response) => res.json()) .catch((error:any) => Observable.throw(error.json().error || 'Server error')); } checkCredentials(){ if (!Cookie.check('access_token')){ this._router.navigate(['/login']); } } logout() { Cookie.delete('access_token'); this._router.navigate(['/login']); } }
Обратите внимание, что:
- Чтобы получить токен доступа, мы отправляем POST к ” /oauth/токен ” конечная точка
- Мы используем учетные данные клиентов и Basic Auth, чтобы попасть в эту конечную точку
- Затем мы отправляем учетные данные пользователей вместе с идентификатором клиента и параметрами типа гранта, закодированными
- После того, как мы получаем токен доступа – мы храним его в печенье
Хранение файлов cookie особенно важно здесь, потому что мы используем файлы cookie только для целей хранения, а не для непосредственного вождения процесса проверки подлинности. Это помогает защитить от атак и уязвимостей при подделке запросов на кросс-сайт (CSRF).
5.2. Компонент входа
Далее, давайте посмотрим на наши ЛогинКомпонент которая отвечает за форму входа:
@Component({ selector: 'login-form', providers: [AppService], template: `Login
` }) export class LoginComponent { public loginData = {username: "", password: ""}; constructor(private _service:AppService) {} login() { this._service.obtainAccessToken(this.loginData); }
5.3. Домашний компонент
Далее, наш ГлавнаяКомпонент который отвечает за отображение и манипулирование нашей домашней страницей:
@Component({ selector: 'home-header', providers: [AppService], template: `Welcome !! Logout` }) export class HomeComponent { constructor( private _service:AppService){} ngOnInit(){ this._service.checkCredentials(); } logout() { this._service.logout(); } }
5.4. Foo Компонент
Наконец, наши FooComponent для отображения наших foo детали:
@Component({ selector: 'foo-details', providers: [AppService], template: `Foo Details
{{foo.id}} {{foo.name}} ` }) export class FooComponent { public foo = new Foo(1,'sample foo'); private foosUrl = 'http://localhost:8082/spring-security-oauth-resource/foos/'; constructor(private _service:AppService) {} getFoo(){ this._service.getResource(this.foosUrl+this.foo.id) .subscribe( data => this.foo = data, error => this.foo.name = 'Error'); } }
5.5. Компонент приложения
Наша простая AppComponent выступать в качестве корневого компонента:
@Component({ selector: 'app-root', template: `` }) export class AppComponent {}
И AppModule где мы обертывание всех наших компонентов, услуг и маршрутов:
@NgModule({ declarations: [ AppComponent, HomeComponent, LoginComponent, FooComponent ], imports: [ BrowserModule, FormsModule, HttpModule, RouterModule.forRoot([ { path: '', component: HomeComponent }, { path: 'login', component: LoginComponent }]) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
6. Неявный поток
Далее мы сосредоточимся на модуле Implicit Flow.
6.1. Служба приложений
Аналогичным образом, мы начнем с нашего сервиса, но на этот раз мы будем использовать библиотечные угловой-oauth2-oidc вместо того, чтобы получить токен доступа самостоятельно:
@Injectable() export class AppService { constructor( private _router: Router, private _http: Http, private oauthService: OAuthService){ this.oauthService.loginUrl = 'http://localhost:8081/spring-security-oauth-server/oauth/authorize'; this.oauthService.redirectUri = 'http://localhost:8086/'; this.oauthService.clientId = "sampleClientId"; this.oauthService.scope = "read write foo bar"; this.oauthService.setStorage(sessionStorage); this.oauthService.tryLogin({}); } obtainAccessToken(){ this.oauthService.initImplicitFlow(); } getResource(resourceUrl) : Observable{ var headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': 'Bearer '+this.oauthService.getAccessToken()}); var options = new RequestOptions({ headers: headers }); return this._http.get(resourceUrl, options) .map((res:Response) => res.json()) .catch((error:any) => Observable.throw(error.json().error || 'Server error')); } isLoggedIn(){ if (this.oauthService.getAccessToken() === null){ return false; } return true; } logout() { this.oauthService.logOut(); location.reload(); } }
Обратите внимание, как после получения токена доступа мы используем его через Авторизация заголовок всякий раз, когда мы потребляем защищенные ресурсы из сервера ресурсов.
6.2. Домашний компонент
Наша ГлавнаяКомпонент для обработки нашей простой домашней странице:
@Component({ selector: 'home-header', providers: [AppService], template: ` ` }) export class HomeComponent { public isLoggedIn = false; constructor( private _service:AppService){} ngOnInit(){ this.isLoggedIn = this._service.isLoggedIn(); } login() { this._service.obtainAccessToken(); } logout() { this._service.logout(); } }
6.3. Foo Компонент
Наша FooComponent точно так же, как в модуле потока паролей.
6.4. Модуль приложения
Наконец, наши AppModule :
@NgModule({ declarations: [ AppComponent, HomeComponent, FooComponent ], imports: [ BrowserModule, FormsModule, HttpModule, OAuthModule.forRoot(), RouterModule.forRoot([ { path: '', component: HomeComponent }]) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
7. Вы запустите передний конец
1. Для запуска любого из наших передних модулей, мы должны построить приложение в первую очередь:
mvn clean install
2. Тогда нам нужно перейти к нашему каталогу Angular app:
cd src/main/resources
3. Наконец, мы начнем наше приложение:
npm start
Сервер начнет по умолчанию на порт 4200, чтобы изменить порт любого модуля изменить
"start": "ng serve"
в package.json чтобы запустить его на порт 8086, например:
"start": "ng serve --port 8086"
8. Заключение
В этой статье мы узнали, как авторизовать наше приложение с помощью OAuth2.
Полную реализацию этого учебника можно найти в проект GitHub .