Springboot + spring security + h2 + jpa + thymeleaf로 URL 접근 보안 구현하기
시작하기
이 글을 SpringBoot와 Spring Security5를 사용하여 URL 접근을 보호하고 사용자 로그인, 로그아웃을 구현하는 것을 설명한다.
보안 기본 용어
용어 | 설명 |
---|---|
인증(Authentication) | 인증은 주체의 신원을 증명하는 과정을 말한다. |
주체(Principal) | 주체는 해당 리소스에 접근하고자 하는 사용자를 말한다. |
신원정보(Credential) | 신원정보는 주체가 리소스를 얻기위해 제시하는 것으로서 일반적으로 비밀번호이다. |
인가(Authorization) | 인가는 인증을 마친 주체에게 권한(Authority)을 부여하여 특정 리소스에 접근할 수 있게 허가하는 과정을 말한다. |
접근 통제(Access controll) | 접근통제는 리소스에 접근하는 행위를 제어하는 것을 말한다. 어떤 유저가 어떤 리소스에 접근하도록 허락할지를 결정하는 행위, 즉 접근 통제 결정(Access controll decision)이 뒤따른다. |
프로젝트에 사용될 의존성은 다음과 같다.
Dependency
- spring-boot-starter-data-jpa
- spring-boot-starter-security
- spring-boot-starter-thymeleaf
- thymeleaf-extras-springsecurity5
- spring-boot-starter-web
- h2
- lombok
스프링 시큐리티에서는 @EnableWebSecurity 어노테이션과 WebSecurityConfigurerAdapter 추상클래스를 상속하여 웹 애플리케이션 보안을 구성한다.
WebSecurityConfigurerAdapter의 기본 보안 설정 목록
기본 보안 | 설명 |
---|---|
폼 기반 로그인 서비스(form-based login service) | 유저가 애플리케이션에 로그인하는 기본 폼 페이지를 제공합니다. |
HTTP 기본 인증(Basic authentication) | 요청 헤더에 표시된 HTTP 기본 인증 크레덴셜을 처리합니다. 원격 프로토콜, 웹 서비스를 이용해 인증 요청을 할 때에도 쓰입니다. |
로그아웃 서비스 | 유저를 로그아웃 시키는 핸들러를 기본 제공합니다. |
익명 로그인(anonymous login) | 익명 유저도 주체를 할당하고 권한을 부여해서 마치 일반 유저처럼 처리합니다. |
서블릿 API 연계 | HttpServletRequest.isUserInRole(), HttpServletRequest.getuserPrincipal() 같은 표준 서블릿 API를 이용해 웹 애플리케이션에 위치한 보안 정보에 접근합니다. |
CSFR | 사이트 간 요청 위조 방어용 토큰을 생성해 HttpSession에 넣습니다. |
보안 헤더 | 보안이 적용된 패키지에 대해서 캐시를 해제하는 식으로 XSS 방어, 전송 보안(transfer security), X-Frame 보안 기능을 제공합니다. |
URL 접근 제어하기
HttpSecurity의 authorizeRequests 메소드를 이용하여 특정 리소스(URL)의 접근 통제를 할 수 있다. 접근 리소스를 등록해두면 해당 요청이 올때마다 인증 여부를 확인한다. 아래 예제에서는 USER와 ADMIN이라는 권한으로 각각 /user와 /admin 리소스에 접근할 수 있도록 하고있다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/anyone").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/user").hasAnyRole("USER")
.antMatchers("/admin").hasAnyRole("ADMIN")
.antMatchers("/h2-console/**").hasAnyRole("ADMIN")
.and()
}
}
로그인과 로그아웃 요청 처리하기
HttpSecurity에 formLogin()을 설정하면 스프링 시큐리티에서 제공하는 로그인 폼이 자동으로 생성된다. 로그인 페이지를 커스텀하게 사용하려면 formLogin().loginPage(“url”)을 사용하면된다. loginProcessingUrl(“url”)메서드를 사용하여 인증 주체가 권한을 얻기 위한 URL을 지정한다. 이 URL의 메서드는 POST이다.
로그아웃을 사용하기 위해서는 logout()메서드를 사용한다. logoutUrl(“url”)을 사용하여 로그아웃 URL을 지정해 줄 수 있다. 로그인 URl과 마찬가지로 메서드는 POST이다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and()
.formLogin()
.loginProcessingUrl("/login")
.loginPage("/")
.usernameParameter("memberId")
.passwordParameter("password")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
;
}
CSFR 공격방어
CSFR 방어기능은 CSFR 공격에 노출된 위험을 에방해준다. .csrf().disable() 메서드를 사용하여 CSFR 방어기능을 해제할 수도 있다.
그리고 로그인과 로그아웃 시에 CSFR을 위해 발급한 토큰을 전송해주는 코드를 사용자쪽에서도 구현해주어야 한다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and()
.csrf()
;
}
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
유저 인증하기
유저를 만들어 해당 유저가 특정 리소스에 접근할 수 있도록 하자.
@Data
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String memberId;
private String password;
private String memberRole;
private String email;
private String phoneNumber;
}
스프링 시큐리티에서는 AuthenticationManager를 이용해 인증을 수행한다. 위에서 정의한 Member 객체로 인증을 하기 위해서는 다음과 같이 AuthenticationManagerBuilder를 사용하고, UserDetailsService 인터페이스 구현체를 생성해야하는데, UserDetailsService는 Member 객체를 로드해 오는 일을 한다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Autowired
public UserDetailServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public UserDetails loadUserByUsername(String memberId) {
Member member = memberRepository.findMemberByMemberId(memberId);
if (ObjectUtils.isEmpty(member))
throw new UsernameNotFoundException("아이디 또는 비밀번호를 확인해 주세요.");
return new AuthorityMember(member);
}
}
비밀번호 암호화 하기
Member 객체에 저장된 비밀번호가 암호화 되어 있다면, 사용자로부터 인증요청을 받을때 받아온 비밀번호도 같은 형식으로 암호화하여 매칭해야한다. AuthenticationManager가 특정 암화화 알고리즘으로 비밀번호를 암호화 할 수 있도록 설정할 수 있다.
@Bean
public PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder(11);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder());
}
Thymeleaf에서 보안 처리하기
Thymeleaf의 sec:authorize 속성을 이용하여 사용자 인증이 완료되었는지 아닌지를 확인 할 수 있다.
<div sec:authorize="isAuthenticated()">
</div>
그리고 Thymeleaf의 sec:authentication 속성을 이용하면 로그인한 사용자의 아이디 정보도 불러올 수 있다.
<span sec:authentication="name"></span>
이번 글에 사용된 프로젝트는 GitHub에서 확인할 수 있다. 샘플코드에서 암호화된 비밀번호는 1111이다.
참고문서
Thymeleaf + Spring Security integration basics
CSRF Protection with Spring MVC and Thymeleaf
참고도서
제목: 스프링5레시피(4판)
지은이: 마틴데니엄, 다니엘 루비오, 조시 롱
옮긴이: 이일웅
펴낸곳: 한빛미디어