Пользовательское сообщение Spring Boot для ошибки аутентификации Spring Security - PullRequest
0 голосов
/ 04 мая 2019

Я хотел бы отправить пользовательское сообщение об ошибке при сбое аутентификации

Я использую следующий метод, чтобы проверить, существует ли пользователь в базе данных для генерации jwt, если проверка не удалась, она возвращает ошибку 401, но полученный ответ пуст, я хотел бы добавить сообщение с причина в зависимости от ошибки.


@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, LockedException {

                final AppUser user = userService.findByUsername(username);
        if (user != null) {
            if(user.isEnabled()) {
                List<GrantedAuthority> grantedAuthorities = AuthorityUtils
                        .commaSeparatedStringToAuthorityList("ROLE_" + user.getRole());


                return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
            }else{
                throw new LockedException("Username: " + username + " is Locked");
            }

        }
        // If user not found. Throw this exception.
        throw new UsernameNotFoundException("Username: " + username + " not found");
    }


Это конфигурация безопасности:

@EnableWebSecurity  // Enable security config. This annotation denotes config for spring security.
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtConfig jwtConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()

                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()

                .exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED,"your message goes here"))
            .and()

            .addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig))  
        .authorizeRequests()
            // allow all POST requests 
            .antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
            .antMatchers(HttpMethod.POST, jwtConfig.getSignUpUri()).permitAll()
            .antMatchers(HttpMethod.GET, jwtConfig.getValidateEmailUri()).permitAll()
            // any other requests must be authenticated
            .anyRequest().authenticated();
    }


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

    @Bean
    public JwtConfig jwtConfig() {
            return new JwtConfig();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

А это jwtfilter:


public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter   {

    // We use auth manager to validate the user credentials
    private AuthenticationManager authManager;

    private final JwtConfig jwtConfig;

    public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, JwtConfig jwtConfig) {
        this.authManager = authManager;
        this.jwtConfig = jwtConfig;

        // By default, UsernamePasswordAuthenticationFilter listens to "/login" path. 
        // In our case, we use "/auth". So, we need to override the defaults.
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(jwtConfig.getUri(), "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {

        try {

            // 1. Get credentials from request
            UserCredentials creds = new ObjectMapper().readValue(request.getInputStream(), UserCredentials.class);

            // 2. Create auth object (contains credentials) which will be used by auth manager
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    creds.getUsername(), creds.getPassword(), Collections.emptyList());

            // 3. Authentication manager authenticate the user, and use UserDetialsServiceImpl::loadUserByUsername() method to load the user.
            return authManager.authenticate(authToken);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // Upon successful authentication, generate a token.
    // The 'auth' passed to successfulAuthentication() is the current authenticated user.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication auth) throws IOException, ServletException {

        Long now = System.currentTimeMillis();
        String token = Jwts.builder()
            .setSubject(auth.getName()) 
            // Convert to list of strings. 
            // This is important because it affects the way we get them back in the Gateway.
            .claim("authorities", auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
            .setIssuedAt(new Date(now))
            .setExpiration(new Date(now + jwtConfig.getExpiration() * 1000))  // in milliseconds
            .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes())
            .compact();

        // Add token to header
        response.addHeader("Access-Control-Expose-Headers", "authorization");
//      response.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
        response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
    }

    // A (temporary) class just to represent the user credentials
    private static class UserCredentials {
        private String username, password;

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }
        public String getPassword() {
            return password;
        }
        public void setPassword(String password) {
            this.password = password;
        }
    }
}

Как настроить сообщение для ответа об ошибке?

1 Ответ

0 голосов
/ 04 мая 2019

Вы можете настроить UsernamePasswordAuthenticationFilter внутренний AuthenticationFailureHandler. Его onAuthenticationFailure() будет вызываться, когда attemptAuthentication() не сможет аутентифицироваться и выдать AuthenticationException.

Реализация по умолчанию - SimpleUrlAuthenticationFailureHandler, которая устанавливает статус ответа только 401. Вы можете просто расширить его и добавить свое ответное сообщение в HttpServletResponse, что-то вроде ниже:

public MySimpleUrlAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler{


    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {


        if (defaultFailureUrl == null) {
               response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
               response.getWriter().write("Your Response Message blablala");
        }else{
            super.onAuthenticationFailure(request,response, exception);
        }
    }

}

Конечно, вы должны настроить использование AuthenticationFailureHandler после создания JwtUsernameAndPasswordAuthenticationFilter.

Вам также нужно изменить, чтобы бросить AuthenticationException вместо RuntimeException внутри attemptAuthentication().

...