📅  最后修改于: 2023-12-03 14:59:17.674000             🧑  作者: Mango
前言: 本文将介绍如何使用Angular和Spring实现一个简单的登录和注销功能。本示例使用Angular9和SpringBoot2进行开发。
首先,我们需要创建一个新的Angular项目和一个新的Spring Boot项目。
我们可以使用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 Initializr来创建一个新的Spring Boot项目。请确保选择以下依赖项:
我们需要创建一个名为“users”的表来存储用户信息。在PostgreSQL中,可以使用以下命令来创建该表:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL
);
我们需要创建一个名为“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);
}
我们需要创建一个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();
}
}
此配置:
我们需要创建一个名为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");
}
}
}
该类具有以下功能:
我们需要创建一个名为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();
}
}
此过滤器:
我们需要创建一个名为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);
}
}
我们要创建一个名为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();
}
}
此控制器:
我们要创建两个简单的页面:登录和注销。在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']);
});
}
}
最后,我们需要创建一个名为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');
}
}
此服务:
完成上述代码后,启动您的Spring Boot应用程序和Angular应用程序。访问http:// localhost:4200 / login,您将看到以下登录页面:
在输入您的用户名和密码后,单击“登录”按钮。在成功登录后,您将重定向到根页面:
在成功登录后,您将能够访问任何需要JWT的端点,并且当您点击登出按钮时,将退出应用程序。
通过本文,您已经学习了如何使用Angular和Spring Boot构建具有登录和注销功能的应用程序。虽然此示例非常简单,但基础知识对于构建更复杂的应用程序非常重要。 满足现代Web安全需求的正式实现可能会涉及许多附加功能,如密码哈希,防止请求劫持等。 我们强烈建议您仔细研究这些主题,并将它们应用于您的实现中。