[REST API 보안 적용]_05_스프링 시큐리티 폼 인증 설정

2 minute read

스프링 시큐리티 폼 인증 설정

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .anonymous()
    .and()
    .formLogin()
    .and()
    .authorizeRequests()
    .mvcMatchers(HttpMethod.GET, "/api/**").authenticated()
    .anyRequest().authenticated();
}
  • 익명 사용자 사용 활성화

  • 폼 인증 방식 활성화

    • 스프링 시큐리티가 기본 로그인 페이지 제공
  • 요청에 인증 적용

    • /api 이하 모든 GET 요청에 인증이 필요함.

      (permitAll()을 사용하여 인증이 필요없이 익명으로 접근이 가능케 할 수 있음)

    • 그밖에 모은 요청도 인증이 필요함.


스프링 시큐리티 HTTP 설정

http를 제공하는 configure메서드를 재정의하면 얼마든지 스프링 시큐리티 설정이 가능하다.

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .anonymous() // 익명사용자 가능
    .and()
    .formLogin() // 폼인증 사용 (기본 폼인증 페이지 제공함)
    .and()
    .authorizeRequests()
    .mvcMatchers(HttpMethod.GET, "/api/**").authenticated()
    .anyRequest().authenticated(); // 나머지 요청은 인증이 필요
}

PasswordEncoder 설정하기

Account 만들 때 패스워드 인코더를 쓰도록 해야한다. 지금은 PasswordEncoder를 사용하지않고 Repository에 바로 저장하고 있다. 스프링 시큐리티는 PasswordEncoder를 사용하고 있기 때문에 Match되지 않는다.

@Service
public class AccountService implements UserDetailsService {

    @Autowired
    AccountRepository accountRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    public Account saveAccount(Account account) {
        account.setPassword(this.passwordEncoder.encode(account.getPassword()));
        return this.accountRepository.save(account);
    }
  //..
}

테스트코드 수정

PasswordEncoder를 적용한 상태로 값이 매칭되는지 확인

기본 계정 생성

앱이 구동될 때, 기본 계정하나를 자동으로 만들어준다.

@Configuration
public class AppConfig {
	// ...
    @Bean
    public ApplicationRunner applicationRunner() {
        return new ApplicationRunner() {

            @Autowired
            AccountService accountService;

            @Override
            public void run(ApplicationArguments args) throws Exception {
                Account solar = Account.builder()
                        .email("solar@email.com")
                        .password("1234")
                        .roles(Set.of(AccountRole.ADMIN, AccountRole.USER))
                        .build();
                accountService.saveAccount(solar);
            }
        };
    }
}

※ Java 8

Account admin = Account.builder().email(appProperties.getAdminUsername())
 			.password(appProperties.getAdminPassword())
			.roles(Stream.of(AccountRole.ADMIN, AccountRole.USER)
             .collect(Collectors.toSet()))
  		.build();

참고- java initialize hashset


질문

강의를 학습 후 추가적으로 spring docs를 보면서 공부하고 있는데요. anonymous 메소드 설명을 읽고 익명 사용자를 서비스 내에서 어떻게 표현할지 설정하는 방법이라고 이해했습니다.

그래서 간단한 실험을 위해 http.anonymous().authorities(“ROLE_USER”); 이런식으로 설정하면 인증을 하지 않아도 contextholer에 principal 객체가 담기기를 희망했습니다.

@Override
public void configure(HttpSecurity http) throws Exception {
    http.anonymous().authorities("ROLE_USER");
    http.authorizeRequests()
            .mvcMatchers("/", "/info", "/account/**", "/signup").permitAll()
            .mvcMatchers("/admin").hasRole("ADMIN")
            .mvcMatchers("/user").hasRole("USER")
            .anyRequest().authenticated()
            .accessDecisionManager(accessDecisionManager());
    http.formLogin();
    http.httpBasic();

    http.logout().logoutSuccessUrl("/");

    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}

하지만, AnonymousAuthenticationFilter가 인증 객체를 담아줄 것이라는 예상과 달리 principal 객체는 담겨있지 않더군요 ㅜㅜ. 해당 메소드는 어떻게 사용하는 걸까요?

principal 객체가 담겨있지 않은지 어떻게 확인해보셨는지요? 익명 필터는 문서에 적혀있는대로 Authenicaion이 없는 겨웅에 anonymousUser라는 문자열로 principal을 만들어 줍니다. 지금 하신 설정은 그 principal이 가지고 있을 기본 권한을 설정하신거구요.

질문할 때, Authorities와 principal이 같은 역할을 하는지 알았는데 공부하면서 권한과 이름으로 차이가 있다는 것을 알게 되었습니다. 더 공부해서 어느 정도 스스로 답을 찾기는 했습니다. 혹시 제가 이해한게 맞는지 한번 확인해주시면 감사하겠습니다.

제가 원하는 상황은 아래 처럼 Principal 객체를 메소드 파라미터로 설정했을 때, document에 있는 것처럼 해당 객체가 anoymousUser를 담고있는 상황을 기대했는데 null 객체가 담겨있더라고요.

@GetMapping("/user")
public String user(Model model, Principal principal) { // principal 객체 null
    model.addAttribute("message", "Hello Admin, " + principal.getName());
    return "user";
}

@GetMapping("/async-handler")
@ResponseBody
public Callable<String> asyncHandler() {
    SecurityLogger.log("MVC");
    return () -> {
        SecurityLogger.log("Callable");
        return "Async Handler";
    };
}

stackoverflow와 document를 읽어보니 궁금증을 해결할 수 있었습니다.SecurityContextHolder.getContext().getAuthentication() 메소드를 통해서는 인증없이 Authentication객체에 잘 접근할 수 있었지만 Servlet API(Controller 메소드)를 통해 호출한 API는 동작을 하지 않는것 같드라고요.

// Authentication 객체 확인 가능
public static void log(String message) {
    System.out.println(message);
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();  // anonymousUser
    System.out.println("Thread: " + Thread.currentThread().getName());
    System.out.println("Principal: " + principal);
}

이 질문은 spring security 강의에 하는게 맞는것 같은데 해당 강의에서는 아직(제가 수강한 진도 기준) anonymous 내용을 다루지 않는것 같아서 여기에 질문하게 되었네요…

reference

https://stackoverflow.com/questions/51395906/understanding-the-difference-of-permitall-and-anonymous-in-spring-security https://docs.spring.io/spring-security/site/docs/current/reference/html5/#anonymous

(10.14.1 Overview 두번째 문단)

Calls to servlet API calls such as getCallerPrincipal, for example, will still return null even though there is actually an anonymous authentication object in the SecurityContextHolder.

네 문서와 코드로 확인한 내용이 맞습니다. 익명으로 인증된 경우에 분명히 “anonymouse”라는 principal이 존재하긴하지만 요청 매개변수에 null이 들어옵니다. 익명인증 객체는 인증된 사용자가 없는거나 마찬가지로 취급하기 때문에 null로 처리하는게 타당하다 결정한 것 같습니다. 저도 그게 자연스럽다고 생각합니다. 오히려 anonymous인데 Principal이 null이 아니면 이건 진짜로 인증된 사용자인지 아닌지 다시 확인할 방법이 필요해 지겠죠. null체크가 아니라.