본문 바로가기

Spring Boot/유저 관리 기능

인가와 권한 관리 : Spring Security와 권한 검증 플로우

권한 기반 접근 제어의 예제

 

https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/?utm_source=chatgpt.com

 

Spring Boot Reference Documentation

This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe

docs.spring.io

 

위 링크에서

h2ConsoleSecurityFilterChain

 

를 검색하면 h2 에 대한 FilterChain 관련 이슈를 해결할 수 있다

 


Entity

 

User.java

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;

import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users")
@Getter
@Setter
public class User {
    @Id
    private String username; // 유저 이름

    @Column(nullable = false, length = 14)
    private String password; // 비밀번호

    @ManyToMany(fetch = FetchType.EAGER)
    private Set<Role> roles = new HashSet<>(); // 권한

    public User() {

    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public Set<Role> getRoles() {
        return roles;
    }
}

 

 

Role.java

import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;

import java.util.HashSet;
import java.util.Set;

@Entity
public class Role {
    @Id
    private String name; // ROLE_EDITOR, ROLE_VIEWER 등등..

    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> permissions = new HashSet<>();

    public Role() {

    }

    public Role(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public Set<String> getPermissions() {
        return permissions;
    }
}

 

 

Post.java

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id; // 포스트 아이디

    @Column(nullable = false)
    private String owner; // 작성자

    @Column(nullable = false)
    private String content; // 내용
}

 

 

Permission.java

import java.util.Objects;

public class Permission {
    private final ResourceType resourceType;
    private final Action action;

    public Permission(ResourceType resourceType, Action action) {
        this.resourceType = resourceType;
        this.action = action;
    }

    // getter
    public ResourceType getResourceType() {
        return resourceType;
    }

    public Action getAction() {
        return action;
    }

    @Override
    public int hashCode() {
        return Objects.hash(resourceType, action);
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Permission)) {
            return false;
        }
        Permission permission = (Permission) obj;

        return permission.resourceType == resourceType && permission.action == action;
    }

    @Override
    public String toString() {
        return resourceType + ":" + action;
    }
}

 

 

Action.java

public enum Action {
    READ, CREATE, UPDATE, DELETE
}

 

 

ResourceType.java

public enum ResourceType {
    POST, FILE, ORDER
}

 

 


Service

 

UserService.java

import com.b1uffer.sessiontest.entity.User;
import com.b1uffer.sessiontest.repository.UserRepository;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@NoArgsConstructor
public class UserService {
    private UserRepository userRepository;

    public User create(String username, String password) {
        if(username == null) {
            throw new IllegalArgumentException("Username cannot be null");
        }

        if(username.length() > 12) {
            throw new IllegalArgumentException("Username cannot be longer than 12 characters");
        }

        if(password.length() < 7 || password.length() > 15) {
            throw new IllegalArgumentException("비밀번호는 8자리에서 14자리 사이입니다.");
        }

        User user = new User(username, password);
        userRepository.save(user);
        return user;
    }

    public boolean validate(String username, String password) {
        return "user".equals(username) && "password".equals(password);
    }
}

 

 

PostService.java

import com.b1uffer.sessiontest.entity.Post;
import com.b1uffer.sessiontest.repository.PostRepository;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@NoArgsConstructor
public class PostService {
    private PostRepository postRepository;

    public Post update(Long id, Post post) {
        Post findPost = postRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("post not found"));
        post.setContent(post.getContent());
        postRepository.save(findPost);
        return findPost;
    }
}

Controller

 

PostController.java

import com.b1uffer.sessiontest.entity.Post;
import com.b1uffer.sessiontest.service.PostService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/posts")
public class PostController {
    private final PostService postService;
    public PostController(PostService postService) {
        this.postService = postService;
    }
    @PreAuthorize("hasPermission(#post, 'UPDATE')") // PreAuthorize
    @PutMapping("/{id}") // PutMapping
    public ResponseEntity<String> updatePost(@PathVariable Long id,
                                             @RequestBody Post post) {
        postService.update(id, post);
        return ResponseEntity.ok("Updated");
    }
}

Security

 

CustomPermissionEvaluator.java

import com.b1uffer.sessiontest.entity.Post;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Override
    public boolean hasPermission(Authentication authentication,
                                 Object targetDomainObject,
                                 Object permission) {
        /**
         * 소유자만 수정 가능하게끔 하기
         */
        if(targetDomainObject instanceof Post post && "UPDATE".equals(permission)) {
            String currentUser = authentication.getName(); // 인증하는 사람 이름
            return post.getOwner().equals(currentUser); // currentUser와 게시글 작성자의 이름 비교
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication,
                                 Serializable targetId,
                                 String targetType,
                                 Object permission) {
        // 커스텀 ID 기반 정책 구현하는곳
        return false;
    }
}

 

 

UserPrincipal.java

import com.b1uffer.sessiontest.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class UserPrincipal implements UserDetails {

    private final User user;

    public UserPrincipal(User user) {
        this.user = user;
    }

    @Override
    public boolean isAccountNonExpired() {
//        return UserDetails.super.isAccountNonExpired();
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
//        return UserDetails.super.isAccountNonLocked();
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
//        return UserDetails.super.isCredentialsNonExpired();
        return true;
    }

    @Override
    public boolean isEnabled() {
//        return UserDetails.super.isEnabled();
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getRoles().stream()
                /**
                 * permission 필드의 타입이 Set<String>이라서 flatMap을 사용함
                 */
                .flatMap(role -> role.getPermissions().stream())
                .map(SimpleGrantedAuthority::new)
                .toList();
    }

    @Override
    public String getPassword() {
        return user.getPassword(); // User의 password를 가져옴
    }

    @Override
    public String getUsername() {
        return user.getUsername(); // User의 username을 가져옴
    }
}

 

 


config

 

SecurityConfiguration.java

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfiguration {
    @Value("${spring.security.oauth2.client.registration.google.client-id}")
    private String clientId;

    @Value("${spring.security.oauth2.client.registration.google.client-secret}")
    private String clientSecret;


    /**
     * Security filter chain
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers(headers -> headers.frameOptions().disable())
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/posts/**").authenticated()
                        .anyRequest().permitAll()
                )
                .oauth2Login(withDefaults());

        return http.build();
    }

    /**
     * JAVA Configuration을 활용해서 OAUTH2 인증을 설정하기
     */
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        var clientRegistration =  clientRegistration();
        return new InMemoryClientRegistrationRepository(clientRegistration);
    }

    private ClientRegistration clientRegistration() {
        return CommonOAuth2Provider
                .GOOGLE
                .getBuilder("google")
                .clientId(clientId)
                .clientSecret(clientSecret)
                .build();
    }

    /**
     * 권한 기반 제어 Security 설정
     */
    @Bean
    public MethodSecurityExpressionHandler expressionHandler(PermissionEvaluator permissionEvaluator) {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator);
        return expressionHandler;
    }
}