天天看點

Angular5 JWT身份驗證(Spring Boot安全性)

歡迎使用帶有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個子產品使用者和具有路由的登入子產品。但是這裡的登入驗證是在用戶端應用程式本身中進行了寫死,一旦使用者成功登入,他将被重定向到使用者頁面,在該頁面上他可以看到資料表中的使用者清單。

以下是先前的項目結構以及我們現在将要建構的項目結構。

Angular5 JWT身份驗證(Spring Boot安全性)

在這個例子中,我們需要首先建立一個使用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

Angular5 JWT身份驗證(Spring Boot安全性)

建立Spring Boot應用程式

首先,檢查以下項目結構。 這是我們在spring boot jwt認證教程期間建構的項目。

Angular5 JWT身份驗證(Spring Boot安全性)

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