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:oauth2 [2024/04/16 11:55] – créée jcheron | web:framework:spring:oauth2 [2024/04/16 13:58] (Version actuelle) – jcheron | ||
|---|---|---|---|
| Ligne 1: | Ligne 1: | ||
| ====== Spring OAuth2 ====== | ====== Spring OAuth2 ====== | ||
| - | Mise en place d' | + | Mise en place d' |
| + | |||
| + | [[https:// | ||
| + | |||
| + | OAuth2 utilise des jetons, matérialisant l' | ||
| Ligne 16: | Ligne 20: | ||
| < | < | ||
| </ | </ | ||
| + | </ | ||
| + | |||
| + | ===== Sécurisation ===== | ||
| + | |||
| + | Les clés RSA (publique et privée) sont utilisées dans le contexte d'un serveur OAuth 2 pour sécuriser les échanges de données entre les différentes parties impliquées dans le processus d' | ||
| + | |||
| + | ==== Génération des clés ==== | ||
| + | Générer une clé publique et une privée avec openSSL (à installer éventuellement) : | ||
| + | |||
| + | Créer un dossier '' | ||
| + | |||
| + | Créer la clé privée dans **certs** | ||
| + | <sxh bash; | ||
| + | openssl genpkey -algorithm RSA -out private-key.pem | ||
| + | </ | ||
| + | |||
| + | Extraire la clé publique à partir de la clé privée : | ||
| + | <sxh bash; | ||
| + | openssl rsa -pubout -in private-key.pem -out public-key.pem | ||
| + | </ | ||
| + | |||
| + | Convertir la clé privée au format PKCS : | ||
| + | <sxh bash; | ||
| + | openssl pkcs8 -topk8 -inform PEM -outform PEM -in private-key.pem -out private-key-used.pem -nocrypt | ||
| + | </ | ||
| + | |||
| + | |||
| + | La clé privée est gardée secrète et ne devra jamais être partagée, tandis que la clé publique pourra être distribuée librement. | ||
| + | |||
| + | ==== Usage des clés RSA ==== | ||
| + | |||
| + | === Signature des JWT (JSON Web Tokens) === | ||
| + | | ||
| + | |||
| + | === Vérification des JWT === | ||
| + | Lorsqu' | ||
| + | |||
| + | En résumé, les clés RSA (publique et privée) sont utilisées dans le cadre d' | ||
| + | |||
| + | ==== Intégration RSA/Spring ==== | ||
| + | |||
| + | Créer une classe pour gérer les properties à ajouter pour stocker les 2 clés : | ||
| + | |||
| + | Dans un package **security** à créer : | ||
| + | |||
| + | <sxh kotlin; | ||
| + | import org.springframework.boot.context.properties.ConfigurationProperties | ||
| + | import java.security.interfaces.RSAPrivateKey | ||
| + | import java.security.interfaces.RSAPublicKey | ||
| + | |||
| + | |||
| + | @ConfigurationProperties(prefix = " | ||
| + | @JvmRecord | ||
| + | data class RsaKeyConfigProperties(val publicKey: RSAPublicKey, | ||
| + | </ | ||
| + | |||
| + | Activer cette classe de propriétés directement sur la classe de votre application Spring : | ||
| + | |||
| + | <sxh kotlin; | ||
| + | import fr.zerp.api.security.RsaKeyConfigProperties | ||
| + | import org.springframework.boot.autoconfigure.SpringBootApplication | ||
| + | import org.springframework.boot.context.properties.EnableConfigurationProperties | ||
| + | import org.springframework.boot.runApplication | ||
| + | |||
| + | |||
| + | @SpringBootApplication | ||
| + | @EnableConfigurationProperties(RsaKeyConfigProperties:: | ||
| + | class MyApplication | ||
| + | </ | ||
| + | |||
| + | |||
| + | Ajouter les 2 clés à **application.properties** : | ||
| + | |||
| + | <sxh yaml;title: application.properties> | ||
| + | #JWT | ||
| + | rsa.private-key=classpath: | ||
| + | rsa.public-key=classpath: | ||
| + | </ | ||
| + | |||
| + | ==== Services et authentification ==== | ||
| + | |||
| + | === AuthUser === | ||
| + | Créer une classe AuthUser encapsulant un **User** et implémentant l' | ||
| + | |||
| + | <sxh kotlin; | ||
| + | class AuthUser(user: | ||
| + | |||
| + | val user: User = user | ||
| + | |||
| + | override fun getAuthorities(): | ||
| + | return mutableListOf(SimpleGrantedAuthority(" | ||
| + | } | ||
| + | |||
| + | override fun getPassword(): | ||
| + | |||
| + | override fun getUsername(): | ||
| + | |||
| + | override fun isAccountNonExpired(): | ||
| + | |||
| + | override fun isAccountNonLocked(): | ||
| + | |||
| + | override fun isCredentialsNonExpired(): | ||
| + | |||
| + | override fun isEnabled(): | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === UserRepository === | ||
| + | Modifier votre **UserRepository** pour qu'il permette de rechercher un utilisateur par son login/ | ||
| + | |||
| + | <sxh kotlin> | ||
| + | @RepositoryRestResource(collectionResourceRel = " | ||
| + | interface UserRepository : JpaRepository< | ||
| + | fun findByUsernameOrEmail(username: | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === UserDetailsService === | ||
| + | |||
| + | <sxh kotlin> | ||
| + | import org.springframework.beans.factory.annotation.Autowired | ||
| + | import org.springframework.security.core.userdetails.UserDetails | ||
| + | import org.springframework.security.core.userdetails.UserDetailsService | ||
| + | import org.springframework.security.core.userdetails.UsernameNotFoundException | ||
| + | import org.springframework.stereotype.Service | ||
| + | |||
| + | |||
| + | @Service | ||
| + | class JpaUserDetailsService : UserDetailsService { | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var userRepository: | ||
| + | |||
| + | @Throws(UsernameNotFoundException:: | ||
| + | override fun loadUserByUsername(usernameOrEmail: | ||
| + | val user: AuthUser = userRepository | ||
| + | .findByUsernameOrEmail(usernameOrEmail, | ||
| + | .map { AuthUser(it) } | ||
| + | .orElseThrow { UsernameNotFoundException(" | ||
| + | |||
| + | return user | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | === AuthService === | ||
| + | |||
| + | <sxh kotlin; | ||
| + | import org.springframework.beans.factory.annotation.Autowired | ||
| + | import org.springframework.security.core.Authentication | ||
| + | import org.springframework.security.core.GrantedAuthority | ||
| + | import org.springframework.security.crypto.password.PasswordEncoder | ||
| + | import org.springframework.security.oauth2.jwt.JwtClaimsSet | ||
| + | import org.springframework.security.oauth2.jwt.JwtDecoder | ||
| + | import org.springframework.security.oauth2.jwt.JwtEncoder | ||
| + | import org.springframework.security.oauth2.jwt.JwtEncoderParameters | ||
| + | import org.springframework.stereotype.Service | ||
| + | import java.time.Instant | ||
| + | import java.time.temporal.ChronoUnit | ||
| + | import java.util.* | ||
| + | import java.util.stream.Collectors | ||
| + | |||
| + | |||
| + | @Service | ||
| + | class AuthService { | ||
| + | |||
| + | @Autowired | ||
| + | private val jwtEncoder: JwtEncoder? = null | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var JwtDecoder: JwtDecoder | ||
| + | |||
| + | @Autowired | ||
| + | private val passwordEncoder: | ||
| + | |||
| + | @Autowired | ||
| + | private val userRepository: | ||
| + | |||
| + | fun generateToken(authentication: | ||
| + | val now = Instant.now() | ||
| + | |||
| + | val scope: String = authentication.getAuthorities() | ||
| + | .stream() | ||
| + | .map { obj: GrantedAuthority -> obj.authority } | ||
| + | .collect(Collectors.joining(" | ||
| + | |||
| + | val claims = JwtClaimsSet.builder() | ||
| + | .issuer(" | ||
| + | .issuedAt(now) | ||
| + | .expiresAt(now.plus(10, | ||
| + | .subject(authentication.getName()) | ||
| + | .claim(" | ||
| + | .claim(" | ||
| + | .build() | ||
| + | |||
| + | return jwtEncoder!!.encode(JwtEncoderParameters.from(claims)).tokenValue | ||
| + | } | ||
| + | |||
| + | //Exemple de récupération de données dans le token JWT | ||
| + | fun getActiveUser(token: | ||
| + | val claims = JwtDecoder.decode(token).claims | ||
| + | val userId = claims[" | ||
| + | return userRepository!!.findById(userId).orElseThrow { RuntimeException(" | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== Configuration ==== | ||
| + | |||
| + | <sxh kotlin; | ||
| + | import com.nimbusds.jose.jwk.JWK | ||
| + | import com.nimbusds.jose.jwk.JWKSet | ||
| + | import com.nimbusds.jose.jwk.RSAKey | ||
| + | import com.nimbusds.jose.jwk.source.ImmutableJWKSet | ||
| + | import com.nimbusds.jose.jwk.source.JWKSource | ||
| + | import com.nimbusds.jose.proc.SecurityContext | ||
| + | import fr.zerp.api.security.JpaUserDetailsService | ||
| + | import fr.zerp.api.security.RsaKeyConfigProperties | ||
| + | import org.slf4j.Logger | ||
| + | import org.slf4j.LoggerFactory | ||
| + | import org.springframework.beans.factory.annotation.Autowired | ||
| + | import org.springframework.context.annotation.Bean | ||
| + | import org.springframework.context.annotation.Configuration | ||
| + | import org.springframework.security.authentication.AuthenticationManager | ||
| + | import org.springframework.security.authentication.ProviderManager | ||
| + | import org.springframework.security.authentication.dao.DaoAuthenticationProvider | ||
| + | import org.springframework.security.config.Customizer | ||
| + | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity | ||
| + | import org.springframework.security.config.annotation.web.builders.HttpSecurity | ||
| + | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity | ||
| + | import org.springframework.security.config.annotation.web.configurers.CorsConfigurer | ||
| + | import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer | ||
| + | import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer | ||
| + | import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer | ||
| + | import org.springframework.security.config.http.SessionCreationPolicy | ||
| + | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder | ||
| + | import org.springframework.security.crypto.password.PasswordEncoder | ||
| + | import org.springframework.security.oauth2.jwt.JwtDecoder | ||
| + | import org.springframework.security.oauth2.jwt.JwtEncoder | ||
| + | import org.springframework.security.oauth2.jwt.NimbusJwtDecoder | ||
| + | import org.springframework.security.oauth2.jwt.NimbusJwtEncoder | ||
| + | import org.springframework.security.web.SecurityFilterChain | ||
| + | import org.springframework.web.servlet.handler.HandlerMappingIntrospector | ||
| + | |||
| + | |||
| + | @Configuration | ||
| + | @EnableWebSecurity | ||
| + | @EnableMethodSecurity | ||
| + | class SecurityConfig { | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var rsaKeyConfigProperties: | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var userDetailsService: | ||
| + | |||
| + | |||
| + | @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 { cors: CorsConfigurer< | ||
| + | .authorizeHttpRequests { auth -> | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | auth.requestMatchers("/ | ||
| + | auth.anyRequest().authenticated() | ||
| + | }.headers { headers -> | ||
| + | headers.frameOptions { it.sameOrigin() } | ||
| + | } | ||
| + | .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() | ||
| + | } | ||
| + | |||
| + | companion object { | ||
| + | private val log: Logger = LoggerFactory.getLogger(SecurityConfig:: | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | ==== Authentification ==== | ||
| + | === DTO === | ||
| + | |||
| + | <sxh kotlin> | ||
| + | class AuthDTO { | ||
| + | @JvmRecord | ||
| + | data class LoginRequest(val username: String, val password: String) | ||
| + | |||
| + | @JvmRecord | ||
| + | data class Response(val message: String, val token: String) | ||
| + | } | ||
| + | </ | ||
| + | === Controller === | ||
| + | |||
| + | <sxh kotlin> | ||
| + | @RestController | ||
| + | @RequestMapping("/ | ||
| + | @Validated | ||
| + | class AuthController { | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var authService: | ||
| + | |||
| + | @Autowired | ||
| + | lateinit var authenticationManager: | ||
| + | |||
| + | @PostMapping("/ | ||
| + | @Throws(IllegalAccessException:: | ||
| + | fun login(@RequestBody userLogin: AuthDTO.LoginRequest): | ||
| + | val authentication: | ||
| + | authenticationManager | ||
| + | .authenticate( | ||
| + | UsernamePasswordAuthenticationToken( | ||
| + | userLogin.username, | ||
| + | userLogin.password | ||
| + | ) | ||
| + | ) | ||
| + | SecurityContextHolder.getContext().authentication = authentication | ||
| + | val userDetails = authentication.getPrincipal() as AuthUser | ||
| + | log.info(" | ||
| + | val token = authService.generateToken(authentication) | ||
| + | val response: AuthDTO.Response = AuthDTO.Response(" | ||
| + | return ResponseEntity.ok< | ||
| + | } | ||
| + | |||
| + | companion object { | ||
| + | private val log: Logger = LoggerFactory.getLogger(AuthController:: | ||
| + | } | ||
| + | } | ||
| </ | </ | ||