개발자의 오르막

[SpringSecurity #14] 스프링 메소드 시큐리티 본문

SpringFrameWork/SpringSecurity

[SpringSecurity #14] 스프링 메소드 시큐리티

계단 2020. 10. 5. 18:50

# 메소드 시큐리티

- @EnableGlobalMethodSecurity

  @EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true, securedEnabled = true)

 

- @Secured 와 @RollAllowed

  메소드 호출 이전에 권한을 확인한다.

  스프링 EL을 사용하지 못한다.

 

- @PreAuthorize 와 @PostAuthorize

  메소드 호출 이전 이후에 권한을 확인할 수 있다.

  스프링 EL을 사용하여 메소드 매개변수와 리턴값을 검증할 수 있다.

 

 

- 메소드를 활용하여 시큐리티 메소드 사용하기

 

1. 먼저 MethodSecurity 를 사용하기 위해 config 패키지에 클래스를 아래와 같이 생성하고 선언한다.

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class MethodSecurity {

}

  securedEnabled = true 로 설정하면 @Secured 어노테이션 사용 가능

 

 

2. 사용하고자하는 메소드 위에 @Secured 라는 어노테이션으로 설정해준다.

    @Secured("ROLE_USER")
    public void dashboard() {

        // SecurityContextHolder 를 통해서 로그인 한 객체를 어디서든지 사용할 수 있음.
        // UsernamePasswordAuthenticationToken 타입
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // principal 은 로그인한 객체로 User 타입 ( UserDetails 타입 안의 User )
        Object principal = authentication.getPrincipal();

        UserDetails userDetails = (UserDetails) authentication.getPrincipal();

        System.out.println("===============================");
        System.out.println(userDetails.getUsername());

        // 로그인한 principal 의 권한 ( Roles )
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        // 인증을 한 후에는 가지고 있지 않음
        Object credentials = authentication.getCredentials();

        // 인증된 사용자인지 여부
        boolean authenticated = authentication.isAuthenticated();
    }

  @Secured, @RolesAllowed, @PreAuthorize("hasRole('USER')") 는 메소드를 호출하기 전에 권한 검사

  @PostAuthorize("hasRole('USER')") 는 메소드를 호출한 이후에 사용 가능하다.

 

 

3. 시큐리티 config 파일에 AuthenticationManager 를 빈으로 등록해준다.

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

 

4. @Secured 가 선언된 해당 메소드를 테스트 하는 테스트 파일을 만들어준다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleServiceTest {

    @Autowired
    SampleService sampleService;

    @Autowired
    AccountService accountService;

    @Autowired
    AuthenticationManager authenticationManager;

    @Test
    public void dashboard() {
        // 계정을 생성한다.
        Account account = new Account();
        account.setRole("USER");
        account.setUsername("jake");
        account.setPassword("123");
        accountService.createNew(account);

        // 생성한 계정을 UserDetails 타입으로 불러온다.
        UserDetails userDetails = accountService.loadUserByUsername("jake");

        // 인증할 수 있는 UserDetails 의 토큰을 발행한다.
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails, "123");

        // 발행된 토큰을 통해 인증한다.
        Authentication authentication = authenticationManager.authenticate(token);

        // 다른 필터에서 사용할 수 있도록 SecurityContextHolder 에 등록한다.
        SecurityContextHolder.getContext().setAuthentication(authentication);

        sampleService.dashboard();
    }
}

 

5. Hierarchy 를 설정하기 위해서는 아래와 같이 상속받아 메소드를 재정의하여 설정해준다.

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class MethodSecurity extends GlobalMethodSecurityConfiguration {

    @Override
    protected AccessDecisionManager accessDecisionManager() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        AffirmativeBased accessDecisionManager = (AffirmativeBased) super.accessDecisionManager();
        accessDecisionManager.getDecisionVoters().add(new RoleHierarchyVoter(roleHierarchy));
        return accessDecisionManager;
    }
}

  또는 @Secured("ROLE_USER", "ROLE_ADMIN") 으로 정의해줘도 된다.

 

 


# AuthenticationPrincipal

  우리는 도메인 Account 를 UserDeatails 로 변환하는데, 아래처럼 Build 를 해줬다.

        User.builder()
                .username(account.getUsername())
                .password(account.getPassword())
                .roles(account.getRole())
                .build()

  이를 줄이기 위해 UserAccount 라는 클래스를 생성하여 User 를 상속받아 사용한다.

public class UserAccount extends User {

    private Account account;

    public UserAccount(Account account) {
        super(account.getUsername(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_" + account.getRole())));
    }

    public Account getAccount() {
        return account;
    }
}

 

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findAccountByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException(username);
        }

//        User.builder()
//                .username(account.getUsername())
//                .password(account.getPassword())
//                .roles(account.getRole())
//                .build()
        return new UserAccount(account);
    }

 

  그러면 Account 도메인에 User 라는 클래스를 상속받지 않아도 UserAccount 라는 클래스를 통해

  Convert 가능하다.

 

- 기존 컨트롤러

@GetMapping("/")
    public String index(Model model, Principal principal) {
        if (principal == null) {
            model.addAttribute("message", "Hello Spring Security");
        } else {
            model.addAttribute("message", "Hello" + principal.getName());
        }

        return "index";
    }

 

- UserAccount 사용한 컨트롤러

@GetMapping("/")
    public String index(Model model, @AuthenticationPrincipal UserAccount userAccount) {
        if (userAccount == null) {
            model.addAttribute("message", "Hello Spring Security");
        } else {

            model.addAttribute("message", "Hello" + userAccount.getUsername());
        }

        return "index";
    }

- Account 도메인 사용한 컨트롤러

@GetMapping("/")
    public String index(Model model, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account") Account account) {
        if (account == null) {
            model.addAttribute("message", "Hello Spring Security");
        } else {

            model.addAttribute("message", "Hello" + account.getUsername());
        }

        return "index";
    }

  

  위의 코드가 너무 길기 때문에 이를 어노테이션으로 등록하여 사용할 수 있다.

 

- CurrentUser 인터페이션 등록

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
public @interface CurrentUser {
}

 

- 리펙토링 한 컨트롤러

@GetMapping("/")
    public String index(Model model, @CurrentUser Account account) {
        if (account == null) {
            model.addAttribute("message", "Hello Spring Security");
        } else {

            model.addAttribute("message", "Hello" + account.getUsername());
        }

        return "index";
    }

 

 

 

Comments