Configuration Spring Security commune pour plusieurs applis
Bonjour,
Je suis en train de faire un projet Spring Boot en version 2.7.4 qui me permettra de faire un jar qui sera ensuite importé dans plusieurs applications.
Le but étant d'avoir une configuration de sécurité commune pour toutes ces applications plutôt qu'elles fassent chacune leur propre configuration car la base des utilisateurs pour ces applications sera commune et on veut gérer de la même manière la sécurité.
Voici ce que j'ai initialisé (non encore fini):
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
|
@Configuration("personalSecurityConfig")
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);
private static final String[] IGNORING_LIST = {
// Swagger ui
"/api/v2/api-docs", "/api/swagger-ui.html", "/api/webjars/springfox-swagger-ui/**",
"/api/swagger-resources/**",
// URLs without authentication needed
"/api/no-security" };
@Value("${jwt.secret}")
private String jwtSecret;
@Autowired
private PersoSecurityProperties persoSecurityProperties;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Do not allow anonymous access to the application (AnonymousAuthenticationFilter will not be set)
http.anonymous().disable();
http.addFilter(jwtAuthenticationFilter());
http.addFilter(jwtAuthorizationFilter());
// TODO
// The CSRF token must be set in a cookie so that angular can read it
http.csrf().csrfTokenRepository(getCsrfTokenRepository());
return http.build();
}
@Bean
@Order(99)
SecurityFilterChain configurePublicEndpoints(HttpSecurity http) throws Exception {
List<String> publicResources = getPublicResources();
try {
http.requestMatchers(matcher -> matcher.antMatchers(publicResources.toArray(new String[0])))
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()).csrf().disable()
.requestCache().disable().securityContext().disable().sessionManagement().disable();
} catch (Exception ex) {
LOGGER.error("Error applying custom security configuration for public endpoints '{}' ", publicResources,
ex);
}
return http.build();
}
/**
* Permit to indicate to Spring security to not secure the given URI
* @return
*/
@Bean
public WebSecurityCustomizer ignoringCustomizer() {
return (web) -> web.ignoring().antMatchers(IGNORING_LIST);
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter lJwtAuthenticationFilter = new JwtAuthenticationFilter(authManager());
return lJwtAuthenticationFilter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() throws Exception {
JwtAuthorizationFilter lJwtAuthenticationFilter = new JwtAuthorizationFilter(authManager(), jwtSecret);
return lJwtAuthenticationFilter;
}
/**
* Bean for personal authentication user detail service.
* @return
*/
@Bean
public AuthenticationUserDetailsService<UsernamePasswordAuthenticationToken> personalAuthenticationUserDetailsService() {
return new PersonalAuthenticationUserDetailsService();
}
@Autowired
public void configureAuthenticationProvider(AuthenticationManagerBuilder auth) throws Exception {
// TODO
}
/**
* Custom message resource in order not to be poisoned if application is also defining one.
* @return
*/
@Bean("messageResourceSecurityPerso")
public MessageSource messageResource() {
ResourceBundleMessageSource messageBundleResrc = new ResourceBundleMessageSource();
messageBundleResrc.setBasename("messages");
messageBundleResrc.setDefaultEncoding(StandardCharsets.UTF_8.name());
return messageBundleResrc;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Permit to have have all URL patters for public resources.
* @return URL patters for public resources
*/
private List<String> getPublicResources() {
List<String> defaultPublicPatters = Arrays.asList("/**/*.css", "/**/*.gif", "/**/*.png", "/**/*.jpeg",
"/**/*.jpg");
List<String> publicEnpointsProperties = this.persoSecurityProperties.getPublicEndpoints();
StringJoiner joiner = new StringJoiner("', '", "'", "'");
List<String> publicEndpoints = Stream.concat(defaultPublicPatters.stream(), publicEnpointsProperties.stream())
.filter(Objects::nonNull).filter(StringUtils::hasText).collect(Collectors.toList());
publicEndpoints.forEach(joiner::add);
LOGGER.info("Public URL patterns: " + joiner);
return publicEndpoints;
}
private CsrfTokenRepository getCsrfTokenRepository() {
// The cookie can be read by javascript
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
// Front-end and back-end are on different context paths so set cookie path to '/'
tokenRepository.setCookiePath("/");
return tokenRepository;
}
} |
Authentication filter (pour gérer la connexion sur l'URI '/api/login' et la création du token JWT):
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private AuthenticationManager authenticationManager;
private String jwtSecret;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, String jwtSecret) {
this.authenticationManager = authenticationManager;
this.jwtSecret = jwtSecret;
setFilterProcessesUrl("/api/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
LOGGER.info("JwtAuthenticationFilter.attemptAuthentication()");
try {
// Get credentials from request
UserCredentials credentials = new ObjectMapper().readValue(request.getInputStream(), UserCredentials.class);
// Create auth object (contains credentials) which will be used by auth manager
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
credentials.getUsername(), credentials.getPassword(), Collections.emptyList());
// Authentication manager authenticate the user, and use
// UserDetailsService.loadUserByUsername() method to load the user
return this.authenticationManager.authenticate(authToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain, Authentication authentication) {
LOGGER.info("JwtAuthenticationFilter.successfulAuthentication()");
UserCredentials user = (UserCredentials) authentication.getPrincipal();
// INFO: username is the email
String token = TokenUtils.buildToken(user.getUsername(), this.jwtSecret);
response.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
}
} |
Authorization filter (qui gère la vérification du token pour toutes les requêtes sur des URIs sécurisées):
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthorizationFilter.class);
private String jwtSecret;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, String jwtSecret) {
super(authenticationManager);
this.jwtSecret = jwtSecret;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
LOGGER.info("JwtAuthorizationFilter.doFilterInternal()");
UsernamePasswordAuthenticationToken authentication = parseToken(request);
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken parseToken(HttpServletRequest request) {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.replace("Bearer ", "");
try {
TokenData tokenData = TokenUtils.verifyToken(token, this.jwtSecret);
// TODO retrieve roles for email with a service to do a query
List<GrantedAuthority> authorities = new ArrayList<>();
return new UsernamePasswordAuthenticationToken(tokenData.getEmail(), null, authorities);
} catch (JwtException exception) {
LOGGER.warn("Parse token in authorization header '{}' failed with following error: {}",
authorizationHeader, exception.getMessage());
}
}
return null;
}
} |
Ce que je souhaite est que le front-end appelle '/api/login' avec le username et le password de l'utilisateur ce qui doit permettre de vérifier son mot de passe et créer le token si c'est bon qui sera mis dans le header de la réponse. Ensuite, pour chaque requête sur une URI sécurisée, on vérifiera le token et on récupèrera les informations utilisateur dont ses droits afin d'initialiser le contexte de sécurité Spring (=liste de GrantedAuthority).
Est-ce que cette logique et ce que j'ai initialisé vous semble correct?
Egalement, comment puis-je récupérer l'authentication manager dont ont besoin mes filtres car avant on pouvait étendre WebSecurityConfigurerAdapter et surcharger le bean authenticationManagerBean() mais étendre WebSecurityConfigurerAdapter est maintenant déprécié.
Aussi, je ne sais pas comment gérer un ou plusieurs UserDetailsService (avec gestion du UsernamePasswordAuthenticationToken) qui doivent gérer la vérification du mot de passe pour le premier filtre par exemple. Je ne sais pas si j'en ai besoin d'un aussi pour le deuxième filtre.
Idem pour la configuration de l'authentication provider (méthode configureAuthenticationProvider() de ma configuration). Je ne sais pas si j'ai des choses à configurer.
Enfin, je ne sais pas si j'ai besoin d'un token CSRF pour plus de sécurité car il me semblait avoir entendu que ce n'est pas nécessaire quand on a un jeton JWT.
Pouvez-vous m'aider pour tout ca (désolé ca fait beaucoup d'interrogations) svp?