Différences
Ci-dessous, les différences entre deux révisions de la page.
| Prochaine révision | Révision précédente | ||
| web:framework:spring:jwt [2025/03/14 08:11] – créée jcheron | web:framework:spring:jwt [2025/08/12 02:35] (Version actuelle) – modification externe 127.0.0.1 | ||
|---|---|---|---|
| Ligne 7: | Ligne 7: | ||
| < | < | ||
| </ | </ | ||
| + | |||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| </ | </ | ||
| + | ===== Configuration ===== | ||
| + | <sxh kotlin> | ||
| + | @Configuration | ||
| + | @EnableWebSecurity | ||
| + | @EnableMethodSecurity | ||
| + | class SecurityConfig { | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var rsaKeyConfigProperties: | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var userDetailsService: | ||
| + | |||
| + | @Value(" | ||
| + | private lateinit var allowedOrigins: | ||
| + | |||
| + | |||
| + | @Bean | ||
| + | fun authManager(): | ||
| + | val authProvider = DaoAuthenticationProvider() | ||
| + | authProvider.setUserDetailsService(userDetailsService) | ||
| + | authProvider.setPasswordEncoder(passwordEncoder()) | ||
| + | return ProviderManager(authProvider) | ||
| + | } | ||
| + | |||
| + | |||
| + | @Bean | ||
| + | @Throws(Exception:: | ||
| + | fun filterChain(http: | ||
| + | return http | ||
| + | .csrf { csrf: CsrfConfigurer< | ||
| + | csrf.disable() | ||
| + | } | ||
| + | .cors(Customizer.withDefaults()) | ||
| + | .authorizeHttpRequests { auth -> | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | |||
| + | |||
| + | auth.requestMatchers("/ | ||
| + | |||
| + | auth.anyRequest().authenticated() | ||
| + | }.headers { headers -> | ||
| + | headers.frameOptions { it.disable() } | ||
| + | } | ||
| + | .sessionManagement { s: SessionManagementConfigurer< | ||
| + | s.sessionCreationPolicy( | ||
| + | SessionCreationPolicy.STATELESS | ||
| + | ) | ||
| + | } | ||
| + | .oauth2ResourceServer { oauth2: OAuth2ResourceServerConfigurer< | ||
| + | oauth2.jwt { jwt -> | ||
| + | jwt.decoder( | ||
| + | jwtDecoder() | ||
| + | ) | ||
| + | } | ||
| + | } | ||
| + | .userDetailsService(userDetailsService) | ||
| + | .httpBasic(Customizer.withDefaults()) | ||
| + | .build() | ||
| + | } | ||
| + | |||
| + | @Bean | ||
| + | fun jwtDecoder(): | ||
| + | return NimbusJwtDecoder.withPublicKey(rsaKeyConfigProperties.publicKey).build() | ||
| + | } | ||
| + | |||
| + | @Bean | ||
| + | fun jwtEncoder(): | ||
| + | val jwk: JWK = | ||
| + | RSAKey.Builder(rsaKeyConfigProperties.publicKey).privateKey(rsaKeyConfigProperties.privateKey).build() | ||
| + | |||
| + | val jwks: JWKSource< | ||
| + | return NimbusJwtEncoder(jwks) | ||
| + | } | ||
| + | |||
| + | @Bean | ||
| + | fun passwordEncoder(): | ||
| + | return BCryptPasswordEncoder() | ||
| + | } | ||
| + | |||
| + | @Bean | ||
| + | fun corsConfigurationSource(): | ||
| + | val source = UrlBasedCorsConfigurationSource() | ||
| + | val config = CorsConfiguration() | ||
| + | config.allowedOrigins = allowedOrigins.split("," | ||
| + | config.allowedMethods = listOf(" | ||
| + | config.allowedHeaders = listOf(" | ||
| + | config.allowCredentials = true | ||
| + | source.registerCorsConfiguration("/ | ||
| + | return source | ||
| + | } | ||
| + | |||
| + | |||
| + | companion object { | ||
| + | |||
| + | private val log: Logger = LoggerFactory.getLogger(SecurityConfig:: | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== RSA config ===== | ||
| + | <sxh kotlin> | ||
| + | @ConfigurationProperties(prefix = " | ||
| + | @JvmRecord | ||
| + | data class RsaKeyConfigProperties(val publicKey: RSAPublicKey, | ||
| + | </ | ||
| + | |||
| + | |||
| + | ==== Génération des clés RSA ==== | ||
| + | Avec git bash : | ||
| + | <sxh bash; | ||
| + | genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits: | ||
| + | </ | ||
| + | |||
| + | <sxh bash; | ||
| + | openssl rsa -in private.pem -pubout -out public.pem | ||
| + | </ | ||
| + | ==== AuthUser ==== | ||
| + | <sxh kotlin> | ||
| + | class AuthUser(val user: User) : UserDetails { | ||
| + | |||
| + | override fun getAuthorities(): | ||
| + | return mutableListOf(SimpleGrantedAuthority(" | ||
| + | } | ||
| + | |||
| + | override fun getPassword(): | ||
| + | |||
| + | override fun getUsername(): | ||
| + | |||
| + | override fun isAccountNonExpired(): | ||
| + | |||
| + | override fun isAccountNonLocked(): | ||
| + | |||
| + | override fun isCredentialsNonExpired(): | ||
| + | |||
| + | override fun isEnabled(): | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== Services ===== | ||
| + | |||
| + | <sxh kotlin> | ||
| + | @Service | ||
| + | class JpaUserDetailsService( | ||
| + | val userRepository: | ||
| + | val logEventRepository: | ||
| + | ) : UserDetailsService { | ||
| + | |||
| + | |||
| + | @Throws(UsernameNotFoundException:: | ||
| + | @Transactional | ||
| + | override fun loadUserByUsername(usernameOrEmail: | ||
| + | val user: User = userRepository | ||
| + | .findByUsernameOrEmail(usernameOrEmail, | ||
| + | .orElseThrow { UsernameNotFoundException(" | ||
| + | return AuthUser(user) | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <sxh kotlin> | ||
| + | @Service | ||
| + | class AuthService { | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var jwtEncoder: JwtEncoder | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var JwtDecoder: JwtDecoder | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var passwordEncoder: | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var userRepository: | ||
| + | |||
| + | fun generateToken(authentication: | ||
| + | val now = Instant.now() | ||
| + | |||
| + | val scope: String = authentication.getAuthorities() | ||
| + | .stream() | ||
| + | .map { obj: GrantedAuthority -> obj.authority } | ||
| + | .collect(Collectors.joining(" | ||
| + | val user = (authentication.principal as AuthUser).user | ||
| + | val claims = JwtClaimsSet.builder() | ||
| + | .issuer(" | ||
| + | .issuedAt(now) | ||
| + | .expiresAt(now.plus(10, | ||
| + | .subject(authentication.getName()) | ||
| + | .claim(" | ||
| + | .claim(" | ||
| + | .claim(" | ||
| + | .claim(" | ||
| + | .build() | ||
| + | |||
| + | return jwtEncoder.encode(JwtEncoderParameters.from(claims)).tokenValue | ||
| + | } | ||
| + | |||
| + | fun getActiveUser(token: | ||
| + | val claims = JwtDecoder.decode(token).claims | ||
| + | val userId = claims[" | ||
| + | return userRepository.findById(userId).orElseThrow { RuntimeException(" | ||
| + | } | ||
| + | |||
| + | fun hashPassword(password: | ||
| + | if (!isBCryptHash(password)) { | ||
| + | return passwordEncoder.encode(password) | ||
| + | } | ||
| + | return password | ||
| + | } | ||
| + | |||
| + | fun isBCryptHash(password: | ||
| + | return password.matches(Regex(" | ||
| + | } | ||
| + | |||
| + | } | ||
| + | </ | ||