Статьи

Аутентификация Angular5 JWT (Spring Boot Security)

Добро пожаловать в Angular5 JWT аутентификацию с Spring Security. В этом уроке мы будем создавать полнофункциональное приложение с использованием JWT аутентификации в одностраничном приложении Angular5 с резервным сервером, поддерживаемым Spring, с интеграцией Spring Security. Говоря, что у нас будет Пример приложения Angular5 с интегрированным в него HttpInterceptor для перехвата всех HTTP-запросов на добавление токена авторизации jwt в заголовок, а на сервере некоторые конечные точки REST будут открыты и защищены с помощью Spring Security. Ресурс будет доступен только при наличии действительного токена jwt находится в заголовке. Мы будем использовать Mysql DB для постоянного хранения.

Эта статья состоит из 4 разделов. В первом разделе мы будем создавать наше одностраничное приложение angular5, используя дизайн материалов. Во втором разделе мы создадим приложение с весенней загрузкой, в котором будут представлены образцы конечных точек REST. В третьем разделе у нас будет интеграция JWT с пружинной безопасностью, а в четвертом разделе у нас будет интеграция jwt с angular5 с использованием HttpIntrceptor. Итак, начнем.

Используемые технологии

У нас частые обновления версий как в угловой, так и в весенней загрузке. Поэтому давайте сначала подтвердим версии этих технологий, которые мы будем использовать для создания этого приложения.

1. Spring Boot 1.5.8. ВЫПУСК

2. jjwt 0.6.0

3. Угловой 5.2.0

4. Угловой материал 5.1.0

5. MySql

6. Java 1.8

JWT аутентификация

JSON Web Token (JWT) — это открытый стандарт (RFC 7519), который определяет компактный и автономный способ безопасной передачи информации между сторонами в виде объекта JSON. Механизм аутентификации без сохранения состояния, поскольку пользовательское состояние никогда не сохраняется в памяти сервера. Маркер JWT состоит из 3 частей, разделенных точкой (.), Т.е. Header.payload.signature

Заголовок содержит токен типа 2 частей и используемый алгоритм хэширования. Структура JSON, содержащая эти два ключа, кодируется Base64.

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}

Полезная нагрузка содержит утверждения. Прежде всего, существует три типа утверждений: зарезервированные, публичные и частные. Зарезервированные утверждения — это предварительно определенные утверждения, такие как iss (эмитент), exp (время истечения), sub (субъект), aud (аудитория). В частных утверждениях мы можем создавать некоторые пользовательские утверждения, такие как тема, роль и другие.

01
02
03
04
05
06
07
08
09
10
11
{
  "sub": "Alex123",
  "scopes": [
    {
      "authority": "ROLE_ADMIN"
    }
  ],
  "iss": "http://devglan.com",
  "iat": 1508607322,
  "exp": 1508625322
}

Подпись гарантирует, что токен не будет изменен в пути. Например, если вы хотите использовать алгоритм HMAC SHA256, подпись будет создана следующим образом:

1
2
3
4
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

Защищенные маршруты сервера будут проверять допустимый JWT в заголовке авторизации, и, если он присутствует, пользователю будет разрешен доступ к защищенным ресурсам. Когда пользователь хочет получить доступ к защищенному маршруту или ресурсу, пользовательский агент должен отправить JWT, обычно в заголовке авторизации с использованием схемы Bearer. Содержимое заголовка должно выглядеть следующим образом:

1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBbGV4MTIzIiwic2N.v9A80eU1VDo2Mm9UqN2FyEpyT79IUmhg

Создать приложение Angular5

У нас уже есть наше приложение angular5, созданное в моей последней статье здесь — Angular5 Material App. Это было очень простое приложение с интегрированным угловым материалом. В этом приложении у нас есть 2 модуля user и login с интегрированной маршрутизацией. Но здесь проверка входа была жестко запрограммированы в самом клиентском приложении, и как только пользователь успешно вошел в систему, он будет перенаправлен на страницу пользователя, где он сможет увидеть список пользователей в таблице данных.

Ниже приводится структура предыдущего проекта и структура проекта, которую мы будем строить сейчас.

В этом примере нам нужно сначала создать HTTP-клиент, который будет использовать API REST. Для этого мы будем использовать HttpClient из @angular/common/http . Ниже приведен наш app.service.ts. Для демонстрации у нас есть только один HTTP-вызов для получения списка пользователей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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');
  }
 
}

Не забудьте включить userService и HttpClientModule в провайдеров в app.module.ts

Точно так же в user.component.ts мы имеем следующие изменения в сервисе и заполняем таблицу данных. Еще одна вещь, которую стоит отметить, это то, что мы отключили аутентификацию для конечной точки / users в весенней конфигурации безопасности.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 Application

Прежде всего, ознакомьтесь со структурой проекта ниже. это тот же проект, который мы создали во время учебника по аутентификации jwt при весенней загрузке

Приложение весенней загрузки имеет конечную точку, доступную в / users из класса контроллера. Это простая реализация. Кроме того, у нас включен CORS для angular, а класс пользовательской модели имеет 4 атрибута: id, имя пользователя, возраст и зарплата.

UserController.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 Security

Теперь мы настроим безопасность для защиты нашего приложения. На данный момент мы разрешим конечную точку /users для публичного доступа, чтобы позже мы могли проверить нашу аутентификацию jwt и отобразить список пользователей в таблице данных, как на рисунке выше. Все эти конфигурации обсуждались в моей последней статье здесь — Spring Boot Security Аутентификация JWT. Здесь authenticationTokenFilterBean() имеет никакого эффекта, поскольку мы разрешили /users конечную точку для публичного доступа.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@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();
    }
 
}

Добавление аутентификации JWT в Spring Security

Простая реализация для JWT — написать класс фильтра, который будет перехватывать все запросы и искать токен авторизации JWT, и если токен будет найден в заголовке, он извлечет токен, проанализирует его, чтобы найти информацию, связанную с пользователем, такую ​​как имя пользователя и роли. После проверки токена он подготовит контекст безопасности весны и перенаправит запрос следующему фильтру в цепочке фильтров.

Поэтому для этой цели у нас есть пружинный класс OncePerRequestFilter который выполняется один раз для каждого запроса. В фильтре мы жестко кодируем роли, но в приложении реального времени мы можем извлечь его из пользовательских областей из токена JWT или выполнить поиск в БД в UserDetailsService

JwtAuthenticationFilter.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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);
    }
}

Как только эта реализация будет завершена, мы можем фактически удалить «/ users» из WebSecurityConfig.java и проверить, что мы получим 401 при попытке загрузить данные в нашу таблицу угловых данных.

Создать токен JWT в Spring Security

У нас есть этот контроллер, определенный для генерации токена JWT. Этот метод будет вызываться из клиента во время запроса входа в систему. Он проверит пользователя из БД с помощью комбинации имени пользователя и пароля и соответственно сгенерирует токен JWT.

AuthenticationController.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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 Авторизация

Теперь, когда мы подошли к интеграции авторизации JWT в angular5 с системой безопасности Spring, сначала нам нужно сделать запрос POST для входа в систему с именем пользователя и паролем. В ответе сервер предоставит вам токен JWT после успешной аутентификации. Как только мы получим этот токен, мы сможем кэшировать его в нашем браузере для повторного использования для дальнейших вызовов API. А сейчас давайте определим наш authservice, который будет запрашивать токен JWT в авторизоваться.

auth.service.ts

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import { HttpClient, HttpHeaders } from '@angular/common/http';
 
@Injectable()
export class AuthService {
 
 
  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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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, который будет перехватывать все запросы и добавлять этот токен авторизации JWT в заголовок. Кроме того, мы можем перехватить ответ и для любого неавторизованного запроса или токена с истекшим сроком действия мы можем перенаправить пользователя на страницу входа. Также, для локального хранения этого токена, мы можем использовать sessionstorage — объект sessionStorage хранит данные только для одного сеанса (данные удаляются когда вкладка браузера закрыта). Перехватчик реализует интерфейс HttpInterceptor и переопределяет intercept() . Здесь мы клонируем запрос, чтобы установить нужные заголовки. Следующей является реализация перехватчика.

app.interceptor.ts

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 token, с помощью которого мы можем проверить срок действия токена. При этом нам не нужно зависеть от сервера для проверки истечения срока действия токена.

Вывод

В этой статье мы узнали об интеграции токена JWT с приложением Angular5 с защитой весенней загрузки в резервном хранилище. Если вам понравился этот пост, я хотел бы услышать обратно в разделе комментариев.

Смотрите оригинальную статью здесь: Angular5 JWT Authentication (Spring Boot Security)

Мнения, высказанные участниками Java Code Geeks, являются их собственными.