Чтобы построить сервер Spring Boot с OAuth2, JWT и дополнительными требованиями, нам нужно:
1) Добавить зависимость к проекту:
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
2) Добавить конфигурацию веб-безопасности (для публикации AuthenticationManager
bean - она будет использоваться на следующем шаге), например:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(username -> AuthUser.with()
.username(username)
.password("{noop}" + username)
.email(username + "@mail.com")
.authority(AuthUser.Role.values()[ThreadLocalRandom.current().nextInt(2)])
.build()
);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
Здесь реализован простой UserDetailsService
для целей тестирования. Он работает со следующим простым объектом 'User' и перечислением Role
, которое реализует интерфейс GrantedAuthority
. AuthUser
имеет только одно дополнительное свойство email
, которое будет добавлено в токен JWT в качестве заявки.
@Value
@EqualsAndHashCode(callSuper = false)
public class AuthUser extends User {
private String email;
@Builder(builderMethodName = "with")
public AuthUser(final String username, final String password, @Singular final Collection<? extends GrantedAuthority> authorities, final String email) {
super(username, password, authorities);
this.email = email;
}
public enum Role implements GrantedAuthority {
USER, ADMIN;
@Override
public String getAuthority() {
return this.name();
}
}
}
3) Настройте сервер авторизации и включите сервер ресурсов:
@Configuration
@EnableAuthorizationServer
@EnableResourceServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
public static final String TOKEN_KEY = "abracadabra";
private final AuthenticationManager authenticationManager;
public AuthServerConfig(final AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public void configure(ClientDetailsServiceConfigurer clientDetailsService) throws Exception {
clientDetailsService.inMemory()
.withClient("client")
.secret("{noop}")
.scopes("*")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(60 * 2) // 2 min
.refreshTokenValiditySeconds(60 * 60); // 60 min
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain chain = new TokenEnhancerChain();
chain.setTokenEnhancers(List.of(tokenEnhancer(), tokenConverter()));
endpoints
.tokenStore(tokenStore())
.reuseRefreshTokens(false)
.tokenEnhancer(chain)
.authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(tokenConverter());
}
@Bean
public JwtAccessTokenConverter tokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(TOKEN_KEY);
converter.setAccessTokenConverter(authExtractor());
return converter;
}
private TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
if (authentication != null && authentication.getPrincipal() instanceof AuthUser) {
AuthUser authUser = (AuthUser) authentication.getPrincipal();
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("user_email", authUser.getEmail());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
}
return accessToken;
};
}
@Bean
public DefaultAccessTokenConverter authExtractor() {
return new DefaultAccessTokenConverter() {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
};
}
}
Простой ClientDetailsService
реализован здесь. Он содержит только одного клиента с именем «client», пустым паролем и предоставленными типами «password» и «refresh_token». Это дает нам возможность создать новый токен доступа и обновить его. (Для работы со многими типами клиентов или в других сценариях вам необходимо реализовать более сложные и, возможно, постоянные варианты ClientDetailsService
.)
Конечные точки авторизации конфигурируются с TokenEnhancerChain
, который содержит tokenEnhancer
и tokenConverter
. Важно добавить их в этой последовательности. Первый дополняет токен доступа дополнительными претензиями (в нашем случае электронная почта пользователя). Второй создает токен JWT. Набор endpoints
с простыми JwtTokenStore
, нашими TokenEnhancerChain
и authenticationManager
.
Примечание к JwtTokenStore
- Я считаю, что в реальных сценариях должен использоваться постоянный вариант магазина. Более подробная информация здесь .
Последняя вещь здесь authExtractor
, которая дает нам возможность извлекать заявки из токенов JWT входящих запросов.
После того, как все настроено, мы можем запросить у нашего сервера токен доступа:
curl -i \
--user client: \
-H "Content-Type: application/x-www-form-urlencoded" \
-X POST \
-d "grant_type=password&username=user&password=user&scope=*" \
http://localhost:8080/oauth/token
Rsponse:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImV4cCI6MTU0Nzc2NDIzOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiYzk1YzkzYTAtMThmOC00OGZjLWEzZGUtNWVmY2Y1YWIxMGE5IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RWSGMC0w8tNafT28i2GLTnPnIiXfAlCdydEsNNZK-Lw",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImF0aSI6ImM5NWM5M2EwLTE4ZjgtNDhmYy1hM2RlLTVlZmNmNWFiMTBhOSIsImV4cCI6MTU0Nzc2NzcxOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiZDRhNGU2ZjUtNDY2Mi00NGZkLWI0ZDgtZWE5OWRkMDJkYWI2IiwiY2xpZW50X2lkIjoiY2xpZW50In0.m7XvxwuPiTnPaQXAptLfi3CxN3imfQCVKyjmMCIPAVM",
"expires_in": 119,
"scope": "*",
"user_email": "user@mail.com",
"jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9"
}
Если мы расшифруем этот токен доступа на https://jwt.io/, мы увидим, что он содержит утверждение user_email
:
{
"user_email": "user@mail.com",
"user_name": "user",
"scope": [
"*"
],
"exp": 1547764238,
"authorities": [
"ADMIN"
],
"jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9",
"client_id": "client"
}
Чтобы извлечь такую заявку (и другие данные) из токена JWT входящих запросов, мы можем использовать следующий подход:
@RestController
public class DemoController {
@GetMapping("/demo")
public Map demo(OAuth2Authentication auth) {
var details = (OAuth2AuthenticationDetails) auth.getDetails();
//noinspection unchecked
var decodedDetails = (Map<String, Object>) details.getDecodedDetails();
return Map.of(
"name", decodedDetails.get("user_name"),
"email", decodedDetails.get("user_email"),
"roles", decodedDetails.get("authorities")
);
}
}
Моя рабочая демонстрация: sb-jwt-oauth-demo
Информация, связанная с данной: