Skip to content

Firsss21/JWT

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JSON Web Token (JWT)

Содержание:

Описание JWT

JWT - стандарт, для создания токенов доступа, основанный на формате JSON. Используется для передачи данных для аутентификации. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который использует его для подтверждения своей личности

JSON объект, который определен в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как id пользователя, его роль и т.д. и подписи (signature).

Простыми словами, JWT — это лишь зашифрованная JSON строка в следующем формате header.payload.signature.

Области применения JWT:

  • Микросервисы. Данные формируются и подписываются на одном микросервисе, а используются на другом микросервисе, который проверяет подпись токена публичным ключом.

  • Авторизация. Этот кейс может быть полезен и для монолита, если нужно сократить количество запросов в базу данных. При реализации "традиционной" сессии каждый запрос API генерирует дополнительный запрос профайла пользователя к базе данных. С JWT все, что берется в базе данных — помещается в JWT и подписывается.

Приложение использует JWT для проверки аутентификации пользователя следующим образом:

image

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

Структура JWT

JWT состоит из трех частей: заголовок header, полезные данные payload и подпись signature.

image

Хэдер JWT содержит информацию том, как должна вычисляться JWT подпись. Хэдер - это JSON объект, который выглядит следующим образом:

header = { "alg": "HS256", "typ": "JWT"}

Поле typ только показывает, что это JWT, поле alg уже определяет алгоритм хеширования. Будет использоваться при создании подписи.

Поле Payload хранит в себе полезные данные, которые хранятся внутри JWT. Эти данные называют так же JWT-claims(заявки). Пример payload, где токен хранит в себе id пользователя.

payload = {"userId": "b08f86af-35da-48f2-8fab-cef3904660bd" }

Мы положили только одну заявку(claim) в payload. Вы можете положитьь столько заявок, сколько захотите. Существует список стандартных заявок для JWT payload:

  • iss (issuer) - определяет приложение, из которого отправляется токен.
  • sub (subject) - определяет тему токена.
  • exp (expiration time) - время жизни токена.

Создаем Signature (пример на псевдокоде).

const SECRET_KEY = 'cAtwa1kkEy'
const unsignedToken = base64urlEncode(header) + '.' + base64urlEncode(payload)
const signature = HMAC-SHA256(unsignedToken, SECRET_KEY)

Алгоритм base64url кодирует хедер и payload, созданные раннее. Алгоритм соединяет закодированные строки через точку, затем строка хешируется алгоритмом, заданным в хедере на основе нашего секретного ключа, что бы далее мы могли расшифровать токен.

header eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
 payload eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ
 signature -xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

Далее объеденяем все три JWT компонента вместе, просто соединяем полученные элементы через точку.

const token = encodeBase64Url(header) + '.' + encodeBase64Url(payload) + '.' + encodeBase64Url(signature)
// JWT Token
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

Далее можно пользоваться этим токеном, наш сервер авторизации сможет проверить токен с помощью оставшегося у него *секрета.

Как JWT защищает наши данные?

Использование JWT НЕ СКРЫВАЕТ и НЕ МАСКИРУЕТ данные автоматически. Причина использования JWT - проверка, что отправленные данные были действительно отправлены авторизованным источником.

Данные внутри JWT закодированы и подписаны, а это не тоже самое, что зашифрованы. Кодирование - используется для преобразования структуры, подпись - для аутентификации, т.е. не защищают данные, когда главная цель шифрования - защита данных от неавторизированного доступа.

Поскольку JWT только лишь закодирована и подписана и поскольку JWT не зашифрована, JWT не гарантирует никакой безопасности для чувствительных (sensitive) данных.

Пример использования JWT в Spring Security

Наш SecurityConfig, который конфигурирует такие вещи, как:

  • passwordEncoder для паролей наших пользователей
  • Отключает csrf
  • Определяет доступные пользователю пути без авторизации
  • Определяет доступные пользователю данные в зависимости от его уровня доступа
  • Добавляет фильтр для аутентификации пользователя
  • Добавляет фильтр для авторизации пользователя через JWT token
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CustomAuthenticationFilter caf = new CustomAuthenticationFilter(authenticationManagerBean());
        caf.setFilterProcessesUrl("/api/login");
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.authorizeRequests().antMatchers("/api/login/**", "/api/refreshToken/**").permitAll();
        http.authorizeRequests().antMatchers(GET, "/api/user/**").hasAnyAuthority("ROLE_USER");
        http.authorizeRequests().antMatchers(POST, "/api/user/save/**").hasAnyAuthority("ROLE_ADMIN");
        http.authorizeRequests().anyRequest().authenticated();
        http.addFilter(caf);
        http.addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

CustomAuthenticationFilter, в котором мы аутентифицируем пользователя и создаем новые JWT токены, такие как access token и refresh token, которые после ему отправляем

@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        log.info("Username is: {}", username);
        log.info("Password is: {}", password);

        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);
        return authenticationManager.authenticate(authToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        User user = (User) authentication.getPrincipal();
        Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
        String accessToken = JWT.create()
                .withSubject(user.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
                .withIssuer(request.getRequestURL().toString())
                .withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
                .sign(algorithm);

        String refreshToken = JWT.create()
                .withSubject(user.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
                .withIssuer(request.getRequestURL().toString())
                .withClaim("roles", user.getAuthorities().stream().map(e -> e.getAuthority()).collect(Collectors.toList()))
                .sign(algorithm);
        Map<String, String> tokens = new HashMap<>();
        tokens.put("access_token", accessToken);
        tokens.put("refresh_token", refreshToken);
        response.setContentType(APPLICATION_JSON_VALUE);
        new ObjectMapper().writeValue(response.getOutputStream(), tokens);
    }
}

CustomAuthorizationFilter фильтр, в котором мы авторизируем пользователя через его JWT token на каждый его запрос к API

@Slf4j
public class CustomAuthorizationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/refreshToken")) {
            filterChain.doFilter(request, response);
        } else {
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                try {
                    String token = authorizationHeader.substring("Bearer ".length());

                    Algorithm alg = Algorithm.HMAC256("secret".getBytes());
                    JWTVerifier verifier = JWT.require(alg).build();

                    DecodedJWT decodedJWT = verifier.verify(token);
                    String userName = decodedJWT.getSubject();
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    Collection<SimpleGrantedAuthority> authorityCollection = new ArrayList<>();
                    Arrays.stream(roles).forEach(e -> authorityCollection.add(new SimpleGrantedAuthority(e)));

                    UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(userName, null, authorityCollection);
                    SecurityContextHolder.getContext().setAuthentication(upat);
                    filterChain.doFilter(request, response);
                } catch (Exception e) {
                    log.error("Error logging in: {} ", e.getMessage());
                    response.setHeader("error", e.getMessage());
                    response.setStatus(FORBIDDEN.value());
                    HashMap<String, String> error = new HashMap<>();
                    error.put("error_message", e.getMessage());
                    response.setContentType(APPLICATION_JSON_VALUE);
                    new ObjectMapper().writeValue(response.getOutputStream(), error);
                }
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }
}

Refresh token. Эндпоинт для получения нового ключа при инвалидации старого с помощью refresh token.

    @PostMapping("/refreshToken/")
    public void refreshToken(HttpServletRequest request, HttpServletResponse response) {
        String authHeader = request.getHeader(AUTHORIZATION);
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            try {
                String refreshToken = authHeader.substring("Bearer ".length());

                Algorithm alg = Algorithm.HMAC256("secret".getBytes());
                JWTVerifier verifier = JWT.require(alg).build();

                DecodedJWT decodedJWT = verifier.verify(refreshToken);
                String userName = decodedJWT.getSubject();

                User user = userService.getUser(userName);
                String accessToken = JWT.create()
                        .withSubject(user.getUsername())
                        .withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
                        .withIssuer(request.getRequestURL().toString())
                        .withClaim("roles", user.getRoles().stream().map(Role::getName).collect(Collectors.toList()))
                        .sign(alg);

                Map<String, String> tokens = new HashMap<>();
                tokens.put("access_token", accessToken);
                tokens.put("refresh_token", refreshToken);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                new ObjectMapper().writeValue(response.getOutputStream(), tokens);
            } catch (Exception e) {
                response.setHeader("error", e.getMessage());
                response.setStatus(FORBIDDEN.value());
                HashMap<String, String> error = new HashMap<>();
                error.put("error_message", e.getMessage());
                response.setContentType(APPLICATION_JSON_VALUE);
                new ObjectMapper().writeValue(response.getOutputStream(), error);
            }
        } else {
            throw new RuntimeException("Refresh token is missing");
        }
    }

Источники

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages