欢迎使用带有Spring Security的angular5 jwt身份验证。在本教程中,我们将在一个angular5单页应用程序中使用jwt身份验证创建一个完整的堆栈应用程序,该应用程序具有由spring boot支持并集成了spring security的后备服务器。集成了HttpInterceptor的示例angular5示例应用程序,拦截所有HTTP请求以在标头中添加jwt授权令牌,并且在服务器中,我们将使用Spring安全性公开和保护一些REST端点。仅当有效的jwt令牌有效时,该资源才可访问在标题中找到。我们将使用Mysql DB进行持久性存储。
本文由4个部分组成。在第一部分中,我们将使用材料设计来构建单页angular5应用程序。 在第二部分中,我们将创建一个带有示例REST端点公开的Spring Boot应用程序。 在第三部分中,我们将通过Spring Security与JWT集成,在第四部分中,将使用HttpIntrceptor与angular5进行jwt集成。
使用的技术
我们在角度和Spring启动中都进行了频繁的版本升级。 因此,让我们首先确认将用于构建此应用程序的这些技术的版本。
1. Spring Boot 1.5.8.RELEASE
2. jjwt 0.6.0
3.角5.2.0
4.角材料5.1.0
5. MySQL
6. Java 1.8
Jwt认证
JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式来作为JSON对象在各方之间安全地传输信息。无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。 JWT令牌由三部分组成,并用点号(。)分隔,即Header.payload.signature
标头使用了两种部分的令牌和哈希算法。组成这两个密钥的JSON结构是Base64Encoded。
{
"alg": "HS256",
"typ": "JWT"
}
有效负载包含索赔。从根本上讲,索赔有三种类型:预留索赔,公共索赔和私人索赔。 保留的声明是预定义的声明,例如iss(发布者),exp(到期时间),sub(主题),aud(听众)。在私人声明中,我们可以创建一些自定义声明,例如主题,角色等。
{
"sub": "Alex123",
"scopes": [
{
"authority": "ROLE_ADMIN"
}
],
"iss": "http://devglan.com",
"iat": 1508607322,
"exp": 1508625322
}
签名可确保令牌不会在途中发生更改。例如,如果您想使用HMAC SHA256算法,则将通过以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
服务器的受保护路由将在Authorization标头中检查有效的JWT,如果存在该JWT,则将允许用户访问受保护的资源。每当用户要访问受保护的路由或资源时,用户代理都应发送JWT,通常在使用Bearer模式的Authorization标头中。 标头的内容应如下所示:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBbGV4MTIzIiwic2N.v9A80eU1VDo2Mm9UqN2FyEpyT79IUmhg
创建Angular5应用程序
我们已经在上一篇文章Angular5 Material App中创建了angular5应用程序 。这是一个非常简单的集成了angular material的应用程序 。在此应用程序中,我们集成了2个模块用户和具有路由的登录模块。但是这里的登录验证是在客户端应用程序本身中进行了硬编码,一旦用户成功登录,他将被重定向到用户页面,在该页面上他可以看到数据表中的用户列表。
以下是先前的项目结构以及我们现在将要构建的项目结构。
在这个例子中,我们需要首先创建一个使用REST API的HTTP客户端,为此我们将使用@angular/common/http
HttpClient。 以下是我们的app.service.ts 。 出于演示目的,我们只有一个HTTP调用来获取用户列表。
import {Injectable} from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {User} from './user/user.model';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
private userUrl = 'http://localhost:8080/';
public getUsers(): Observable {
return this.http.get(this.userUrl + '/users');
}
}
不要忘记在app.module.ts
的提供程序中包含userService和HttpClientModule
同样,在user.component.ts
我们对服务进行了以下更改并填充了数据表。还要注意的一件事是,我们在spring安全配置中禁用了/ users端点的身份验证。
import {Component, OnInit} from '@angular/core';
import {MatTableDataSource} from '@angular/material';
import {User} from './user.model';
import {UserService} from '../app.service';
import {Router} from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
displayedColumns = ['id', 'username', 'salary', 'age'];
dataSource = new MatTableDataSource();
constructor(private router: Router, private userService: UserService) {
}
ngOnInit(): void {
this.userService.getUsers().subscribe(
data => {
this.dataSource.data = data;
}
);
}
}
现在,有了这样的实现,我们应该能够在URL的数据表中显示用户列表– http:// localhost:4200 / user
创建Spring Boot应用程序
首先,检查以下项目结构。 这是我们在spring boot jwt认证教程期间构建的项目。
Spring Boot应用程序的端点在控制器类的/ users中公开。 这是一个简单的实现,此外,我们为angular启用了CORS,并且用户模型类具有4个属性,如id,用户名,年龄和薪水。
UserController.java
package com.devglan.controller;
import com.devglan.model.User;
import com.devglan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@CrossOrigin(origins = "http://localhost:4200", maxAge = 3600)
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value="/users", method = RequestMethod.GET)
public List listUser(){
return userService.findAll();
}
}
Spring安全配置
现在,我们将配置安全性以保护我们的应用程序。 现在,我们将允许/users
端点进行公共访问,以便稍后可以验证jwt身份验证并在数据表中显示用户列表(如上图所示)。所有这些配置已在我的上一篇文章中讨论过-Spring Boot Security JWT身份验证。这里authenticationTokenFilterBean()
无效,因为我们允许/users
端点进行公共访问。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
@Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().
authorizeRequests()
.antMatchers("/token/*").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}
在Spring Security中添加JWT身份验证
JWT的简单实现是编写一个过滤器类,该类将拦截所有请求并查找JWT授权令牌,如果在标头中找到该令牌,它将提取该令牌,解析该令牌以查找与用户相关的信息,例如username令牌被验证后,它将准备spring安全上下文,并将请求转发到过滤器链中的下一个过滤器。
因此,为此目的,我们提供了OncePerRequestFilter
类,该类每个请求执行一次。 在过滤器中,我们正在对角色进行硬编码,但在实时应用程序中,我们可以从JWT令牌的自定义范围中提取它,或者在UserDetailsService
进行数据库查找
JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX,"");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
} catch (ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
} catch(SignatureException e){
logger.error("Authentication Failed. Username or Password not valid.");
}
} else {
logger.warn("couldn't find bearer string, will ignore the header");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}
完成此实现后,我们实际上可以从WebSecurityConfig.java
删除“ / users”并验证在尝试将数据加载到角度数据表中时将得到401。
在Spring Security中创建JWT令牌
我们定义了这个控制器来生成JWT令牌。该控制器will方法将在登录请求期间从客户端调用。 它将使用用户名和密码组合从数据库验证用户,并相应地生成JWT令牌。
AuthenticationController.java
@RestController
@RequestMapping("/token")
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserService userService;
@RequestMapping(value = "/generate-token", method = RequestMethod.POST)
public ResponseEntity register(@RequestBody LoginUser loginUser) throws AuthenticationException {
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginUser.getUsername(),
loginUser.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
final User user = userService.findOne(loginUser.getUsername());
final String token = jwtTokenUtil.generateToken(user);
return ResponseEntity.ok(new AuthToken(token));
}
}
Angular5 JWT授权
现在,当要在带有Spring Security的angular5中进行JWT授权集成时,首先我们需要发出POST请求以使用用户名和密码登录。 在响应中,服务器将在成功身份验证后为您提供JWT令牌。一旦获得此令牌,我们便可以将其缓存在浏览器中以供以后的API调用重用。现在让我们定义我们的authservice,它将在以下位置请求JWT令牌登录。
验证服务
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import { HttpClient, HttpHeaders } from '@angular/common/http';
@Injectable()
export class AuthService {
baseUrl: 'http://localhost:8080/email2sms/';
constructor(private http: HttpClient) {
}
attemptAuth(ussername: string, password: string): Observable {
const credentials = {username: ussername, password: password};
console.log('attempAuth ::');
return this.http.post('http://localhost:8080/token/generate-token', credentials);
}
}
现在,在登录期间,我们将通过调用spring security AUTH API来调用此服务以对用户进行身份验证。
login.component.ts
import { Component, OnInit } from '@angular/core';
import {Router} from '@angular/router';
import {MatDialog} from '@angular/material';
import {AuthService} from '../core/auth.service';
import {TokenStorage} from '../core/token.storage';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
constructor(private router: Router, public dialog: MatDialog, private authService: AuthService, private token: TokenStorage) {
}
username: string;
password: string;
login(): void {
this.authService.attemptAuth(this.username, this.password).subscribe(
data => {
this.token.saveToken(data.token);
this.router.navigate(['user']);
}
);
}
}
手动在所有API请求的标头中添加此令牌并不是一种更干净的方法。 因此,我们将实现一个HTTPInterceptor,它将拦截所有rquest并将此JWT授权令牌添加到标头中。 此外,我们可以拦截响应,对于任何未经授权的请求或过期的令牌,我们都可以将用户重定向到登录页面。此外,要在本地存储此令牌,我们可以使用sessionstorage – sessionStorage对象仅存储一个会话的数据(该数据将被删除当浏览器标签关闭时)。 拦截器将实现HttpInterceptor
接口,并重写intercept()
。 在这里,我们正在克隆设置所需标题的请求,下面是拦截器的实现。
应用拦截器
import { Injectable } from '@angular/core';
import {HttpInterceptor, HttpRequest, HttpHandler, HttpSentEvent, HttpHeaderResponse, HttpProgressEvent,
HttpResponse, HttpUserEvent, HttpErrorResponse} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { Router } from '@angular/router';
import {TokenStorage} from './token.storage';
import 'rxjs/add/operator/do';
const TOKEN_HEADER_KEY = 'Authorization';
@Injectable()
export class Interceptor implements HttpInterceptor {
constructor(private token: TokenStorage, private router: Router) { }
intercept(req: HttpRequest, next: HttpHandler):
Observable | HttpUserEvent> {
let authReq = req;
if (this.token.getToken() != null) {
authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + this .token.getToken())});
}
return next.handle(authReq).do(
(err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this.router.navigate(['user']);
}
}
}
);
}
}
不要错过在app.module.ts中注册该拦截器的机会。
要将此令牌存储在浏览器存储中,让我们定义我们的token.storage.ts
import { Injectable } from '@angular/core';
const TOKEN_KEY = 'AuthToken';
@Injectable()
export class TokenStorage {
constructor() { }
signOut() {
window.sessionStorage.removeItem(TOKEN_KEY);
window.sessionStorage.clear();
}
public saveToken(token: string) {
window.sessionStorage.removeItem(TOKEN_KEY);
window.sessionStorage.setItem(TOKEN_KEY, token);
}
public getToken(): string {
return sessionStorage.getItem(TOKEN_KEY);
}
}
还有客户端jwt令牌验证器,我们可以使用它来检查令牌到期。 这样做,我们不必依赖服务器来检查令牌到期。
结论
在本文中,我们了解了如何将JWT令牌与Angular5应用程序集成,并在后台支持Spring Boot安全性。 如果您喜欢这篇文章,我很乐意在评论部分回覆。
翻译自: https://www.javacodegeeks.com/2018/03/angular5-jwt-authentication-spring-boot-security.html