Skip to content

Commit

Permalink
🔒 Security with JWT Tokens (Merge)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbatovK authored Feb 6, 2024
2 parents 532eb73 + 9685649 commit bfac6dd
Show file tree
Hide file tree
Showing 21 changed files with 487 additions and 29 deletions.
7 changes: 5 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ dependencies {
runtimeOnly("org.postgresql:postgresql")

// Security
// implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
implementation("io.jsonwebtoken:jjwt-impl:0.12.3")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.3")

// Swagger-UI + OpenApi
// OpenApi + SwaggerUI
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0")

// Validation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.albatros.springsecurity

import com.albatros.springsecurity.config.api.ApiInfoConfig
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableConfigurationProperties(ApiInfoConfig::class)
@EnableCaching
@ConfigurationPropertiesScan
class SpringSecurityApplication

fun main(args: Array<String>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.albatros.springsecurity.config.api
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotEmpty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.validation.annotation.Validated

@Validated
@EnableConfigurationProperties(ApiInfoConfig::class)
@ConfigurationProperties(prefix = "api-info")
data class ApiInfoConfig(
@field:NotEmpty
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.albatros.springsecurity.config.security

import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotEmpty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.validation.annotation.Validated

@Validated
@EnableConfigurationProperties(JwtConfig::class)
@ConfigurationProperties("jwt")
data class JwtConfig(
@field:NotEmpty
val key: String,
@field:Min(0)
val accessTokenExpiration: Long,
@field:Min(0)
val refreshTokenExpiration: Long,
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,79 @@
package com.albatros.springsecurity.config.security

//import org.springframework.context.annotation.Configuration
//import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
//
//@Configuration
//@EnableWebSecurity
//class SecurityConfig {
//
//}
//
import com.albatros.springsecurity.domain.service.UserService
import com.albatros.springsecurity.filter.JwtAuthenticationFilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
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.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration

@Configuration
@EnableMethodSecurity
@EnableWebSecurity
class SecurityConfig(
private val jwtAuthenticationFilter: JwtAuthenticationFilter,
private val userService: UserService
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http.csrf { it.disable() }
.cors {
it.configurationSource {
val corsConfig = CorsConfiguration()
corsConfig.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
corsConfig.setAllowedOriginPatterns(
listOf("*")
)
corsConfig.allowedHeaders = listOf("*")
corsConfig.allowCredentials = true
corsConfig
}
}
.authorizeHttpRequests {
it.requestMatchers("/auth/**").permitAll()
.requestMatchers("/user/admin").hasRole("USER")
.requestMatchers("/user/**").hasRole("ADMIN")
.requestMatchers(
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/*",
"/api-docs/**",
"/api-docs"
).permitAll()
.anyRequest().authenticated()
}
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authenticationProvider(
getAuthenticationProvider()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.build()

@Bean
fun getAuthenticationProvider(): AuthenticationProvider =
DaoAuthenticationProvider().apply {
setUserDetailsService(userService.userDetailsService())
setPasswordEncoder(getPasswordEncoder())
}

@Bean
fun getPasswordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

@Bean
fun getAuthenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
config.authenticationManager

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.albatros.springsecurity.controller

import com.albatros.springsecurity.domain.model.request.SignInRequest
import com.albatros.springsecurity.domain.model.request.SignUpRequest
import com.albatros.springsecurity.domain.service.AuthenticationService
import jakarta.validation.Valid
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@Validated
@RequestMapping("/auth")
class AuthController(
private val authenticationService: AuthenticationService,
) {
@PostMapping("/sign-up")
fun signUp(@Valid @RequestBody signUpRequest: SignUpRequest) = authenticationService.signUp(signUpRequest)

@PostMapping("/sign-in")
fun signIn(@Valid @RequestBody signInRequest: SignInRequest) = authenticationService.signIn(signInRequest)

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ class UserController(private val service: UserService) {
fun deleteUser(@PathVariable userId: Long) = service.deleteById(userId)

@PostMapping("/save", consumes = ["application/json"])
fun saveUser(@Valid @RequestBody user: User) = service.saveUser(user)
fun saveUser(@Valid @RequestBody user: User) = service.createUser(user)

@GetMapping("/get/{userId}")
fun getById(@PathVariable userId: Long) = service.getUserById(userId)

@GetMapping("/admin")
@Deprecated(message = "For demonstration purposes only")
fun getAdminForCurrentUser() = service.getAdmin()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,56 @@ package com.albatros.springsecurity.domain.model.database

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import org.springframework.validation.annotation.Validated
import java.io.Serializable
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.validation.annotation.Validated

@Entity(name = "users")
@Validated
class User(
@Column(nullable = false, length = 30)

@Column(nullable = false, length = 10)
@field:NotBlank
var name: String,
@Column(nullable = false, length = 125)
private var username: String,

@Column(nullable = false, unique = true, length = 20)
@field:Email
var email: String,
) : AbstractEntity(), Serializable

@Column(nullable = false, length = 120)
@field:NotBlank
private var password: String,

@Enumerated(EnumType.STRING)
@Column(nullable = false)
var role: Role = Role.ROLE_USER

) : AbstractEntity(), UserDetails, Serializable {
override fun getAuthorities(): MutableCollection<out GrantedAuthority> = mutableListOf(
SimpleGrantedAuthority(role.name)
)

override fun getPassword(): String = password

override fun getUsername(): String = username

override fun isAccountNonExpired(): Boolean = true

override fun isAccountNonLocked(): Boolean = true

override fun isCredentialsNonExpired(): Boolean = true

override fun isEnabled(): Boolean = true

}

enum class Role {
ROLE_USER,
ROLE_ADMIN;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.albatros.springsecurity.domain.model.exception

import org.springframework.http.HttpStatus

class AlreadyExistsException(override val message: String) : AbstractApiException() {
override val status: HttpStatus
get() = HttpStatus.CONFLICT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.albatros.springsecurity.domain.model.request

import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

data class SignInRequest(

@field:Size(min = 5, max = 30)
@field:NotBlank
val username: String,

@field:Size(min = 5, max = 30)
@field:NotBlank
val password: String

)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.albatros.springsecurity.domain.model.request

import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

data class SignUpRequest(

@field:Size(min = 5, max = 30)
@field:NotBlank
val username: String,

@field:Email
val email: String,

@field:Size(min = 5, max = 30)
@field:NotBlank
val password: String

)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.albatros.springsecurity.domain.model.response

data class JwtAuthenticationResponse(
val token: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@ import com.albatros.springsecurity.domain.model.database.User
import org.springframework.stereotype.Repository

@Repository
interface UserRepository : CommonRepository<User>
interface UserRepository : CommonRepository<User> {
fun findByUsername(username: String): User?

fun existsByUsername(username: String): Boolean

fun existsByEmail(email: String): Boolean

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.albatros.springsecurity.domain.service

import com.albatros.springsecurity.domain.model.request.SignInRequest
import com.albatros.springsecurity.domain.model.request.SignUpRequest
import com.albatros.springsecurity.domain.model.response.JwtAuthenticationResponse
import org.springframework.validation.annotation.Validated

@Validated
interface AuthenticationService {

fun signUp(request: SignUpRequest): JwtAuthenticationResponse

fun signIn(request: SignInRequest): JwtAuthenticationResponse

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.albatros.springsecurity.domain.service

import java.util.Date
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.validation.annotation.Validated

@Validated
interface JwtService {
fun isTokenValid(token: String, userDetails: UserDetails): Boolean
fun extractUsername(token: String): String?
fun generateToken(userDetails: UserDetails): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import com.albatros.springsecurity.domain.model.database.User
import jakarta.validation.Valid
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.validation.annotation.Validated

@Validated
interface UserService {
fun deleteById(userId: Long)
fun saveUser(@Valid user: User): User
fun createUser(@Valid user: User): User
fun getUserById(userId: Long): User
fun list(): List<User>
fun listPaginated(page: Pageable): Slice<User>
fun updateUser(@Valid user: User, userId: Long): User

fun userDetailsService(): UserDetailsService

@Deprecated(message = "For demonstration purposes only")
fun getAdmin()

}
Loading

0 comments on commit bfac6dd

Please sign in to comment.