1. 概述
本文将演示如何结合 Spring Security 和 Angular 系列框架实现一个完整的登录功能,涵盖:
- AngularJS(1.x)
- Angular 2、4、5、6
整个示例项目由一个前端 Angular 客户端和一个后端 REST 服务组成,后端使用 HTTP Basic 认证进行安全保护。客户端通过提交用户名密码完成登录,服务端验证通过后返回认证信息,后续请求携带 Authorization
头进行访问控制。
⚠️ 注意:本文仅用于学习演示,切勿直接用于生产环境。真实项目中应使用更安全的认证机制如 OAuth2、JWT 等。
2. Spring Security 配置
首先搭建后端服务,使用 Spring Security + Basic Auth 保护 REST 接口。
安全配置类
@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();
}
}
✅ 关键点说明:
userDetailsService()
:内存中定义用户user / password
,角色为USER
password("{noop}password")
:表示明文密码,不加密(仅测试用)csrf().disable()
:关闭 CSRF,便于前端调试(生产环境必须开启).requestMatchers("/login").permitAll()
:允许未认证访问登录接口.anyRequest().authenticated()
:其余所有请求都需要认证httpBasic()
:启用 HTTP Basic 认证
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];
}
}
📌 说明:
/login
接口用于验证用户名密码(实际项目应交给 Spring Security 自动处理)/user
接口返回当前登录用户名,从Authorization
头中解析 Base64 编码的username:password
@CrossOrigin
:允许跨域请求,前端才能调用
📌 提示:若需更高级的认证方案,可参考 Spring Security OAuth2 教程
3. Angular 客户端环境准备
前端使用 npm 管理依赖,Node.js 运行环境。不同 Angular 版本所需配置文件略有差异。
Angular 采用单页应用(SPA)架构,通过路由动态加载组件(如 login、home),所有组件注入到根 DOM 中。
各版本关键配置文件对比
Angular 版本 | 关键配置文件 | 说明 |
---|---|---|
2 | systemjs.config.js , tsconfig.json , package.json |
使用 SystemJS 模块加载器 |
4~5 | 新增 tsconfig.app.json , .angular-cli.json |
CLI 配置文件出现 |
6+ | angular.json |
.angular-cli.json 升级版,统一项目配置 |
🔧 开发前确保安装:
npm install http-server --save-dev
运行命令启动本地服务:
npx http-server -o
4. 登录页面实现
4.1 使用 AngularJS(1.x)
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>
ng-view
:路由占位符,动态加载组件模板
路由与全局逻辑(app.js)
(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');
}
});
}
})();
✅ 核心逻辑:
- 应用启动时检查
sessionStorage
是否已有用户数据 - 若有,则设置全局
Authorization
头 - 路由切换时拦截未登录访问,跳转到
/login
登录模板(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>
登录控制器(login.controller.js)
(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.")
}
});
};
}
})();
🔑 踩坑提醒:btoa()
编码后存入 sessionStorage
,后续请求通过 $http.defaults.headers.common
自动携带。
主页模板(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';
}
}
})();
⚠️ 注意:response.data.name
应为 response.data
直接返回用户名字符串,原文有误。
4.2 使用 Angular 2/4/5
index.html
<!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>
<app>
标签为根组件占位符
主入口(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 { }
根组件模板(app.component.html)
<router-outlet></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>
🔧 安装依赖并启动:
npm install
npm run lite
4.3 使用 Angular 6+
最大变化:弃用 HttpModule
,改用 HttpClientModule
模块导入变更
import { HttpClientModule } from '@angular/common/http';
// 替换原来的 HttpModule
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
routing
]
服务调用方式更新
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.")
}
});
✅ 变化点:
HttpClient
自动解析 JSON,无需.map(res => res.json())
- 类型更明确,支持泛型
<Observable<boolean>>
- API 更简洁,错误处理更方便
5. 总结
本文完整演示了 Spring Security 与多个 Angular 版本集成实现登录功能的全过程:
- 后端使用 Spring Security Basic Auth 保护接口
- 前端通过 Base64 编码用户名密码,存储
sessionStorage
并自动携带Authorization
头 - 不同 Angular 版本在模块加载、HTTP 客户端上有差异,尤其 Angular 6+ 推荐使用
HttpClientModule
✅ 推荐实践:
- 开发阶段可用 Angular CLI 快速搭建项目(文中未展示,但强烈建议)
- 生产环境应使用 JWT 或 OAuth2 替代 Basic Auth
- 前后端分离项目务必配置好 CORS
所有示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-security-modules/spring-security-web-angular