Настройка фильтра в Spring Security

Пытаюсь настроить приложение с использованием Spring Security, но не сталкивался еще с созданием собственных фильтров, провайдеров и реализации класса UsernamePasswordAuthenticationToken. Задача: реализовать генерацию JWT-токена, но с добавлением вышеперечисленных классов. Прошу помощи, так как не знаю, куда копать Так Выглядит класс SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private JwtAuthenticationProvider provider;

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

    @Bean
    public UserDetailsService userDetailsService() {
        return new CustomUserDetailsService();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authenticationProvider(provider)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer.configurationSource(request ->
                        new CorsConfiguration().applyPermitDefaultValues()))
                .authorizeHttpRequests(authorize -> authorize.requestMatchers("/auth/**").permitAll()
                        .requestMatchers(HttpMethod.GET, "/secured/**").authenticated())
                .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));

        return http.build();
    }
}

MainController

@RestController
@RequestMapping("/secured")
public class MainController {
    @GetMapping(value = "/user")
    public ResponseEntity<String> getInfo(Principal principal){
        return ResponseEntity.ok(principal.getName());
    }
}

SecurityController

@RestController
@RequestMapping("/auth")
public class SecurityController {
    private final CustomUserDetailsService userService;
    private final JwtUtil jwtUtil;

    public SecurityController(CustomUserDetailsService userService, JwtUtil jwtUtil) {
        this.userService = userService;
        this.jwtUtil = jwtUtil;
    }

    @PostMapping(value = "/signup")
    public ResponseEntity<?> signup(@RequestBody RequestDTO signupDTO) {
        return ResponseEntity.ok(userService.createPerson(signupDTO));
    }

    @PostMapping(value = "/signin")
    public ResponseEntity<?> signin(@RequestBody RequestDTO requestDTO) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        UserDetails userDetails = userService.loadUserByUsername(requestDTO.username());
        if (auth.getName().equals(userDetails.getUsername())) {
            String token = jwtUtil.generateToken(userDetails);
            return ResponseEntity.ok(token);
        }
        return ResponseEntity.badRequest().body("NOT VALID");
    }

}

RequestDTO

public record RequestDTO(String username, String password) {
}

Класс JwtAuthenticationFilter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private CustomUserDetailsService customUserDetailsService;
    @Autowired
    private AuthenticationManager authenticationManager;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = extractTokenFromRequest(request);
        if (token != null && validateToken(token)) {
            Authentication auth = createAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }

    private String extractTokenFromRequest(HttpServletRequest request) {
        // Логика извлечения токена из запроса (например, из заголовка Authorization)
        String headerAuth = request.getHeader("Authorization");
        if (headerAuth != null && headerAuth.startsWith("Bearer ")) {
            return headerAuth.substring(7);
        }
        return null;
    }

    private boolean validateToken(String token) {
        // Логика верификации токена
        Claims claims = Jwts.parser().setSigningKey(jwtUtil.getSecret()).parseClaimsJws(token).getBody();
        Date expirationDate = claims.getExpiration();
        return !expirationDate.before(new Date());
    }

    private Authentication createAuthentication(String token) {
        // Логика создания объекта Authentication на основе токена
        CustomUserDetails userDetails = extractUserDetailsFromToken(token);
        return new JwtAuthenticationToken(userDetails, token, userDetails.getAuthorities());
    }
    private CustomUserDetails extractUserDetailsFromToken(String token) {
        // Логика извлечения информации о пользователе из токена
        return jwtUtil.extractUserDetailsFromToken(token);
    }
}

Класс JwtAuthenticationProvider

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private UserRepository userRepository;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        Person person = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("Not found"));
        if (!password.equals(person.getPassword())){
            throw new BadCredentialsException("Invalid password");
        }
        CustomUserDetails userDetails = (CustomUserDetails) User.builder()
                .username(person.getUsername())
                .password(person.getPassword())
                .roles(person.getRole()).build();
        return new JwtAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

Класс JwtAuthenticationToken

@Component
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {

    public JwtAuthenticationToken(CustomUserDetails principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

Класс JwtUtil

@Component
@Data
public class JwtUtil {
    @Value("${testing.app.secret}")
    private String secret;
    @Value("${testing.app.expirationMs}")
    private int lifeTime;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private CustomUserDetailsService userDetailsService;


    public String generateToken(UserDetails userDetails){
        return Jwts.builder().setSubject(userDetails.getUsername())
                .setIssuedAt(new Date()).setExpiration(new Date((new Date()).getTime() + lifeTime))
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }


    public String getNameFromJwt(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
    }

    public Authentication createAuthentication(String token) {
        // Получение информации о пользователе из токена
        String username = getNameFromJwt(token);
        CustomUserDetails userDetails = userDetailsService.loadUserByUsername(username);
        // Создание объекта Authentication
        return authenticationManager.authenticate(new JwtAuthenticationToken(userDetails, token, userDetails.getAuthorities()));
    }

    public boolean validateToken(String token) {
        // Логика верификации токена
        Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        Date expirationDate = claims.getExpiration();
        if (expirationDate.before(new Date())) {
            return false;
        }
        return true;
    }

    public CustomUserDetails extractUserDetailsFromToken(String token) {
        // Логика извлечения информации о пользователе из токена
        Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        String username = claims.get("username", String.class);
        return userDetailsService.loadUserByUsername(username);
    }
}

Сущность Person

@Data
@Entity
@Table(name = "persons_table")
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role;

    public Person(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }
}

Реализация UserDetails

@Data
public class CustomUserDetails implements UserDetails {

    private Person person;
    @Autowired
    public CustomUserDetails(Person person) {
        this.person = person;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public String getPassword() {
        return person.getPassword();
    }

    @Override
    public String getUsername() {
        return person.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Реализация UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;


    @Override
    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Person> person = userRepository.findByUsername(username);
        try {
            return person.map(CustomUserDetails::new).orElseThrow(Exception::new);
        } catch (Exception e) {
            throw new UsernameNotFoundException("Person not found");
        }
    }

    public Person createPerson(RequestDTO signupDTO) {
        Person person = new Person(signupDTO.username(), passwordEncoder.encode(signupDTO.password()), "ROLE_USER");
        return userRepository.save(person);
    }
}

Главный класс SpringSecurityJwtApplication

@SpringBootApplication
public class SpringSecurityJwtApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityJwtApplication.class, args);
    }

}

Файл application.yml

spring:
  application:
    name: SpringSecurityJWT
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: 1
    driver-class-name: org.postgresql.Driver
  jpa:
    database: postgresql
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
  main:
    allow-bean-definition-overriding: true
    allow-circular-references: true
  liquibase:
    change-log: db/changelog/db.changelog-master.yaml
    enabled: true
testing:
  app:
    secret: qBTmv4oXFFR2GwjexDJ4t6fsIUIUhhXqlktXjXdkcyygs8nPVEwMfo29VDRRepYDVV5IkIxBMzr7OEHXEHd37w==
    expirationMs: 60000

Также файлы миграции liquibase

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
    <changeSet id="1" author="labazov">
        <createTable tableName="persons_table">
            <column name="id" type="BIGINT" autoIncrement="true">
                <constraints primaryKey="true" nullable="false" unique="true"/>
            </column>
            <column name="username" type="VARCHAR(255)">
                <constraints nullable="false"/>
            </column>
            <column name="password" type="VARCHAR(255)">
                <constraints nullable="false"/>
            </column>
            <column name="role" type="VARCHAR(255)">
                <constraints nullable="false"/>
            </column>
        </createTable>
    </changeSet>
</databaseChangeLog>

При такой конфигурации приложения в терминале ошибка:

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-06-08T00:28:57.242+07:00 ERROR 5372 --- [SpringSecurityJWT] [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.SpringSecurityJWT.jwt.JwtAuthenticationToken required a bean of type 'com.example.SpringSecurityJWT.service.CustomUserDetails' that could not be found.


Action:

Consider defining a bean of type 'com.example.SpringSecurityJWT.service.CustomUserDetails' in your configuration.

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


Ответы (0 шт):