歡迎使用帶有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個子產品使用者和具有路由的登入子產品。但是這裡的登入驗證是在用戶端應用程式本身中進行了寫死,一旦使用者成功登入,他将被重定向到使用者頁面,在該頁面上他可以看到資料表中的使用者清單。
以下是先前的項目結構以及我們現在将要建構的項目結構。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbwxCdh1mcvZ2LcV2Zh1Wa9M3clN2byBXLzN3btg3Pjd0YxVTakpFZy4kMrFTU21ERNZ3ZU10dJpHT6J1VZZHeHNWM5MEZ1Z1RkVXOykFdBNDZ2BjMipWN5NmcWdlWuZ1RaZnTXllMG1WY1N2MkNTO5xkNNh0YwIFSh9CXuNmLn1WauR2cj5CdyVmdu92Yn1Wavw1LcpDc0RHaiojIsJye.jpg)
在這個例子中,我們需要首先建立一個使用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.javapackage 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.javapublic 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.tsimport { 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.tsimport { 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