[Spring Security] Basic 인증 실습

WOOK JONG KIM·2022년 11월 30일
0

패캠_java&Spring

목록 보기
76/103
post-thumbnail

BasicAuthenticationFilter

기본적으로 서버에서 내려주는 로그인 page 사용 불가시 사용

  • SPA 페이지 (react, angular, vue ...)
  • 브라우저 기반의 모바일 앱(브라우저 개반의 앱, ex: inoic )

id/pw로 토큰을 만들어 올린 뒤 서버 인증이 완료되면 principal 객체를 내려주는 방식으로 주로 사용

설정 방법

public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http
              .httpBasic()
              ;
  }
}

SecurityContext 에 인증된 토큰이 없다면 아래와 같은 포멧의 토큰을 받아서 인증처리를 하고 과정 진행

http 에서는 header 에 username:password 값이 묻어서 가기 때문에 보안에 매우 취약
-> https 프로토콜에서 사용할 것을 권장

최초 로그인시에만 인증을 처리하고, 이후에는 session에 의존

RememberMe 를 설정한 경우, remember-me 쿠키가 브라우저에 저장되기 때문에 세션이 만료된 이후라도 브라우저 기반의 앱에서는 장시간 서비스를 로그인 페이지를 거치지 않고 이용 가능

에러가 나면 401 (UnAuthorized) 에러를 내려보냄


코드 예시1

@RestController
public class HomeController {

    @GetMapping("/greeting")
    public String greeting(){
        return "hello";
    }

    @PostMapping("/greeting")
    public String greeting(@RequestBody String name){
        return "hello " + name;
    }
}
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser(
                        User.withDefaultPasswordEncoder()
                                .username("user1")
                                .password("1111")
                                .roles("USER")
                                .build()
                );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // csrf disable 시
                // -> 서로 다른 보안 정책이 적용된 두개의 페이지가 서버에 공존해야되는 경우엔.. disable 할 수 없다
                .csrf().disable()
                .authorizeRequests().anyRequest().authenticated()
                .and()
                // basic filter 작동
                .httpBasic()
                ;
    }
}

test 코드 작성
-> RestTemplate : Spring에서 HTTP 통신을 RESTful 형식에 맞게 손쉬운 사용을 제공해주는 템플릿

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BasicAuthenticationTest {


    @LocalServerPort
    int port;

    RestTemplate client = new RestTemplate();

    private String greetingUrl(){
        return "http://localhost:" + port+ "/greeting";
    }
    @DisplayName("1, 인증 실패")
    @Test
    void test_1(){

        HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, ()->{
            client.getForObject(greetingUrl(), String.class);
        });

        assertEquals(401, exception.getRawStatusCode());
    }

    @DisplayName("2, 인증 성공")
    @Test
    void test_2(){
        // HttpEntity에 Header를 실어 갈려는 경우
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(
                "user1:1111".getBytes()
        ));

        HttpEntity entity = new HttpEntity(null, headers);
        ResponseEntity<String> res = client.exchange(greetingUrl(), HttpMethod.GET, entity, String.class);
        assertEquals("hello", res.getBody());
    }

    @DisplayName("3, 인증 성공")
    @Test
    void test_3(){
        // TestRestTemplate에서는 기본적으로 Basic 토큰 지원
        TestRestTemplate testClient = new TestRestTemplate("user1", "1111");
        String response = testClient.getForObject(greetingUrl(), String.class);
        assertEquals("hello", response);
    }

    @DisplayName("4, POST 인증 테스트")
    @Test
    void test_4(){
        // post는 csrf filter가 작동(disable시 오류 X)
        TestRestTemplate testClient2 = new TestRestTemplate("user1", "1111");
        ResponseEntity<String> resp = testClient2.postForEntity(greetingUrl(), "kim", String.class);
        assertEquals("hello kim", resp.getBody());
    }
}

csrf disable 시
-> 서로 다른 보안 정책이 적용된 두개의 페이지가 서버에 공존해야되는 경우엔.. disable 할 수 없다


코드 예시2

Web에서 되는 리소스가 mobile,SPA에서도 되도록 하기 위해서는 또다른 filter chain proxy를 configure를 통해 구성해야 함

Security config

// 디폴트 리퀘스트는 Security Config가 처리해야 하기에
@Order(2)
@EnableWebSecurity(debug = false)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


   private final StudentManager studentManager;
   private final TeacherManager teacherManager;

   public SecurityConfig(StudentManager studentManager, TeacherManager teacherManager) {
       this.studentManager = studentManager;
       this.teacherManager = teacherManager;
   }


   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.authenticationProvider(studentManager);
       auth.authenticationProvider(teacherManager);
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       CustomLoginFilter filter = new CustomLoginFilter(authenticationManager());
       http
               .authorizeRequests(request->
                       request.antMatchers("/", "/login").permitAll()
                       .anyRequest().authenticated()
               )
               .formLogin(
                       login->login.loginPage("/login")
                               .permitAll()
                               .defaultSuccessUrl("/", false)
                               .failureUrl("/login-error")
               )
               .addFilterAt(filter, UsernamePasswordAuthenticationFilter.class)
               .logout(logout->logout.logoutSuccessUrl("/"))
               .exceptionHandling(e->e.accessDeniedPage("/access-denied"))
               ;
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
       web.ignoring()
               .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
               ;
   }
}

mobile Security Config(또 다른 filter chain proxy 구성)

@Order(1)
@Configuration
//@EnableWebSecurity(debug = true)
//@EnableGlobalMethodSecurity(prePostEnabled = true) -> 이 두 어노테이션은 중복선언 되있기에 configuration만 선언
public class MobileSecurityConfig extends WebSecurityConfigurerAdapter {


    private final StudentManager studentManager;
    private final TeacherManager teacherManager;

    public MobileSecurityConfig(StudentManager studentManager, TeacherManager teacherManager) {
        this.studentManager = studentManager;
        this.teacherManager = teacherManager;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(studentManager);
        auth.authenticationProvider(teacherManager);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 모바일 쪽에서 날라오는 convention을 두어야 함
                // api 서비스로 호출하게 하는 것
                .antMatcher("/api/**")
                .csrf().disable()
                .authorizeRequests(request-> request.anyRequest().authenticated())
                .httpBasic();
    }
}

ApiTeacherController

@RestController
@RequestMapping("/api/teacher")
public class ApiTeacherController {

    @Autowired
    StudentManager studentManager;

    @PreAuthorize("hasAnyAuthority('ROLE_TEACHER')")
    @GetMapping("/students")
    public List<Student> studentList(@AuthenticationPrincipal Teacher teacher){
        return studentManager.myStudentList(teacher.getId());
    }
}

TeacherManager

@Component
public class TeacherManager implements AuthenticationProvider, InitializingBean {

    private HashMap<String, Teacher> teacherDB = new HashMap<>();

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        if(teacherDB.containsKey(token.getName())){
            Teacher teacher = teacherDB.get(token.getName());
            return TeacherAuthenticationToken.builder()
                    .principal(teacher)
                    .details(teacher.getUsername())
                    .authenticated(true)
                    .build();
        }
        return null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication == UsernamePasswordAuthenticationToken.class;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Set.of(
                new Teacher("choi", "최선생", Set.of(new SimpleGrantedAuthority("ROLE_TEACHER")), null)
        ).forEach(s->
            teacherDB.put(s.getId(), s)
        );
    }
}

Debug를 통해 BasicAuthenticationFilter를 들여다 보면

BasicAuthenticationFilter

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		try {
			UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
            
            ....
            
            
            if (authenticationIsRequired(username)) {
				Authentication authResult = this.authenticationManager.authenticate(authRequest);
                
                ...
                
}

Basic 토큰 형태의 Request를 UserName 형태의 토큰으로 convert 한후에

authenticationManager.authenticate를 통해 Provider 중 TeacherManager가 인증을 해주는 방식

-> 처음엔 StudentManager가 검증을 시작하지만 return 값이 null이여서 이후 TeacherManager로 인증 권한을 넘김

test 코드

 @DisplayName("1. 학생 조사")
    @Test
    void test_1() throws JsonProcessingException{
        String url = format("http://localhost:%d/api/teacher/students", port);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(
                "choi:1".getBytes() // BasicAuthentication을 통한 인증
        ));
        HttpEntity<String> entity = new HttpEntity<>("", httpHeaders);
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);

        List<Student> list = new ObjectMapper().readValue(response.getBody(),
                new TypeReference<List<Student>>() {
                });
        System.out.println(list);
        assertEquals(3, list.size());
    }
profile
Journey for Backend Developer

0개의 댓글