티스토리 뷰

Spring/Security

SpringSecurity : 폼인증

Bong Gu 2021. 10. 9. 17:30
728x90

스프링 시큐리티 : 폼인증

whiteship_security

폼 인증 예제 살펴보기

Principal

  • Java Security에 있는 인터페이스
    • 인증된 사용자가 있다면, 해당 정보를 pricipal 인터페이스 타입으로 SpringMVC 핸들러에 받아서 사용할 수 있도록 해준다.
    • arguments resolver 사용

예제 살표보기

예제 내용 미리보기

  • 홈펨이지
    • /
    • 인증된 사용자, 인증되지 않은 사용자 모두 접근이 가능
    • 인증된 사용자가 로그인 한 경우에는 이름을 출력할 것
  • 정보
    • /info
    • 인증된 사용자, 인증되지 않은 사용자 모두 접근이 가능
  • 대쉬보드
    • /dashboard
    • 반드시 로그인 한 사용자만 접근 가능
    • 인증하지 않은 사용자가 접근 시, 로그인 페이지로 이동한다.
  • 어드민
    • /admin
    • ADMIN권한을 가진 사용자만 접근 가능
    • 인증하지 않은 사용자가 접근 시, 로그인 페이지로 이동한다.
    • 인증은 거쳤으나, 권한이 충분하지 않은경우 에러 메세지를 출력



스프링 웹 프로젝트 만들기

타임리프

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Index</title>
</head>
<body>
  <h1 th:text="${message}">hello</h1>
</body>
</html>
  • resources > template 디렉토리에 html파일로 사용
  • xmlns:th=”http://www.thymeleaf.org” 네임스페이스를 html 태그에 추가한다.
  • th:text=${message} 를 사용해서 Model에 들어있는 값을 출력 가능하다.



스프링 시큐리티 연동

의존성 추가

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 의존성을 추가하고 나면 자동설정이 적용된다.
    • 모든 요청은 인증을 필요로 한다.
    • 기본 유저가 생성된다.
    • username = user
    • password = 콘솔창에 출력된 문자열 확인(매번 다른 문자열)



스프링 시큐리티 설정하기

  • Security 설정파일 생성
  • 주로 WebSecurityConfigurerAdapter 의 메소드를 오버라이딩 하는 방식으로 시큐리티를 설정한다.

예제

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .mvcMatchers("/", "/info").permitAll()
        .mvcMatchers("/admin").hasRole("ADMIN")
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .and()
        .httpBasic();
  }
}
  • "/", "/info" 의 요청은 권한이 필요없다.
    • "/**" 과 같이 ant pattern도 사용가능
  • "/admin" 요청은 "ADMIN" 권한이 필요하다.
  • 그외 다른 요청은 인증만 필요하다.
  • formlogin 과 httpBasic 을 사용한다.
  • .and() 로 반드시 이어서 할 필요는 없다.
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .mvcMatchers("/", "/info").permitAll()
        .mvcMatchers("/admin").hasRole("ADMIN")
        .anyRequest().authenticated();
    http.formLogin();
    http.httpBasic();
  }



스프링 시큐리티 커스터마이징: 인메모리 유저 추가

  • 시큐리티 인메모리 유저눈 어디에서 설정될까?
    • 로그를 확인해야한다.
    security01-1
    • UserDetailsServiceAutoConfiguration
    security01-2
    • properties를 읽어서 user를 만들어준다.
    • SecurityProperties.User
    security01-3
    • name, password, role 을 properties에서 설정 가능하다.
  • Properties에서 설정하고 다시 실행
    • application.properties 설정 (ADMIN 권한)
    security01-4
    • "/admin" 접근 후 설정한 정보로 접속
    security01-5
    • 결과
    security01-6
  • 그러나 이러한 방식은 계정이 하나만 설정됨으로 거의 사용되지 않는다.
  • 내가 원하는 유저정보를 임의대로 사용하고 싶다면 configure(AuthenticationManagerBuilder auth) 를 오버라이딩해서 사용하자.
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("bong").password("{noop} 1234").roles("USER").and()
        .withUser("admin").password("{noop} 1234").roles("ADMIN");
  }
  • {noop} 은 SpringSecurity5 부터 사용하는 기본 패스워드 인코더가 사용하는 패스워드 구조
    • 앞에 prefix로 어떠한 방식으로 인코딩 된것인지 알린다.
    • noop 은 no option 암호화를 하지 않는다.
  • 그러나 이러한 방법도 계정이 추가될때 마다 코딩이 추가되야한다. 때문에 잘 사용되지 않는다.
  • DB에 저장된 유저 정보를 가지고 인증을 하는 방법에 대해서 알아보자.(JPA를 사용)



스프링 시큐리티 커스터마이징: JPA 연동

  • 인메모리가 아닌 DB를 사용한다면 그에 맞게 의존성 및 설정을 추가해주어야 한다.
    • JPA 및 H2 의존성 추가
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>

Account

@Getter
@Entity
public class Account {

  @Id @GeneratedValue
  private Long id;

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

  @Column(nullable = false)
  private String password;

  private String role;

}
  • username, password는 필수값
  • username 은 유니크한 값을 가진다.

AccountRepository

public interface AccountRepository extends JpaRepository<Account, Long> {
  Optional<Account> findByUsername(String username);
}
  • spring-data-jpa를 사용할것이므로 위와같이 레포지토리를 만든다.

AccountService

@RequiredArgsConstructor
@Transactional
@Service
public class AccountService implements UserDetailsService {

  private final AccountRepository accountRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional<Account> byUsername = accountRepository.findByUsername(username);
    Account account = byUsername.orElseThrow(() -> new UsernameNotFoundException(String.format("%s is not founded!", username)));

    return User.builder()
        .username(account.getUsername())
        .password(account.getPassword())
        .roles(account.getRole())
        .build();
  }
}
  • Security 설정을 위해 UserDetailsService를 구현한다.
    • SecurityConfig 에 명시적으로 설정해줄수 있지만, 빈으로 등록만 되면 자동으로 설정된다.
    security01-8
  • UserDetailsService
    • loadUserByUsername() 를 구현해야한다.
    • 역할 : DAO로 유저 정보 읽기
    • 조건 : username으로 정보를 가져와서 UserDetails로 리턴한다.
  • Security에서 제공하는 User객체(UserDeatils를 구현)를 사용하면 보다 편리하게 UserDetails를 리턴할 수 있다.
    • builder() 를 사용
    • 없던 시절에는 UserDetails 구현 객체를 직접 만들어서 사용했다. (이부분에 대해서는 나중에 다시…)

테스트

  • 계정을 생성하는 API를 간단한게 생성 후에, 테스트 한다.
    (테스트용으로 간다하게 구현하지만 이렇게 만들면 안된다.)
  • Security Config에 계정 생성 API의 권한체크를 permitAll() 로 설정한다.
  • security01-7
  • 결과는.. 로그인 시, 에러발생
    • 스프링 시큐리티는 password에 어떠한 패턴을 요구한다.
    • 예> {암호화알고리즘이름}123
    • PasswordEncoder 설정없기때문에 발생
    • {noop}을 패스워드 앞에 추가하면 가능하다. (암호화 알고리즘의 이름을 의미한다. noop은 암호화 없음.)
    security01-9
    • 실제로는 이렇게 사용하면 안된다. 반드시 암호화를 해야한다.



스프링 시큐리티 커스터마이징: PasswordEncoder

  • 비밀번호는 반드시 인코딩해서 저장해야한다. (단방향 암호화 알고리즘으로..)
    • 스프링 시큐리티가 제공하는 PasswordEncoder는 특정한 포맷으로 동작한다.
    {id}encoderPassword
    ex..){bcrypt}$2a$10$YNQzAkb6TR/1eh7SRG21ruPSaFSFO6iIMhHMZhYFnQHOp8Rg.az06
    • 동시에 여러가지 인코딩을 지원한다.
  • NoOp 패스워드 인코딩 : 패스워드 인코딩을 사용하지 않고 평문 그대로 저장
  @Bean
  public PasswrodEncoder passwordEncoder() {
      return NoOpPasswrodEncoder.getInstance();
  }
  • 시큐리티5 이전에는 사용가능했으나 추천하지 않는다.
    • bcrypt로 암호화해서 저장, 비교 시에는 {id}을 확인하여 다양한 인코딩을 지원한다.(추천)
  @Bean
  public PasswordEncoder passwordEncoder() {
      return PasswrodEncoderFactories.createDelegatingPasswordEncoder();
  }



스프링 시큐리티 테스트

  <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <version>${spring-security.version}</version>
      <scope>test</scope>
  </dependency>
  • spring-boot-dependencies에 관련 버전 명시가 없음으로 버전을 명시하는것이 좋다.
  • SecurityVersion을 따라가도록 설정했다.

RequestProcessor를 사용하는 방법

@Test
public void index_user() throws Exception {
  mockMvc.perform(get("/")
      .with(user("yrchoi").roles("USER"))
  )
      .andDo(print())
      .andExpect(status().isOk());
}
  • yrchoi라는 USER 권한을 가진 유저가 로그인된 상태라고 가정하고 perform 한다.
    • DB에 해당계정이 있는것이 아닌 이러한 유저가 이미 로그인되어있다고 가정한다.(어노테이션 사용방법도 동일하다.)

어노테이션을 사용하는 방법

@WithMockUser(username = "yrchoi", roles = "ADMIN")
@Test
public void admin_admin() throws Exception {
  mockMvc.perform(get("/admin"))
      .andDo(print())
      .andExpect(status().isOk());
}
  • username, roles 설정 가능
  • @WithAnonymousUser등도 사용가능 하다.
  • 커스텀 어노테이션을 만들어 사용가능
  @WithYrchoiUser
  @Test
  public void admin_user() throws Exception {
    mockMvc.perform(get("/admin"))
        .andDo(print())
        .andExpect(status().isForbidden());
  }
  @Retention(RetentionPolicy.RUNTIME)
  @WithMockUser(username = "yrchoi", roles = "USER")
  public @interface WithYrchoiUser {
  }
  • runtime시 까지 해당 어노테이션을 참조함으로 Retention.Runtime으로 설정한다.
728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday