Перейти к основному содержимому

Страница входа в Spring Security с Angular

· 7 мин. чтения

1. Обзор

В этом руководстве мы создадим страницу входа с использованием Spring Security с помощью:

  • AngularJS
  • Угловые 2, 4, 5 и 6

Пример приложения, которое мы собираемся здесь обсудить, состоит из клиентского приложения, которое взаимодействует со службой REST и защищено базовой HTTP-аутентификацией.

2. Конфигурация безопасности Spring

Прежде всего, давайте настроим REST API с Spring Security и Basic Auth:

Вот как это настроено:

@Configuration
@EnableWebSecurity
public class BasicAuthConfiguration
extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER");
}

@Override
protected void configure(HttpSecurity http)
throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}

Теперь давайте создадим конечные точки. У нашего REST-сервиса будет два — один для входа в систему, а другой — для получения пользовательских данных:

@RestController
@CrossOrigin
public class UserController {

@RequestMapping("/login")
public boolean login(@RequestBody User user) {
return
user.getUserName().equals("user") && user.getPassword().equals("password");
}

@RequestMapping("/user")
public Principal user(HttpServletRequest request) {
String authToken = request.getHeader("Authorization")
.substring("Basic".length()).trim();
return () -> new String(Base64.getDecoder()
.decode(authToken)).split(":")[0];
}
}

Точно так же вы можете ознакомиться с другим нашим руководством по Spring Security OAuth2 , если вы заинтересованы во внедрении сервера OAuth2 для авторизации.

3. Настройка клиента Angular

Теперь, когда мы создали службу REST, давайте настроим страницу входа с различными версиями клиента Angular.

Примеры, которые мы здесь увидим, используют npm для управления зависимостями и nodejs для запуска приложения.

Angular использует одностраничную архитектуру, в которой все дочерние компоненты (в нашем случае это логин и домашний компонент) внедряются в общий родительский DOM.

В отличие от AngularJS, который использует JavaScript, Angular версии 2 использует TypeScript в качестве основного языка. Следовательно, приложению также требуются определенные вспомогательные файлы, необходимые для его правильной работы.

Из-за постепенных улучшений Angular необходимые файлы различаются от версии к версии.

Давайте познакомимся с каждым из них:

  • systemjs.config.js — системные конфигурации (версия 2)
  • package.json — зависимости модуля узла (начиная с версии 2)
  • tsconfig.json — конфигурации Typescript корневого уровня (начиная с версии 2)
  • tsconfig.app.json — конфигурации Typescript на уровне приложения (начиная с версии 4)
  • .angular- cli.json — конфигурации Angular CLI (версии 4 и 5)
  • angular.json — конфигурации Angular CLI (начиная с версии 6)

4. Страница входа

4.1. Использование AngularJS

Давайте создадим файл index.html и добавим в него соответствующие зависимости:

<html ng-app="app">
<body>
<div ng-view></div>

<script src="//code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="//code.angularjs.org/1.6.0/angular.min.js"></script>
<script src="//code.angularjs.org/1.6.0/angular-route.min.js"></script>
<script src="app.js"></script>
<script src="home/home.controller.js"></script>
<script src="login/login.controller.js"></script>
</body>
</html>

Поскольку это одностраничное приложение, все дочерние компоненты будут добавлены в элемент div с атрибутом ng-view на основе логики маршрутизации.

Теперь давайте создадим app.js , который определяет сопоставление URL-адреса с компонентом:

(function () {
'use strict';

angular
.module('app', ['ngRoute'])
.config(config)
.run(run);

config.$inject = ['$routeProvider', '$locationProvider'];
function config($routeProvider, $locationProvider) {
$routeProvider.when('/', {
controller: 'HomeController',
templateUrl: 'home/home.view.html',
controllerAs: 'vm'
}).when('/login', {
controller: 'LoginController',
templateUrl: 'login/login.view.html',
controllerAs: 'vm'
}).otherwise({ redirectTo: '/login' });
}

run.$inject = ['$rootScope', '$location', '$http', '$window'];
function run($rootScope, $location, $http, $window) {
var userData = $window.sessionStorage.getItem('userData');
if (userData) {
$http.defaults.headers.common['Authorization']
= 'Basic ' + JSON.parse(userData).authData;
}

$rootScope
.$on('$locationChangeStart', function (event, next, current) {
var restrictedPage
= $.inArray($location.path(), ['/login']) === -1;
var loggedIn
= $window.sessionStorage.getItem('userData');
if (restrictedPage && !loggedIn) {
$location.path('/login');
}
});
}
})();

Компонент входа состоит из двух файлов: login.controller.js и login.view.html.

Смотрим на первый:

<h2>Login</h2>
<form name="form" ng-submit="vm.login()" role="form">
<div>
<label for="username">Username</label>
<input type="text" name="username"
id="username" ng-model="vm.username" required />
<span ng-show="form.username.$dirty
&& form.username.$error.required">Username is required</span>
</div>
<div>
<label for="password">Password</label>
<input type="password"
name="password" id="password" ng-model="vm.password" required />
<span ng-show="form.password.$dirty
&& form.password.$error.required">Password is required</span>
</div>
<div class="form-actions">
<button type="submit"
ng-disabled="form.$invalid || vm.dataLoading">Login</button>
</div>
</form>

и второй:

(function () {
'use strict';
angular
.module('app')
.controller('LoginController', LoginController);

LoginController.$inject = ['$location', '$window', '$http'];
function LoginController($location, $window, $http) {
var vm = this;
vm.login = login;

(function initController() {
$window.localStorage.setItem('token', '');
})();

function login() {
$http({
url: 'http://localhost:8082/login',
method: "POST",
data: {
'userName': vm.username,
'password': vm.password
}
}).then(function (response) {
if (response.data) {
var token
= $window.btoa(vm.username + ':' + vm.password);
var userData = {
userName: vm.username,
authData: token
}
$window.sessionStorage.setItem(
'userData', JSON.stringify(userData)
);
$http.defaults.headers.common['Authorization']
= 'Basic ' + token;
$location.path('/');
} else {
alert("Authentication failed.")
}
});
};
}
})();

Контроллер вызовет службу REST, передав имя пользователя и пароль. После успешной аутентификации он закодирует имя пользователя и пароль и сохранит закодированный токен в хранилище сеансов для использования в будущем.

Как и компонент входа в систему, домашний компонент также состоит из двух файлов, home.view.html :

<h1>Hi {{vm.user}}!</h1>
<p>You're logged in!!</p>
<p><a href="#!/login" class="btn btn-primary" ng-click="logout()">Logout</a></p>

и home.controller.js:

(function () {
'use strict';
angular
.module('app')
.controller('HomeController', HomeController);

HomeController.$inject = ['$window', '$http', '$scope'];
function HomeController($window, $http, $scope) {
var vm = this;
vm.user = null;

initController();

function initController() {
$http({
url: 'http://localhost:8082/user',
method: "GET"
}).then(function (response) {
vm.user = response.data.name;
}, function (error) {
console.log(error);
});
};

$scope.logout = function () {
$window.sessionStorage.setItem('userData', '');
$http.defaults.headers.common['Authorization'] = 'Basic';
}
}
})();

Домашний контроллер запросит пользовательские данные, передав заголовок авторизации . Наша служба REST вернет пользовательские данные только в том случае, если токен действителен.

Теперь давайте установим http-сервер для запуска приложения Angular:

npm install http-server --save

После установки мы можем открыть корневую папку проекта в командной строке и выполнить команду:

http-server -o

4.2. Использование Angular версии 2, 4, 5

index.html в версии 2 немного отличается от версии AngularJS:

<!DOCTYPE html>
<html>
<head>
<base href="/" />
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>

<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function (err) { console.error(err); });
</script>
</head>
<body>
<app>Loading...</app>
</body>
</html>

main.ts — это основная точка входа в приложение. Он загружает модуль приложения, и в результате браузер загружает страницу входа:

platformBrowserDynamic().bootstrapModule(AppModule);

app.routing.ts отвечает за маршрутизацию приложений:

const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: '**', redirectTo: '' }
];

export const routing = RouterModule.forRoot(appRoutes);

app.module.ts объявляет компоненты и импортирует соответствующие модули:

@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
routing
],
declarations: [
AppComponent,
HomeComponent,
LoginComponent
],
bootstrap: [AppComponent]
})

export class AppModule { }

Поскольку мы создаем одностраничное приложение, давайте создадим корневой компонент, который добавит к нему все дочерние компоненты:

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})

export class AppComponent { }

В app.component.html будет только тег <router-outlet> . Angular использует этот тег для своего механизма маршрутизации местоположения.

Теперь давайте создадим компонент входа и соответствующий ему шаблон в файле login.component.ts:

@Component({
selector: 'login',
templateUrl: './app/login/login.component.html'
})

export class LoginComponent implements OnInit {
model: any = {};

constructor(
private route: ActivatedRoute,
private router: Router,
private http: Http
) { }

ngOnInit() {
sessionStorage.setItem('token', '');
}

login() {
let url = 'http://localhost:8082/login';
let result = this.http.post(url, {
userName: this.model.username,
password: this.model.password
}).map(res => res.json()).subscribe(isValid => {
if (isValid) {
sessionStorage.setItem(
'token',
btoa(this.model.username + ':' + this.model.password)
);
this.router.navigate(['']);
} else {
alert("Authentication failed.");
}
});
}
}

Наконец, давайте посмотрим на login.component.html :

<form name="form" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate>
<div [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="username">Username</label>
<input type="text"
name="username" [(ngModel)]="model.username"
#username="ngModel" required />
<div *ngIf="f.submitted
&& !username.valid">Username is required</div>
</div>
<div [ngClass]="{ 'has-error': f.submitted && !password.valid }">
<label for="password">Password</label>
<input type="password"
name="password" [(ngModel)]="model.password"
#password="ngModel" required />
<div *ngIf="f.submitted
&& !password.valid">Password is required</div>
</div>
<div>
<button [disabled]="loading">Login</button>
</div>
</form>

4.3. Использование Ангуляр 6

Команда Angular внесла некоторые улучшения в версию 6. Из-за этих изменений наш пример также будет немного отличаться от других версий. Единственное изменение, которое мы имеем в нашем примере по отношению к версии 6, касается части вызова службы.

Вместо HttpModule версия 6 импортирует HttpClientModule из @angular/common/http.

Часть вызова службы также будет немного отличаться от более старых версий:

this.http.post<Observable<boolean>>(url, {
userName: this.model.username,
password: this.model.password
}).subscribe(isValid => {
if (isValid) {
sessionStorage.setItem(
'token',
btoa(this.model.username + ':' + this.model.password)
);
this.router.navigate(['']);
} else {
alert("Authentication failed.")
}
});

5. Вывод

Мы узнали, как реализовать страницу входа Spring Security с помощью Angular. Начиная с версии 4, мы можем использовать проект Angular CLI для упрощения разработки и тестирования.

Как всегда, все примеры, которые мы здесь обсуждали, можно найти в проекте GitHub .