1. 概述
在本教程中,我們將使用 Spring Security 創建一個 登錄頁面,內容包括:
- AngularJS
- Angular 2, 4, 5, 和 6
我們將在本教程中討論的示例應用程序包含一個與 REST 服務通信的客户端應用程序,該應用程序使用基本 HTTP 身份驗證進行保護。
2. Spring Security 配置
首先,我們使用 Spring Security 和 Basic Auth 設置 REST API:
以下是如何配置的:
@Configuration
@EnableWebSecurity
public class BasicAuthConfiguration {
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.cors(withDefaults())
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/login").permitAll()
.anyRequest().authenticated())
.httpBasic(withDefaults());
return http.build();
}
}現在我們來創建端點。我們的 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];
}
}同樣,如果您對實施 OAuth2 服務器進行授權感興趣,也可以查看我們關於 Spring Security OAuth2 的其他教程。
3. 設置 Angular 客户端
現在我們已經創建了 REST 服務,接下來我們將設置帶有不同版本的 Angular 客户端的登錄頁面。
我們將在這裏看到的示例使用 npm 進行依賴管理,使用 nodejs 運行應用程序。
Angular 採用單頁面架構,其中所有子組件(在本例中為登錄和主頁組件)都注入到一個通用的父 DOM 中。
與 AngularJS 相比,Angular 從版本 2 開始使用 TypeScript 作為主要語言。因此,應用程序還需要某些支持文件,以確保其正確運行。
由於 Angular 的增量增強,不同版本的所需文件各不相同。
讓我們熟悉一下這些文件:
- systemjs.config.js – systemjs 配置(版本 2)
- package.json – node 模塊依賴(版本 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 元素中。
現在,讓我們創建 ,它定義了組件與 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';
}
}
})();主控制器將通過傳遞 Authorization 標頭來請求用户數據。 我們的 REST 服務僅在令牌有效時才會返回用户數據。
現在,讓我們安裝 http-server 以運行 Angular 應用程序:
npm install http-server --save一旦安裝完成,您可以在命令提示符下打開項目根目錄,並執行以下命令:
http-server -o或npx http-server -o
4.2. 使用 Angular 版本 2、4、5
<em >index.html</em> 在版本 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);<em/>app.module.ts</em>聲明瞭組件並導入相關模塊:
@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>現在我們來安裝 node_modules(所有本地依賴項,將在 npm 中下載)。
npm install.
在從 package.json 下載所有依賴項後,使用以下命令運行項目:
npm run lite — 用於運行 Angular 項目的輕量級服務器。
4.3. 使用 Angular 6
Angular 團隊在版本 6 中進行了一些增強。由於這些更改,我們的示例也會與其它版本略有不同。我們示例中與版本 6 唯一的變化在於服務調用部分。
不再使用 <em >HttpModule</em >》,而是從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. 結論
我們學習瞭如何使用 Angular 實現 Spring Security 登錄頁面。從 Angular 4 版本開始,我們可以利用 Angular CLI 項目進行便捷的開發和測試。
本文檔的目的僅僅是為了展示如何使用 Angular 與 Spring Security 結合使用。請勿將其直接應用於生產環境。