Я хотел бы защитить свой сервер ресурсов с помощью Spring Security 5, чтобы его могли использовать многие пользователи. У меня есть сущность Task, которая содержит поле userId.
@Entity
@Table(name = "tasks")
data class Task(
@NotBlank
val name: String,
val description: String?,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = "task_id_seq")
val id: Long = 0,
@UUID //custom validation annotation
val userId: String? = null
)
Я нашел способ использовать SpEL для внедрения принципала JWT внутрь запроса. Что я уже сделал, так это переопределил метод findAll() в моем interface TaskRepository : JpaRepository<Task, Long> с аннотацией @Query:
@Query("select t from Task t where t.userId = :#{principal.claims['sub']}")
//sub is a userId in OpenID Connect 1.0, claim is a Map<String, Object>
override fun findAll(): List<Task>
Проблема в том, что я хотел бы сделать что-то подобное с методом save(). Единственное, что я нашел, это обновить уже сохраненный объект. Я создал совершенно новый метод:
@Modifying
@Query("update Task t set t.userId = :#{principal.claims['sub']} where t.id = :id")
fun setUserId(@Param("id") id: Long)
но проблема в том, что мне нужно вызвать этот метод в моем TaskService, что выглядит так:
@Transactional
fun createTask(taskDto: CreateTaskDto): Task {
val task = taskRepository.save(taskDto.toEntity())
taskRepository.setUserId(task.id)
return task
}
Можно ли автоматически добавлять эту информацию каждый раз, когда я сохраняю объект в своей базе данных? Тогда мой код внутри TaskService будет выглядеть так:
@Transactional
fun createTask(taskDto: CreateTaskDto): Task {
return taskRepository.save(taskDto.toEntity())
}
Это самое простое, но самое раздражающее решение этой проблемы. Каждая конечная точка должна будет добавить принципала в качестве параметра, и это необходимо для дальнейшего прохождения. Во-вторых, мне нужно было бы везде использовать некоторый класс Util, чтобы получить идентификатор пользователя. Когда я делаю это на уровне JpaRepository, я думаю, что это самое элегантное и наиболее подходящее решение, потому что я имею дело с предоставлением авторизации на уровне данных. Сервис и RestController ничего не знают о пользователе, и код намного чище.
мне нравится четкий и понятный код, даже если писать надоедает ;-)
Вы предполагаете, что, по вашему мнению, мое решение менее понятно, чем каждый раз передавать Principal внутри методов обслуживания?
По крайней мере, я полагаю, что «раздражающий» не является аргументом в пользу того, что решение хорошее или плохое. В моем случае я каждый раз передаю принципала в методы службы, потому что, если вы читаете метод, который вы знаете об аутентификации, уровень данных, следовательно, довольно глуп.. с другой стороны, это функция аудита, о которой я еще не знаю, после прочтения это выглядит довольно полезным. Но вы должны знать об этом - и это может быть некоторый риск




Решение на самом деле довольно простое, но я не знал, что ищу. Тем не менее, я не знаю, является ли это каноническим решением. Для этого я использую Jpa Auditing. Во-первых, я добавил аннотацию @CreatedBy к полю, которое я хотел обновить перед вставкой внутри моей сущности. Класс сущности, который я также пометил @EntityListeners(AuditingEntityListener::class). Обратите внимание, что было необходимо изменить ключевое слово val на var (с поля final на неконечное поле в мире Java).
@Entity
@Table(name = "tasks")
@EntityListeners(AuditingEntityListener::class)
data class Task(
@NotBlank
val name: String,
val description: String?,
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = "task_id_seq")
val id: Long = 0,
@UUID
@CreatedBy
var userId: String? = null
)
Следующим шагом было реализовать interface AuditorAware<T> и сделать его видимым для Spring с помощью @Component (или создать bean-компонент внутри класса конфигурации)
@Component
class SpringSecurityAuditorAware : AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
return Optional.ofNullable(SecurityContextHolder.getContext()?.authentication)
.filter { authentication -> authentication.isAuthenticated }
.map { authentication -> authentication.principal as Jwt }
.map { jwt -> jwt.claims["sub"] as String? }
}
}
И последнее, но не менее важное: добавление @EnableJpaAuditing в некоторый класс конфигурации:
@Configuration
@EnableJpaAuditing
class ResourceServerContext
Почему бы не внедрить принципала/аутентификацию в RestController и не предоставить его службе, а службе разрешить проверку подлинности?