使用Angular的Spring Security登录页面

2023/05/17

1. 概述

在本教程中,我们将使用Spring Security创建一个登录页面,并通过Angular访问我们的后端服务:

2. Spring Security配置

首先,我们配置Rest API的安全规则和用户名密码:


@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(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/login").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .httpBasic();
    }
}

然后,我们新建一个Controller,它包含两个端点:一个用于登录,另一个用于获取用户数据:


@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];
    }
}

@Setter
@Getter
public class User {
    private String userName;
    private String password;
}

3. Angular客户端

现在我们使用不同版本的Angular客户端设置登录页面。

我们在这里介绍的例子使用npm进行依赖管理,使用nodejs运行应用程序。

Angular使用单页面架构,其中所有子组件(在我们的例子中是login和home组件)都被注入到一个公共的父DOM中

与使用JavaScript的AngularJS不同,Angular 2及更高版本使用TypeScript作为其主要语言。 因此,该应用程序还需要某些支持文件才能正常工作。

由于Angular的增量增强,所需的文件因版本而异。

  • systemjs.config.js – 系统配置(版本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文件并向其中添加相关依赖项:

<!DOCTYPE html>
<html ng-app="app" lang="en">

<head>
    <title></title></head>
<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属性的div元素中。

现在,让我们创建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) {
        let userData = $window.sessionStorage.getItem('userData');
        if (userData) {
            $http.defaults.headers.common['Authorization'] = 'Basic ' + JSON.parse(userData).authData;
        }

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

登录组件由两个文件组成,login.view.html和login.controller.js。

我们来看第一个:


<div>
    <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>
</div>

第二个:

(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服务。 身份验证成功后,它将对用户名和密码进行编码,并将编码后的令牌存储在session存储中以供将来使用。

与登录组件类似,home组件也包含两个文件,即home.view.html:

<h1>Hi !</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';
        }
    }
})();

HomeController通过传递Authorization头来请求用户数据。 仅当token有效时,我们的REST服务才会返回用户数据。

现在让我们安装http-server来运行Angular应用程序:

npm install http-server --save

安装完成后,我们可以进入到项目根文件夹,并执行命令:

http-server -o

4.2 Angular 2,4,5版本

版本2中的index.html与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',
    templateUrl: './app/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:


<div class="col-md-6 col-md-offset-3">
    <h2>Login</h2>
    <form name="form" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate>
        <div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
            <label for="username">Username</label>
            <input type="text" class="form-control" name="username" [(ngModel)]="model.username" #username="ngModel"
                   required/>
            <div *ngIf="f.submitted && !username.valid" class="help-block">Username is required</div>
        </div>
        <div class="form-group" [ngClass]="{ 'has-error': f.submitted && !password.valid }">
            <label for="password">Password</label>
            <input type="password" class="form-control" name="password" [(ngModel)]="model.password" #password="ngModel"
                   required/>
            <div *ngIf="f.submitted && !password.valid" class="help-block">Password is required</div>
        </div>
        <div class="form-group">
            <button [disabled]="loading" class="btn btn-primary">Login</button>
        </div>
    </form>
</div>

4.3 Angular 6

Angular团队在版本6中进行了一些增强,由于这些更改,我们的示例与其他版本相比也会有些不同。 我们在示例中对版本6的唯一更改是服务调用部分。

版本6从@angulal/common/http导入HttpClientModule,而不是HttpModule

服务调用部分也将与旧版本略有不同:

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登录页面。 从版本4开始,我们可以使用Angular CLI项目来轻松开发和测试。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章