> Hello World !!!

     

@syaku

spring boot security 스프링 부트 시큐리티 설정과 사용자 인증 #1

스프링 부트 시큐리티 설정과 사용자 인증 #1

Github: https://github.com/syakuis/syaku-blog

모든글


이번 포스팅은 스프링 시큐리티를 사용하여 기존에 개발된 블로그에 보안적인 요소를 적용한다. 보안적인 요소는 사용자 인증, 허가 그리고 접근 제어가 있다.

그리고 설명하면서 이전 포스팅과 중복되는 CRUD Service, Repository 등등은 구현 및 테스트 등등은 필요에 따라 생략한다. 모든 소스 코드는 Github 에서 참고할 수 있다.

스프링 시큐리티는 필터의 조합이며 필터를 잘 활용하면 스프링 시큐리티를 잘 다룰수 있게된다. 그리고 기존에 개발된 블로그 프로그램을 수정하지 않고 혹은 보안적인 코드를 삽입하지 않고 보안을 구현할 수 있도록 작업한다.

스프링 시큐리티를 사용하기 위해 gradle 라이브러리 의존성을 추가한다

compile "org.springframework.boot:spring-boot-starter-security"
testCompile "org.springframework.security:spring-security-test"

스프링 부트 스타터용 시큐리티를 추가하였다. 버전은 5.0.8.RELEASE 가 사용했다.

다음은 스프링 시큐리티 설정을 구성하기 위해 클래스를 생성한다.

흔히 사용하는 form 로그인 방식으로 처리하나 세션을 사용하지 않는 다. 왜냐하면 블로그 만들기에서는 뷰페이지 UI 를 서버사이드 스크립트나 백엔드 템플릿 엔진을 사용하지 않고 클라이언트용 자바스크립트를 사용하기 때문에 세션을 유지할 수 없다. 유지할 수 있더라도 권장되는 방식이 아니다.

그래서 BasicAuthentication 방식을 사용한다. 본 인증 방식은 계정과 암호를 암호화하여 요청 헤더에 넣어 서버에 데이터를 요청할때마다 함께 전송하는 방식이다. 하지만 BasicAuthentication 방식은 안전하지 않기때문에 절때 운영서버에 사용해서는 안된다. 그리고 현재 포스팅에 내용에서 우선이 될 부분이 아니기 때문에 임시적으로 BasicAuthentication 방식을 사용하고 추후에 안전한 인증 방식으로 리팩토링 할 것이다.

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

private BasicAuthenticationFilter basicAuthenticationFilter(
     AuthenticationManager authenticationManager) {
     return new BasicAuthenticationFilter(authenticationManager, authenticationEntryPoint());
  }

당장 사용자 정보를 관리하는 기능이 없기때문에 스프링 시큐리티에서 제공되는 InMemoryUserDetailsManager 를 이용하여 admin 사용자를 추가하였다. userDetailsService 빈을 생성하면 스프링 시큐리트에서 해당 정보를 사용할 수 있게된다.

  @Bean
 public UserDetailsService userDetailsService() {
   InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
   manager.createUser(User.withDefaultPasswordEncoder().username("admin").password("1234").roles("USER").build());
   return manager;
}

WebSecurityConfigurerAdapter 는 스프링 시큐리티에서 기본적으로 제공하는 보안 설정으로 구성된 클래스 구현체이다. 이를 상속받아 필요한 부분만 수정하면 된다. 그래서 접근 권한이나 로그인 처리방식 등등에 대해 어떻게 처리할건지 결정하여 configure 기능에 구현하면 된다.

    @Override
   protected void configure(HttpSecurity http) throws Exception {
     http
      .authorizeRequests()
      .antMatchers(HttpMethod.POST, "/post").hasRole("USER")
      .anyRequest()
      .authenticated()
      .and()
      .addFilterAt(basicAuthenticationFilter(authenticationManagerBean()), BasicAuthenticationFilter.class)
      .csrf();
  }

/post 경로에 POST 방식으로 접근할 경우 ROLE_USER 을 가진 사용자만 접근할 수 있게 설정하였다. 그외 요청은 인증된 사용자만 접근할 수 있게 설정하였고 CSRF 보안인증을 적용하였다.

그리고 스프링 시큐리티에서 사용자 역활에 ROLE_ 이 없는 경우 자동으로 앞에 붙여준다. 이는 기본 원칙이고 ROLE_ 이 싫을 경우 직접 설정을 수정할 수 있다. 하지만 그냥 써도 상관없으니 구지 변경하지 않는 다.

이제 테스트를 실행하면 PostControllerTest 클래스에서 쓰기 테스트에서 403 오류가 발생한다. 로그인 처리를 위한 BasicAuthentication 요구사항은 아래와 같다.

header name: Authorization
header value: Basic base64(username:password)

아래와 같이 테스트를 수정한다.


 String authorization = "Authorization";
 String username = "admin";
 String password = "1234";
 String token;

 @Before
 public void setup() {
   assertNotNull(postService);
   objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
   objectMapper.registerModule(new JavaTimeModule());

   postService.save(PostEntity.builder().subject("제목").contents("내용").build());

   String token = username + ":" + password;
   this.token = "Basic " + Base64.getEncoder().encodeToString(token.getBytes());
}

 @Test
 public void 쓰기() throws Exception {
   this.mvc.perform(
     post("/post")
      .header(authorization, this.token)
      .with(csrf())
      .content(objectMapper.writeValueAsString(
         PostEntity.builder().subject("쓰기_제목").contents("쓰기_내용").build()))
      .contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE))
  )
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE)))
    .andExpect(jsonPath("$.subject").value("쓰기_제목"))
    .andExpect(jsonPath("$.contents").value("쓰기_내용"));
}

테스트를 실행하면 쓰기를 제외한 기능에 접근하면 오류가 발생한다.

현재 프로그램은 스프링 시큐리티 설정에 맞게 구성되어 있지 않다. 그래서 스프링 시큐리티에 맞게 기존 프로그램을 모두 수정한다는 것은 문제가 있고 스프링 시큐리티 설정도 최종적으로 정의된 설정도 아니다. 그리고 앞으로 테스트 작성하려면 매번 시큐리티 설정에 문제가 되지 않도록 설정을 해줘야하는 것도 문제다. 그래서 일단 기존에 개발된 프로그램에 영향을 주지 않도록 스프링 시큐리티를 설정을 수정한다. 전체 소스는 아래와 같다.

< / > WebSeucirytConfig

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
 @Bean
 public UserDetailsService userDetailsService() {
   InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
   manager.createUser(User.withDefaultPasswordEncoder().username("admin").password("1234").roles("USER").build());
   return manager;
}

 @Configuration
 static class Security extends WebSecurityConfigurerAdapter {
   private SecurityProperties securityProperties;
   
   @Autowired
   public void setSecurityProperties(SecurityProperties securityProperties) {
     this.securityProperties = securityProperties;
  }
   
   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception {
     return super.authenticationManagerBean();
  }
   
   private AuthenticationEntryPoint authenticationEntryPoint() {
     return new BasicAuthenticationEntryPoint();
  }

   private BasicAuthenticationFilter basicAuthenticationFilter(
     AuthenticationManager authenticationManager) {
     return new BasicAuthenticationFilter(authenticationManager, authenticationEntryPoint());
  }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
     if (securityProperties.isEnableCsrf()) {
       http.csrf();
    } else {
       http.csrf().disable();
    }

     http
      .authorizeRequests()
      .antMatchers(HttpMethod.GET, "/post/**").permitAll()
      .antMatchers(HttpMethod.POST, "/post/*").hasRole("USER")
      .antMatchers(HttpMethod.DELETE, "/post/*").hasRole("USER")
      .anyRequest().authenticated()
      .and()
      .addFilterAt(basicAuthenticationFilter(authenticationManagerBean()), BasicAuthenticationFilter.class);
  }
}
}

포스트 쓰기와 삭제에 USER 권한이 필요하고 나머지는 모두 접근할 수 있도록 접근 권한을 수정했다. 그리고 CSRF 를 사용하면 테스트에 항상 .with(csrf()) 헤더에 포함해야한다. 이것도 필요한 테스트에만 사용할 수 있도록 동적으로 제어한다. 그래서 spring properties 를 사용하여 구현하였다. 아래와 같이하여 사용할 수 있다.

< / > src/main/resources/org/syaku/blog/config/security.properties

blog.security.enableCsrf=true

프로퍼티 파일을 생성하고 클래스 파일을 만든다.

package org.syaku.blog.security.config;

@Configuration
@ConfigurationProperties(prefix = "blog.security")
@PropertySource("classpath:org/syaku/blog/config/security.properties")
@Data
public class SecurityProperties {
 private boolean enableCsrf;
}

스프링 프로퍼티를 사용하기 위해 아래의 의존성을 추가해야 한다.

compileOnly "org.springframework.boot:spring-boot-configuration-processor"

항상 csrf 를 켜져있지만 테스트 구동시에는 아래와 같이 끄고 사용한다.

< / > src/test/resources/application.properties

blog.security.enableCsrf=false

spring.datasource.driver-class-name=org.h2.Driver

spring.jpa.hibernate.use-new-id-generator-mappings=true
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

logging.level.org.hibernate=info
#logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql=trace
logging.level.org.springframework.security=debug
logging.level.org.syaku=debug

맨 첫줄이 csrf 설정이다. 그리고 driver-class-name 은 꼭 설정해야 한다. 나머지는 JPA 와 블로그 프로그램 로그를 출력하기 위한 설정이다.logging.level.org.hibernate.SQL 이 설정과 spring.jpa.properties.hibernate.show_sql 이 설정은 같은 것이다. 둘중 하나만 하면 된다. 후자를 선택한 건 쿼리 로그를 보기 좋게 정렬해주기 때문이다.

그리고 스프링 프로퍼티는 아래의 순서로 읽어지며 같은 키는 이후에 읽어지는 값으로 대처한다. 이를 잘 응용하면 다양한 서버 환경에서 필요한 값을 동적으로 사용할 수 있도록 구현할 수 있다.

@PropertySource
application.properties
application-{profile}.properties

프로퍼티를 읽기 위한 다른 방식이 더 있지만 나는 위와 같은 조건만 사용한다.

참고: https://docs.spring.io/spring-boot/docs/2.0.6.RELEASE/reference/htmlsingle/#boot-features-external-config-application-property-files

각 패키지에 개발적으로 @PropertySource 를 사용하여 프로퍼티를 관리하고 이를 제어하기 위한 기본 프로퍼티 application.properties를 사용하고 마지막으로 제어할 프로퍼티를 프로파일 {profile} 을 설정하여 사용한다.

yaml 을 사용하지 않는 이유가 @PropertySource 를 사용할 수 없어서 이다.

이제 아래와 같이 테스트를 수정하고 실행한다.

... skip ...
@WithMockUser(username = "admin", roles = "USER")
public class PostControllerTest {

... skip ...

 @Before
 public void setup() {
   assertNotNull(postService);
   objectMapper = new ObjectMapper();
   objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
   objectMapper.registerModule(new JavaTimeModule());
   postService.save(PostEntity.builder().subject("제목").contents("내용").build());
}

 @Test
 public void 쓰기() throws Exception {
   this.mvc.perform(
     post("/post")
      .content(objectMapper.writeValueAsString(
         PostEntity.builder().subject("쓰기_제목").contents("쓰기_내용").build()))
      .contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE))
  )
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE)))
    .andExpect(jsonPath("$.subject").value("쓰기_제목"))
    .andExpect(jsonPath("$.contents").value("쓰기_내용"));
}

... skip ...

 @Test
 public void 삭제() throws Exception {
   assertNotNull(postService.getPost(1));
   this.mvc.perform(delete("/post/{id}", 1)).andExpect(status().isOk());

   assertNull(postService.getPost(1));
}
}

@WithMockUser(username = "admin", roles = "USER") 를 이용하여 허가된 사용자 인증을 만들 수 있다. 성공하면 다음 작업으로 넘어간다.

블로그 사용자를 매번 정적으로 스프링 시큐리티 설정에 등록할 수 없기에 사용자를 추가하고 삭제할 수 있는 기능을 개발한다. 즉 회원가입 기능을 구현하고 스프링 시큐리티가 회원가입 정보를 바라보도록 작업한다.

사용자 엔티티와 테스트를 작성한다. 설명하지 않고 생략된 소스 코드를 github 저장소에서 확인한다.

< / > UserEntity.java

package org.syaku.blog.user.domain;

@Entity
@Table(name = "USER")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class UserEntity {
 @Id
 @Column(nullable = false)
 @SequenceGenerator(
   name = "USER_ID_GEN",
   sequenceName = "USER_ID_SEQ",
   allocationSize = 1)
 @GeneratedValue(generator = "USER_ID_GEN", strategy = GenerationType.SEQUENCE)
 private Long id;

 @Column(nullable = false, unique = true)
 private String username;

 @Column(nullable = false)
 private String password;

 @Column(nullable = false, unique = true)
 private String email;

 @Column(nullable = false)
 @Temporal(TemporalType.TIMESTAMP)
 @CreationTimestamp
 private Date creationDateTime;
}

< / > UserServiceTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class UserServiceTest {
 @Autowired
 private UserService userService;

 @After
 @Rollback
 public void exit() {

}
 
 @Test
 public void 사용자등록() {
   UserEntity userEntity = UserEntity.builder()
    .username("admin").password("1234").email("syaku@naver.com").build();
   userService.signup(userEntity);

   assertEquals(userEntity, userService.getUserByUsername("admin"));
   assertSame(userEntity, userService.getUserByUsername("admin"));
}
}

@After 은 기능 테스트 후 작업을 실행할 수 있다. 해당 클래스만 테스트를 실행하면 문제가 없으나 여러 테스트 클래스를 실행할 경우 해당 데이터가 유지 되므로 문제가 될 수 있다. 상황에 맞게 롤백을 해줘야 한다. 다른 테스트에서 admin 생성을 할 수 있고 해당 테스트는 다른 테스트와 연계되지 않아 데이터를 롤백했다.

테스트에 필요한 리포지토리와 서비스 클래스를 생성한다.

public interface UserRepository extends Repository<UserEntity, Long> {
 UserEntity findOneByUsername(String username);
 UserEntity save(UserEntity userEntity);
}

@Service
@Transactional
public class UserService {
 private UserRepository userRepository;

 @Autowired
 public void setUserRepository(UserRepository userRepository) {
   this.userRepository = userRepository;
}

 public UserEntity saveUser(UserEntity userEntity) {
   return userRepository.save(userEntity);
}

 public UserEntity getUserByUsername(String username) {
   return userRepository.findOneByUsername(username);
}
}

테스트를 실행한다.

생성 날짜 값이 틀려 오류가 발생한다. 참고: 날짜 형식 부분은 여기서 먼저 작성되었고 이전 포스팅에 반영하였다.

Expected :UserEntity(... creationDateTime=Wed Oct 24 10:21:40 KST 2018)
Actual   :UserEntity(... creationDateTime=2018-10-24 10:21:40.411)

왜 날짜 포맷이 서로 다른가? creationDateTime 이 null 인 경우 자동으로 현재 시간으로 설정한다. 기본적으로 java.util.Date 를 사용하기 때문에 이를 toString 으로 출력하면 해당 클래스에 설명된 것처럼 EEE MMM dd HH:mm:ss zzz yyyy 포맷으로 출력된다. 그리고 데이터베이스에 저장될때 바인딩되는 타입은 java.sql.Timestamp 타입으로 저장되게 선언했다. (@Temporal(TemporalType.TIMESTAMP)) 해당 클래스의 포맷은 yyyy-mm-dd hh:mm:ss.fffffffff 이므로 결과는 오류가 발생되는 게 맞다. 아래의 방법은 자바8 이상에만 사용할 수 있다.

@Column(nullable = false)
@CreationTimestamp
private Timestamp creationDateTime;

같은 username 을 두번 저장하면 오류가 발생하는 지 테스트한다.

    @Before
 public void setup() {
   userEntity = userService.saveUser(UserEntity.builder()
    .username("test").password("1234").email("test@naver.com").build());
}

@Test(expected = DataIntegrityViolationException.class)
 public void 사용자중복검사() {
   userService.saveUser(UserEntity.builder()
    .username("test").password("1234").email("test@naver.com").build());
}

expected 속성을 사용하여 해당 예외가 발생해야 테스트는 성공한다. 하지만 테스트를 실행하면 실패한다. setup 기능에 이미 test 계정을 등록하고 또 테스트에서 등록했는 데 실패한다. 로그를 확인해보면 인서트된 쿼리 로그가 전혀 없다.

이유는 JPA 는 사용하지 않는 데이터를 구지 작업을 하지 않는 다. 이를 지연로딩(Lazy Loading) 이라고한다. 즉 saveUser 는 실행했으나 이 데이터를 실제 사용하지 않기때문에 작업을 하지 않았다.

그래서 아래와 같이 강제로 실제 반영되도록 flush 해주었다.

public class UserServiceTest {
 @Autowired
 private EntityManager entityManager;

... skip ...

 @Test(expected = PersistenceException.class)
 public void 사용자중복검사() {
   userService.saveUser(UserEntity.builder()
    .username("test").password("1234").email("test@naver.com").build());

   entityManager.flush();
}

이어서 사용자 수정과 사용자 삭제 테스트를 작성하였다. 직접해보고 필요하다면 소스 코드를 참고하도록한다. 가끔 전체 테스트를 실행해보는 것이 좋다.

이제 스프링시큐리티의 사용자 인증을 위한 서비스를 구현한다. 구현하기 위한 인터페이스는 UserDetailsService 이며 이를 상속받아 구현한다.

package org.syaku.blog.user.service;

@Service("userDetailsService")
@Transactional(readOnly = true)
public class BlogUserDetailsService implements UserDetailsService {
 private UserRepository userRepository;

 @Autowired
 public void setUserRepository(UserRepository userRepository) {
   this.userRepository = userRepository;
}

 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   UserEntity userEntity = userRepository.findOneByUsername(username);
   if (userEntity == null) {
     throw new UsernameNotFoundException("사용자를 찾을 수 없다.");
  }

   return new User(
     userEntity.getUsername(), userEntity.getPassword(), List.of(new SimpleGrantedAuthority("USER")));
}
}

해당 클래스를 빈으로 생성하고 스프링 시큐리티에 주입해주면 직접 사용자를 입력하지 않고 데이터베이스에 저장된 정보를 사용하게 된다. 기본 권한은 USER 로 설정하였다.

해당 빈을 스프링 시큐리티에 주입한다. 기존에 있던 userDetailsService() 제거했다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
... skip ...
}

이제 테스트를 작성한다.

package org.syaku.blog.user.web;


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
 @Autowired
 private UserService userService;

 @Autowired
 private MockMvc mvc;
 private ObjectMapper objectMapper;

 @Before
 public void setup() {
   objectMapper = new ObjectMapper();
   userService.saveUser(UserEntity.builder()
    .username("admin").password("1234").email("syaku@naver.com").build());
}

 @After
 public void exit() {
   userService.deleteUserByUsername("admin");
}

 private String getToken(String username) {
   UserEntity userEntity = userService.getUserByUsername(username);
   String token = userEntity.getUsername() + ":" + userEntity.getPassword();
   return "Basic " + Base64.getEncoder().encodeToString(token.getBytes());
}

 @Test
 public void 사용자인증허가() throws Exception {
   this.mvc.perform(
     get("/user")
      .header("Authorization", getToken("admin"))
      .contentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_UTF8_VALUE))
  )
    .andExpect(status().isOk()).andExpect(jsonPath("$.username").value("admin"));
}
}

테스트를 실행하면 There is no PasswordEncoder mapped for the id "null" 오류가 발생한다. PasswordEncoder 는 스프링 시큐리티의 비밀번호 암호화 설정은 필수이다. 지금은 테스트 개발과정이니 암호화를 사용하지 않도록 설정한다. 실제 운영에서는 절대적으로 암호화를 사용해야 한다. 데이터베이스가 유출될 경우 비밀번호를 1차적으로 보호할 수 있기 때문이다. 아래의 설정을 추가한다.

  @Bean
 public static PasswordEncoder passwordEncoder() {
   return NoOpPasswordEncoder.getInstance();
}

그리고 5.x 부터 암호화 설정 방식을 변경하였다. 다양한 암호화 방식을 실시간으로 반영하기 위해 기존에 암호화된 값을 새로운 암호화 방식으로 변경하는 것이 쉽지 않았다. 그리고 sha 알고리즘deprecated 되었다.

그래서 이부분을 보완하기 위해 아래와 같이 변경되었다.

참고 : https://docs.spring.io/spring-security/site/docs/5.1.1.RELEASE/reference/htmlsingle/#pe-dpe

  @Bean
 public static PasswordEncoder passwordEncoder() {
   return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

직접 여러 암호화 알고리즘을 설정할 수 있고 위 처럼 스프링 시큐리티에서 제공하는 묶음을 사용할 수 있다. 어떤식으로 동작하는 지 아래와 같은 테스트를 작성했다.

  @Autowired
 private PasswordEncoder passwordEncoder;

 @Test
 public void 암호화테스트() {
   assertEquals(passwordEncoder.matches("1234", "{noop}1234"), true);
   assertEquals(passwordEncoder.matches("1234", "{bcrypt}1234"), false);
}

이제 스프링 시큐리티에서 로그인 처리를 위의 방식으로 처리되도록 적용해야한다. 하지만 우린 스프링 시큐리티를 구현하는 상황이고 아직 많은 작업들이 남아 있기때문에 암호화를 사용하지 않고 작업이 완료되면 해당 부분을 리팩토링하도록한다. 지금은 아래와 같이 설정하고 사용한다.

  @Bean
 public static PasswordEncoder passwordEncoder() {
   return NoOpPasswordEncoder.getInstance();
}

모든 테스트를 실행한다.

지금까지는 사용자 인증을 거치지 않고 사용자 인증을 허가받은 것처럼 사용자 토큰을 사용하여 데이터를 요청하였다. 하지만 실제로 사용자 토큰은 로그인을 과정을 거쳐야만 얻을 수 있다. 그래서 이부분을 지금부터 구현해본다.

스프링 시큐리티 필터중에 UsernamePasswordAuthenticationFilter 필터를 사용하여 이부분을 구현할 수 있다. 해당 필터는 특정한 경로와 기능을 요청할 경우 필터에 의해 우선 읽어지면 로그인 처리 요청을 진행하게 된다. 이때 사용자 계정과 암호가 맞는 지를 판단해여 결과를 반환한다. 맞지 않은 경우 인증 예외가 발생한다.

< / > WebSecurityConfig.java

public class WebSecurityConfig {

 @Configuration
 static class Security extends WebSecurityConfigurerAdapter {
   @Autowired
   private SecurityProperties securityProperties;
   
   private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter(
     AuthenticationManager authenticationManager) {
     UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();
     filter.setUsernameParameter(securityProperties.getUsernameParameter());
     filter.setPasswordParameter(securityProperties.getPasswordParameter());
     filter.setFilterProcessesUrl(securityProperties.getLoginProcessingUrl());
     filter.setAuthenticationManager(authenticationManager);
     return filter;
  }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
     
     http
       
      .addFilterAt(usernamePasswordAuthenticationFilter(authenticationManagerBean()),
         UsernamePasswordAuthenticationFilter.class);
  }
}
}

UsernamePasswordAuthenticationFilter 를 추가하였다. 클래스 구현체에 몇가지 설정이 있는 데 변경이 쉽도록 프로퍼티를 이용하였다.

blog.security.usernameParameter=username
blog.security.passwordParameter=password
blog.security.loginProcessingUrl=/login

필터가 요청 값을 인지할 수 있도록 파라메터 명과 로그인 처리 경로로 해당 요청이 로그인 처리라는 것을 판단할 수 있는 설정이다. 그외 설정도 있으니 참고한다. 마지막으로 테스트를 추가하고 실행한다.

< / > UserControllerTest.java

  @Test
 public void 사용자인증() throws Exception {
   this.mvc.perform(post("/login")
    .param("username", "admin")
    .param("password", "1234")
    .contentType(MediaType.APPLICATION_FORM_URLENCODED)).andExpect(redirectedUrl("/"));
}

로그인 처리를 성공하면 원하는 경로로 리다이렉트할 수 있는 데 기본값은 "/" 이다. 추후 해당 설정을 사용하게 될때 설명하겠다.

이번 포스트는 여기에서 마친다. 다음 포스트는 현재까지 코드를 리팩토링하고 Spring RestDocs 를 사용하여 Restful API 메뉴얼을 자동으로 생성되게 구현할 것이다.