spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
Просто чтобы добавить немного истории, у нас есть замечательное введение, любезно предоставленное jwt.io ! Давайте посмотрим:
JSON Web Token (JWT) — это открытый стандарт (RFC 7519), определяющий компактный и автономный способ безопасной передачи информации между сторонами в виде объекта JSON. Эту информацию можно проверить и ей можно доверять, поскольку она имеет цифровую подпись. JWT можно подписать с использованием секрета (с помощью алгоритма HMAC) или пары открытого/закрытого ключей с использованием RSA.
Давайте объясним некоторые понятия этого определения дальше.
Компактный : из-за меньшего размера JWT можно отправлять через URL-адрес, параметр POST или внутри заголовка HTTP. Кроме того, меньший размер означает быструю передачу.
Автономность : полезные данные содержат всю необходимую информацию о пользователе, что позволяет избежать необходимости многократного запроса к базе данных.
Вот несколько сценариев, в которых могут быть полезны веб-токены JSON:
Аутентификация : это наиболее распространенный сценарий использования JWT. После входа пользователя в систему каждый последующий запрос будет включать JWT, что позволяет пользователю получать доступ к маршрутам, службам и ресурсам, разрешенным с помощью этого токена. Единый вход — это функция, которая в настоящее время широко использует JWT из-за ее небольших накладных расходов и возможности легкого использования в разных доменах.
Обмен информацией : веб-токены JSON — хороший способ безопасной передачи информации между сторонами. Поскольку JWT можно подписывать, например, с использованием пар открытого и закрытого ключей, вы можете быть уверены, что отправители являются теми, кем они себя называют. Кроме того, поскольку подпись рассчитывается с использованием заголовка и полезных данных, вы также можете убедиться, что содержимое не было подделано.
Веб-токены JSON состоят из трех частей, разделенных точками (.) , а именно:
Таким образом, JWT обычно выглядит следующим образом.
xxxxx
. yyyyy
. zzzzz
Давайте разберем разные части.
Заголовок
Заголовок обычно состоит из двух частей: типа токена (JWT) и используемого алгоритма хеширования, например HMAC SHA256 или RSA.
Например:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
Затем этот JSON кодируется Base64Url и образует первую часть JWT.
Полезная нагрузка
Вторая часть токена — это полезная нагрузка, содержащая утверждения. Утверждения — это утверждения об объекте (обычно о пользователе) и дополнительных метаданных. Существует три типа утверждений: зарезервированные, общедоступные и частные утверждения.
Обратите внимание, что имена утверждений состоят всего из трех символов, поскольку JWT должен быть компактным.
Публичные утверждения : они могут быть определены по желанию теми, кто использует JWT. Но во избежание коллизий их следует определить в реестре веб-токенов IANA JSON или определить как URI, содержащий пространство имен, устойчивое к коллизиям.
Частные утверждения . Это настраиваемые утверждения, созданные для обмена информацией между сторонами, которые согласились их использовать.
Примером полезной нагрузки может быть:
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
Затем полезная нагрузка кодируется Base64Url для формирования второй части веб-токена JSON.
Подпись
Чтобы создать часть подписи, вам нужно взять закодированный заголовок, закодированные полезные данные, секрет, алгоритм, указанный в заголовке, и подписать их.
Например, если вы хотите использовать алгоритм HMAC SHA256, подпись будет создана следующим образом:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Подпись используется для проверки того, что отправитель JWT является тем, кем он себя называет, и для того, чтобы гарантировать, что сообщение не было изменено в процессе. Собираем все вместе
Выходные данные представляют собой три строки Base64, разделенные точками, которые можно легко передавать в средах HTML и HTTP, при этом они более компактны по сравнению со стандартами на основе XML, такими как SAML.
Ниже показан JWT, в котором закодированы предыдущий заголовок и полезные данные, и он подписан секретом. Закодированный JWT
При аутентификации, когда пользователь успешно входит в систему, используя свои учетные данные, возвращается веб-токен JSON, который необходимо сохранить локально (обычно в локальном хранилище, но также можно использовать файлы cookie) вместо традиционного подхода создания сеанса в сервер и возврат файла cookie.
Всякий раз, когда пользователь хочет получить доступ к защищенному маршруту или ресурсу, пользовательский агент должен отправить JWT, обычно в заголовке авторизации, используя схему носителя. Содержимое заголовка должно выглядеть следующим образом:
Authorization: Bearer <token>
Это механизм аутентификации без сохранения состояния, поскольку состояние пользователя никогда не сохраняется в памяти сервера. Защищенные маршруты сервера проверят наличие допустимого JWT в заголовке авторизации, и если он присутствует, пользователю будет разрешен доступ к защищенным ресурсам. Поскольку JWT являются автономными, вся необходимая информация находится там, что снижает необходимость многократного запроса к базе данных.
Это позволяет вам полностью полагаться на API данных, которые не сохраняют состояние, и даже отправлять запросы к нижестоящим службам. Не имеет значения, какие домены обслуживают ваши API, поэтому совместное использование ресурсов между источниками (CORS) не будет проблемой, поскольку оно не использует файлы cookie.
На следующей диаграмме показан этот процесс:
Схемы аутентификации на основе токенов стали чрезвычайно популярными в последнее время, поскольку они предоставляют важные преимущества по сравнению с сеансами/файлами cookie:
При использовании этого подхода необходимо пойти на некоторые компромиссы:
Процесс аутентификации JWT очень прост.
Важно отметить, что утверждения авторизации будут включены в токен доступа. Почему это важно? Хорошо, предположим, что утверждения авторизации (например, привилегии пользователя в базе данных) изменяются в течение срока действия токена доступа. Эти изменения не вступят в силу до тех пор, пока не будет выпущен новый токен доступа. В большинстве случаев это не является большой проблемой, поскольку токены доступа недолговечны. В противном случае используйте непрозрачный шаблон токена.
Давайте посмотрим, как мы можем реализовать аутентификацию на основе токена JWT с использованием Java и Spring, пытаясь при этом повторно использовать поведение безопасности Spring по умолчанию, где это возможно. Платформа Spring Security поставляется с подключаемыми классами, которые уже работают с механизмами авторизации, такими как файлы cookie сеанса, HTTP Basic и HTTP Digest. Тем не менее, ему не хватает встроенной поддержки JWT, и нам нужно запачкать руки, чтобы заставить его работать.
В настоящее время эта демонстрация использует базу данных H2 под названием test_db, поэтому вы можете запустить ее быстро и сразу после установки без особой настройки. Если вы хотите подключиться к другой базе данных, вам необходимо указать соединение в файле application.yml
внутри каталога ресурсов. Обратите внимание, что hibernate.hbm2ddl.auto=create-drop
будет удалять и создавать чистую базу данных при каждом развертывании (вы можете изменить ее, если используете ее в реальном проекте). Вот пример из проекта: посмотрите, как легко можно поменять комментарии к свойствам url
и dialect
, чтобы использовать собственную базу данных MySQL:
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtTokenFilter
Фильтр JwtTokenFilter
применяется к каждому API ( /**
), за исключением конечной точки токена входа ( /users/signin
) и конечной точки регистрации ( /users/signup
).
Этот фильтр имеет следующие обязанности:
JwtTokenProvider
в противном случае выдайте исключение аутентификации. Убедитесь, что chain.doFilter(request, response)
вызывается после успешной аутентификации. Вы хотите, чтобы обработка запроса перешла к следующему фильтру, поскольку самый последний фильтр FilterSecurityInterceptor#doFilter отвечает за фактический вызов метода в вашем контроллере, который обрабатывает запрошенный ресурс API.
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilterConfigurer
Добавляет JwtTokenFilter
в DefaultSecurityFilterChain
безопасности весенней загрузки.
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
JwtTokenProvider
JwtTokenProvider
имеет следующие обязанности:
Мои данные пользователя
Реализует UserDetailsService
для определения нашей собственной функции loadUserbyUsername . Интерфейс UserDetailsService
используется для получения данных, связанных с пользователем. Он имеет один метод с именем loadUserByUsername , который находит объект пользователя на основе имени пользователя и может быть переопределен для настройки процесса поиска пользователя.
Он используется DaoAuthenticationProvider
для загрузки сведений о пользователе во время аутентификации.
Вебсекуритиконфиг
Класс WebSecurityConfig
расширяет WebSecurityConfigurerAdapter
для обеспечения настраиваемой конфигурации безопасности.
В этом классе настраиваются и создаются следующие bean-компоненты:
JwtTokenFilter
PasswordEncoder
Кроме того, внутри метода WebSecurityConfig#configure(HttpSecurity http)
мы настроим шаблоны для определения защищенных/незащищенных конечных точек API. Обратите внимание, что мы отключили защиту CSRF, поскольку не используем файлы cookie.
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
Убедитесь, что у вас установлены Java 8 и Maven.
Форкните этот репозиторий и клонируйте его
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
в своем браузере, чтобы убедиться, что все работает правильно. Вы можете изменить порт по умолчанию в файле application.yml
. server :
port : 8080
/users/me
, чтобы убедиться, что вы не прошли аутентификацию. Вы должны получить ответ 403
с сообщением Access Denied
, поскольку вы еще не установили действительный токен JWT. $ curl -X GET http://localhost:8080/users/me
/users/signin
с пользователем-администратором по умолчанию, которого мы создали программно, чтобы получить действительный токен JWT. $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
. $ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}