1. 概述

本文将演示如何结合 Spring SecurityAngular 系列框架实现一个完整的登录功能,涵盖:

  • 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


原始标题:Spring Security Login Page with Angular