📜  Angular + Spring登录和注销示例(1)

📅  最后修改于: 2023-12-03 14:59:17.674000             🧑  作者: Mango

Angular + Spring登录和注销示例

前言: 本文将介绍如何使用Angular和Spring实现一个简单的登录和注销功能。本示例使用Angular9和SpringBoot2进行开发。

技术栈
  • Angular 9
  • Angular Material
  • Spring Boot 2
  • Spring Security
  • JSON Web Token (JWT)
功能描述
  • 用户可以通过登录表单输入用户名和密码登录系统
  • 在成功登录后,系统将返回一个包含JWT的响应
  • 在随后的每个请求中,用户必须使用该JWT作为请求头
  • 用户可以注销,该JWT将被销毁
准备工作

首先,我们需要创建一个新的Angular项目和一个新的Spring Boot项目。

Angular部分

我们可以使用Angular CLI工具来创建一个新的Angular项目。在命令行中,运行以下命令来生成一个新的Angular项目:

ng new angular-spring-login-example

接下来,我们将要添加Angular Material和Angular Flex-Layout到我们的项目中。这些包将帮助我们构建一个漂亮的登录表单。在命令行中,运行以下命令:

ng add @angular/material

该命令将会安装Angular Material及其依赖,并将它们添加到我们的app.module.ts文件中。

接下来,我们将要添加Angular Flex-Layout到我们的项目中。在命令行中,运行以下命令:

npm i -s @angular/flex-layout@9.0.0-beta.31

该命令将会安装Angular Flex-Layout及其依赖,并将其添加到我们的app.module.ts文件中。

Spring部分

我们可以使用Spring Initializr来创建一个新的Spring Boot项目。请确保选择以下依赖项:

  • Spring Web
  • Spring Security
  • Spring Data JPA
  • PostgreSQL Driver
实现步骤
Step 1: 创建数据库表

我们需要创建一个名为“users”的表来存储用户信息。在PostgreSQL中,可以使用以下命令来创建该表:

CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  password VARCHAR(255) NOT NULL,
  enabled BOOLEAN NOT NULL
);
Step 2: 创建实体类和仓库

我们需要创建一个名为“User”的实体类来表示该表的行。在src/main/java/com/example/demo/models/User.java文件中添加以下代码:

@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    private boolean enabled;

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return enabled;
    }
}

接下来,我们需要为此实体类创建一个名为“UserRepository”的仓库。在src/main/java/com/example/demo/repositories/UserRepository.java文件中添加以下代码:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findUserByUsername(String username);
}
Step 3: 创建Spring Security配置

我们需要创建一个Spring Security配置,该配置定义了哪些URL需要验证以及如何验证用户。在src/main/java/com/example/demo/configurations/SecurityConfig.java文件中添加以下代码:

@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.apply(new JwtSecurityConfigurer(tokenProvider()));
    }

    @Bean
    public JwtTokenProvider tokenProvider() {
        return new JwtTokenProvider();
    }
}

此配置:

  • 禁用了CSRF保护
  • 声明了哪些URL不需要验证
  • 通过JwtSecurityConfigurer将JWT验证添加到Spring Security过滤器链中
Step 4: 创建JwtTokenProvider

我们需要创建一个名为JwtTokenProvider的类来生成JWT令牌并验证令牌。在src/main/java/com/example/demo/security/JwtTokenProvider.java文件中添加以下代码:

@Component
public class JwtTokenProvider {

    private static final String JWT_SECRET = "secret";
    private static final int JWT_EXPIRATION_MS = 86400000;

    public String generateToken(Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        Date now = new Date();
        return Jwts.builder()
                .setSubject(Long.toString(user.getId()))
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + JWT_EXPIRATION_MS))
                .signWith(SignatureAlgorithm.HS512, JWT_SECRET)
                .compact();
    }

    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token).getBody();
        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            throw new JwtAuthenticationException("JWT token is expired or invalid");
        }
    }
}

该类具有以下功能:

  • 根据认证对象生成JWT令牌
  • 从令牌中提取用户ID
  • 验证JWT令牌是否有效
Step 5: 创建JwtAuthenticationFilter

我们需要创建一个名为JwtAuthenticationFilter的过滤器,该过滤器在每个请求中提取JWT,并使用它来验证用户。在src/main/java/com/example/demo/security/JwtAuthenticationFilter.java文件中添加以下代码:

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) {
        this.tokenProvider = tokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(getUsernameFromToken(token));
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    private String getUsernameFromToken(String token) {
        return tokenProvider.getUserIdFromToken(token).toString();
    }
}

此过滤器:

  • 从请求中提取JWT
  • 验证JWT是否有效
  • 使用JWT中的用户信息设置spring security上下文
Step 6: 创建JwtSecurityConfigurer

我们需要创建一个名为JwtSecurityConfigurer的配置器,该配置器将在Spring Security过滤器链中添加JwtAuthenticationFilter。在src/main/java/com/example/demo/configurations/JwtSecurityConfigurer.java文件中添加以下代码:

public class JwtSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private final JwtTokenProvider tokenProvider;

    public JwtSecurityConfigurer(JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        JwtAuthenticationFilter customFilter = new JwtAuthenticationFilter(tokenProvider, userDetailsService());
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
Step 7: 创建控制器

我们要创建一个名为AuthController的控制器,该控制器将公开登录和注销端点。在src/main/java/com/example/demo/controllers/AuthController.java文件中添加以下代码:

@AllArgsConstructor
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider tokenProvider;

    @PostMapping("/login")
    public ResponseEntity<JwtAuthenticationResponse> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
    }

    @GetMapping("/logout")
    public ResponseEntity<Void> logoutUser(HttpServletRequest request) {
        SecurityContextHolder.clearContext();
        return ResponseEntity.noContent().build();
    }
}

此控制器:

  • 允许用户使用用户名和密码登录
  • 在成功登录后生成并返回JWT令牌
  • 使当前用户退出系统
Step 8: 创建登录和注销页面

我们要创建两个简单的页面:登录和注销。在src/app/login和src/app/logout目录中分别创建一个新的组件。在这些组件中,我们将使用Angular Material和Angular Flex-Layout来创建漂亮的登录和注销页面。

/src/app/login/login.component.html

<mat-card>
  <mat-card-header>
    <mat-card-title>Login</mat-card-title>
  </mat-card-header>

  <mat-card-content>
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
      <mat-form-field appearance="outline">
        <mat-label>Username</mat-label>
        <input matInput type="text" formControlName="username">
      </mat-form-field>

      <mat-form-field appearance="outline">
        <mat-label>Password</mat-label>
        <input matInput type="password" formControlName="password">
      </mat-form-field>

      <div fxLayout="row" fxLayoutAlign="end center">
        <button mat-raised-button color="primary" type="submit" [disabled]="loginForm.invalid">Login</button>
      </div>
    </form>
  </mat-card-content>
</mat-card>

/src/app/login/login.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {

  loginForm: FormGroup;

  constructor(private fb: FormBuilder, private authService: AuthService, private router: Router) { }

  ngOnInit(): void {
    this.loginForm = this.fb.group({
      username: ['', Validators.required],
      password: ['', Validators.required]
    });
  }

  onSubmit(): void {
    this.authService.login(this.loginForm.value).subscribe(() => {
      this.router.navigate(['/']);
    });
  }

}

/src/app/logout/logout.component.html

<mat-card>
  <mat-card-header>
    <mat-card-title>Logout</mat-card-title>
  </mat-card-header>

  <mat-card-content>
    <p>Are you sure you want to logout?</p>

    <div fxLayout="row" fxLayoutAlign="end center">
      <button mat-raised-button color="primary" (click)="onSubmit()">Logout</button>
    </div>
  </mat-card-content>
</mat-card>

/src/app/logout/logout.component.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-logout',
  templateUrl: './logout.component.html',
  styleUrls: ['./logout.component.scss']
})
export class LogoutComponent implements OnInit {

  constructor(private authService: AuthService, private router: Router) { }

  ngOnInit(): void {
  }

  onSubmit(): void {
    this.authService.logout().subscribe(() => {
      this.router.navigate(['/login']);
    });
  }

}
Step 9: 创建认证服务

最后,我们需要创建一个名为AuthService的服务,该服务将处理用户的登录,注销和验证。在/src/app/auth.service.ts文件中添加以下代码:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { JwtHelperService } from '@auth0/angular-jwt';
import { tap } from 'rxjs/operators';
import { Observable, BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  private apiUrl = 'http://localhost:8080/api/auth';
  private loggedIn = new BehaviorSubject<boolean>(false);

  constructor(private http: HttpClient, private jwtHelperService: JwtHelperService) { }

  login(credentials: { username: string, password: string }): Observable<any> {
    return this.http.post(`${this.apiUrl}/login`, credentials).pipe(
      tap(response => {
        localStorage.setItem('access_token', response.token);
        this.loggedIn.next(true);
      })
    );
  }

  logout(): Observable<any> {
    return this.http.get(`${this.apiUrl}/logout`).pipe(
      tap(() => {
        localStorage.removeItem('access_token');
        this.loggedIn.next(false);
      })
    );
  }

  isLoggedIn(): boolean {
    const token = localStorage.getItem('access_token');
    return !this.jwtHelperService.isTokenExpired(token);
  }

  get isLoggedIn$() {
    return this.loggedIn.asObservable();
  }

  getJwtToken(): string {
    return localStorage.getItem('access_token');
  }
}

此服务:

  • 提供登录和注销用户的功能
  • 通过检查JWT令牌来检查用户是否已登录
实现效果

完成上述代码后,启动您的Spring Boot应用程序和Angular应用程序。访问http:// localhost:4200 / login,您将看到以下登录页面:

login-page1

在输入您的用户名和密码后,单击“登录”按钮。在成功登录后,您将重定向到根页面:

root-page

在成功登录后,您将能够访问任何需要JWT的端点,并且当您点击登出按钮时,将退出应用程序。

结论

通过本文,您已经学习了如何使用Angular和Spring Boot构建具有登录和注销功能的应用程序。虽然此示例非常简单,但基础知识对于构建更复杂的应用程序非常重要。 满足现代Web安全需求的正式实现可能会涉及许多附加功能,如密码哈希,防止请求劫持等。 我们强烈建议您仔细研究这些主题,并将它们应用于您的实现中。