스프링 시큐리티 : 폼인증

폼 인증 예제 살펴보기
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 설정파일 생성
- @EnableWebSecurity
- @Configuration
- extends WebSecurityConfigurerAdapter
- 추후 다시 설명..
- 주로 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();
}
스프링 시큐리티 커스터마이징: 인메모리 유저 추가
- 시큐리티 인메모리 유저눈 어디에서 설정될까?
- 로그를 확인해야한다.
- UserDetailsServiceAutoConfiguration
- properties를 읽어서 user를 만들어준다.
- SecurityProperties.User
- name, password, role 을 properties에서 설정 가능하다.
- Properties에서 설정하고 다시 실행
- application.properties 설정 (ADMIN 권한)
- "/admin" 접근 후 설정한 정보로 접속
- 결과
- 그러나 이러한 방식은 계정이 하나만 설정됨으로 거의 사용되지 않는다.
- 내가 원하는 유저정보를 임의대로 사용하고 싶다면
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 에 명시적으로 설정해줄수 있지만, 빈으로 등록만 되면 자동으로 설정된다.
- UserDetailsService
loadUserByUsername()
를 구현해야한다.- 역할 : DAO로 유저 정보 읽기
- 조건 : username으로 정보를 가져와서 UserDetails로 리턴한다.
- Security에서 제공하는 User객체(UserDeatils를 구현)를 사용하면 보다 편리하게 UserDetails를 리턴할 수 있다.
builder()
를 사용- 없던 시절에는 UserDetails 구현 객체를 직접 만들어서 사용했다. (이부분에 대해서는 나중에 다시…)
테스트
- 계정을 생성하는 API를 간단한게 생성 후에, 테스트 한다.
(테스트용으로 간다하게 구현하지만 이렇게 만들면 안된다.) - Security Config에 계정 생성 API의 권한체크를
permitAll()
로 설정한다. -
- 결과는.. 로그인 시, 에러발생
- 스프링 시큐리티는 password에 어떠한 패턴을 요구한다.
- 예> {암호화알고리즘이름}123
- PasswordEncoder 설정없기때문에 발생
- {noop}을 패스워드 앞에 추가하면 가능하다. (암호화 알고리즘의 이름을 의미한다. noop은 암호화 없음.)
- 실제로는 이렇게 사용하면 안된다. 반드시 암호화를 해야한다.
스프링 시큐리티 커스터마이징: 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으로 설정한다.