Пользовательская аутентификация Spring webflux для API
Я создаю API для приложения Angular 5. Я хотел бы использовать JWT для аутентификации.
Я хотел бы использовать функции, предоставляемые spring security, чтобы я мог легко работать с ролями.
мне удалось отключить базовую аутентификацию. Но при использовании http.authorizeExchange().anyExchange().authenticated();
Я все еще получаю приглашение для входа.
Я хотел бы просто дать 403 вместо приглашения. Таким образом, переопределение приглашения входа в систему "вещью" (это фильтр?) что проверяет Authorization
заголовок знак.
логин, который я просто хочу сделать в контроллере, который вернет токен JWT. Но какой Spring security bean я должен использовать для проверки учетных данных пользователя? Я могу создавать свои собственные сервисы и репозитории, но я хотел бы использовать возможности spring security как можно больше.
краткая версия этого вопроса просто:
как настроить аутентификацию spring security?
Какие бобы я должен создать?
Где должен ли я поставить конфигурацию? (Теперь у меня есть Боб SecurityWebFilterChain
)
единственная документация, которую я мог найти об аутентификации в webflux с spring security, это: https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#jc-webflux
3 ответов
после долгих поисков и попыток я, кажется, нашел решение:
вам нужен Боб SecurityWebFilterChain
, который содержит все конфигурации.
Это мое:
@Configuration
public class SecurityConfiguration {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private SecurityContextRepository securityContextRepository;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// Disable default security.
http.httpBasic().disable();
http.formLogin().disable();
http.csrf().disable();
http.logout().disable();
// Add custom security.
http.authenticationManager(this.authenticationManager);
http.securityContextRepository(this.securityContextRepository);
// Disable authentication for `/auth/**` routes.
http.authorizeExchange().pathMatchers("/auth/**").permitAll();
http.authorizeExchange().anyExchange().authenticated();
return http.build();
}
}
Я отключил httpBasic, formLogin, csrf и logout, чтобы я мог сделать свою пользовательскую аутентификацию.
установка AuthenticationManager
и SecurityContextRepository
Я переопределил конфигурацию безопасности spring по умолчанию для проверки подлинности/авторизации пользователя для запроса.
в менеджер аутентификации:
@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
// JwtAuthenticationToken is my custom token.
if (authentication instanceof JwtAuthenticationToken) {
authentication.setAuthenticated(true);
}
return Mono.just(authentication);
}
}
Я не совсем уверен, где менеджер аутентификации, но я думаю, что для окончательной аутентификации, поэтому настройка authentication.setAuthenticated(true);
когда все правильно.
SecurityContextRepository:
@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
@Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
// Don't know yet where this is for.
return null;
}
@Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
// JwtAuthenticationToken and GuestAuthenticationToken are custom Authentication tokens.
Authentication authentication = (/* check if authenticated based on headers in serverWebExchange */) ?
new JwtAuthenticationToken(...) :
new GuestAuthenticationToken();
return new SecurityContextImpl(authentication);
}
}
в загрузке я проверю на основе заголовков в serverWebExchange
если пользователь аутентифицирован. Я использую https://github.com/jwtk/jjwt. Я возвращаю токен аутентификации другого типа, если пользователь подлинный или нет.
в моем старом проекте я использовал эту конфигурацию:
@Configuration
@EnableWebSecurity
@Import(WebMvcConfig.class)
@PropertySource(value = { "classpath:config.properties" }, encoding = "UTF-8", ignoreResourceNotFound = false)
public class WebSecWebSecurityCfg extends WebSecurityConfigurerAdapter
{
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("objectMapper")
private ObjectMapper mapper;
@Autowired
@Qualifier("passwordEncoder")
private PasswordEncoder passwordEncoder;
@Autowired
private Environment env;
public WebSecWebSecurityCfg(UserDetailsService userDetailsService)
{
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
JWTAuthorizationFilter authFilter = new JWTAuthorizationFilter
( authenticationManager(),//Auth mgr
env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
env.getProperty("config.jwt.header.string"), //nome header
env.getProperty("config.jwt.token.prefix") //Prefisso token
);
JWTAuthenticationFilter authenticationFilter = new JWTAuthenticationFilter
(
authenticationManager(), //Authentication Manager
env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
Long.valueOf(env.getProperty("config.jwt.token.duration")),//Durata del token in millisecondi
env.getProperty("config.jwt.header.string"), //nome header
env.getProperty("config.jwt.token.prefix"), //Prefisso token
mapper
);
http
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.addFilter(authenticationFilter)
.addFilter(authFilter)
// Disabilitiamo la creazione di sessione in spring
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Bean
CorsConfigurationSource corsConfigurationSource()
{
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
здесь JWTAuthorizationFilter
- это:
public class JWTAuthorizationFilter extends BasicAuthenticationFilter
{
private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class.getName());
private String secretKey;
private String headerString;
private String tokenPrefix;
public JWTAuthorizationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, String secretKey, String headerString, String tokenPrefix)
{
super(authenticationManager, authenticationEntryPoint);
this.secretKey = secretKey;
this.headerString = headerString;
this.tokenPrefix = tokenPrefix;
}
public JWTAuthorizationFilter(AuthenticationManager authenticationManager, String secretKey, String headerString, String tokenPrefix)
{
super(authenticationManager);
this.secretKey = secretKey;
this.headerString = headerString;
this.tokenPrefix = tokenPrefix;
}
@Override
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException
{
AuthenticationErrorEnum customErrorCode = null;
StringBuilder builder = new StringBuilder();
if( failed.getCause() instanceof MissingJwtTokenException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_MANCANTE;
}
else if( failed.getCause() instanceof ExpiredJwtException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_SCADUTO;
}
else if( failed.getCause() instanceof MalformedJwtException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NON_CORRETTO;
}
else if( failed.getCause() instanceof MissingUserSubjectException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NESSUN_UTENTE_TROVATO;
}
else if( ( failed.getCause() instanceof GenericJwtAuthorizationException ) || ( failed.getCause() instanceof Exception ) )
{
customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
}
builder.append("Errore duranre l'autorizzazione. ");
builder.append(failed.getMessage());
JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
String errore = ( new ObjectMapper() ).writeValueAsString(apiError);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
}
и JWTAuthenticationFilter
is
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
private AuthenticationManager authenticationManager;
private String secretKey;
private long tokenDurationMillis;
private String headerString;
private String tokenPrefix;
private ObjectMapper mapper;
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
{
AuthenticationErrorEnum customErrorCode = null;
StringBuilder builder = new StringBuilder();
if( failed instanceof BadCredentialsException )
{
customErrorCode = AuthenticationErrorEnum.CREDENZIALI_SERVIZIO_ERRATE;
}
else
{
//Teoricamente nella fase di autenticazione all'errore generico non dovrebbe mai arrivare
customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
}
builder.append("Errore durante l'autenticazione del servizio. ");
builder.append(failed.getMessage());
JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
String errore = mapper.writeValueAsString(apiError);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
}
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, String secretKey, long tokenDurationMillis, String headerString, String tokenPrefix, ObjectMapper mapper)
{
super();
this.authenticationManager = authenticationManager;
this.secretKey = secretKey;
this.tokenDurationMillis = tokenDurationMillis;
this.headerString = headerString;
this.tokenPrefix = tokenPrefix;
this.mapper = mapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException
{
try
{
ServiceLoginDto creds = new ObjectMapper().readValue(req.getInputStream(), ServiceLoginDto.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getCodiceServizio(), creds.getPasswordServizio(), new ArrayList<>()));
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException
{
DateTime dt = new DateTime();
Date expirationTime = dt.plus(getTokenDurationMillis()).toDate();
String token = Jwts
.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(expirationTime)
.signWith(SignatureAlgorithm.HS512, getSecretKey().getBytes())
.compact();
res.addHeader(getHeaderString(), getTokenPrefix() + token);
res.addHeader("jwtExpirationDate", expirationTime.toString());
res.addHeader("jwtTokenDuration", String.valueOf(TimeUnit.MILLISECONDS.toMinutes(getTokenDurationMillis()))+" minuti");
}
public String getSecretKey()
{
return secretKey;
}
public void setSecretKey(String secretKey)
{
this.secretKey = secretKey;
}
public long getTokenDurationMillis()
{
return tokenDurationMillis;
}
public void setTokenDurationMillis(long tokenDurationMillis)
{
this.tokenDurationMillis = tokenDurationMillis;
}
public String getHeaderString()
{
return headerString;
}
public void setHeaderString(String headerString)
{
this.headerString = headerString;
}
public String getTokenPrefix()
{
return tokenPrefix;
}
public void setTokenPrefix(String tokenPrefix)
{
this.tokenPrefix = tokenPrefix;
}
}
деталь пользователя является классическим userservicedetail
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
@Autowired
private IServizioService service;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
Service svc;
try
{
svc = service.findBySvcCode(username);
}
catch (DbException e)
{
throw new UsernameNotFoundException("Errore durante il processo di autenticazione; "+e.getMessage(), e);
}
if (svc == null)
{
throw new UsernameNotFoundException("Nessun servizio trovato per il codice servizio "+username);
}
else if( !svc.getAbilitato().booleanValue() )
{
throw new UsernameNotFoundException("Servizio "+username+" non abilitato");
}
return new User(svc.getCodiceServizio(), svc.getPasswordServizio(), Collections.emptyList());
}
}
обратите внимание, что я не использовал Spring webflux
Я надеюсь, что это полезно
Анджело
спасибо Ян вы помогли мне много с вашим примером, чтобы настроить аутентификацию в моем приложении Spring Webflux и безопасный доступ к API.
В моем случае мне просто нужно прочитать заголовок, чтобы установить роли пользователей, и я хочу, чтобы Spring security проверила разрешения пользователей для обеспечения доступа к моим методам.
Вы дали ключ с custom http.securityContextRepository(this.securityContextRepository);
в SecurityConfiguration (нет необходимости в пользовательском authenticationManager).
благодаря этому SecurityContextRepository я смог построить и установите пользовательскую аутентификацию (упрощенную ниже).
@Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
String role = serverWebExchange.getRequest().getHeaders().getFirst("my-header");
Authentication authentication =
new AnonymousAuthenticationToken("authenticated-user", someUser, AuthorityUtils.createAuthorityList(role) );
return Mono.just(new SecurityContextImpl(authentication));
}
и, таким образом, я могу защитить свои методы, используя следующие роли:
@Component
public class MyService {
@PreAuthorize("hasRole('ADMIN')")
public Mono<String> checkAdmin() {
// my secure method
}
}