8. 스프링 시큐리티로 로그인/로그아웃, 회원가입 구현
8.1 스프링 시큐리티
스프링 시큐리티: 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크입니다. 에너테이션으로 설정이 쉽고 CSRF 공격, 세션 고정 공격을 방어해주고 요청 헤더도 보안 처리를 해주므로 개발자가 보안 관련 개발을 해야 하는 부담을 크게 줄여줍니다.
8.2 회원 도메인 만들기
User.java
@Table(name = "users")
@NoArgsConstructor
@Getter
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id", updatable = false)
private Long id;
@Column(name = "email", nullable = false, unique= true)
private String email;
@Column(name = "password")
private String password;
@Builder
public User(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("user"));
}
@Override
public String getUsername() {
return email;
}
@Override
public String getPassword() {
return password;
}
@Override
public boolean isAccountNonExpired() {
//로직
return true;
}
@Override
public boolean isAccountNonLocked() {
//로직
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
User 클래스가 상속한 UserDetails 클래스는 스프링 시큐리티에서 사용자의 인증 정보를 담아두는 인터페이스입니다. 스프링 시큐리티에서 해당 객체를 통해 인증 정보를 가져오려면 필수 오버라이드 메서드들을 여러 개 사용해야 합니다.
메서드 | 반환 타입 | 설명 |
getAuthorities() | Collection<? extends GratedAuthority> | 사용자가 가지고 있는 권한의 목록을 반환합니다. 현재 예제 코드에서는 사용자 이외의 권한이 없기 때문에 user 권한만 담아 반환합니다. |
getUsername() | String | 사용자를 식별할 수 있는 사용자 이름을 반환합니다. 이때 사용되는 사용자 이름은 반드시 고유해야 합니다. |
getPassword() | String | 사용자의 비밀번호를 반환합니다. 이때 저장되어있는 비밀번호는 암호화해서 저장해야 합니다. |
isAccountNonExpired() | boolean | 계정이 만료되었는지 확인하는 메서드입니다. 만약 만료되지 않은 때는 true를 반환합니다. |
isAccountNonLocked() | boolean | 계정이 잠금되었는지 확인하는 메서드입니다. 만약 잠금되지 않은 때는 true를 반환합니다. |
isCredentialsNonExpired() | boolean | 비밀번호가 만료되었는지 확인하는 메서드입니다. 만료되지 않은 때는 true를 반환합니다. |
isEnabled() | boolean | 계정이 사용가능한지 확인하는 메서드입니다. 사용 가능하다면 true를 반환합니다. |
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
스프링 데이터 JPA는 메서드 규칙에 맞춰 메서드를 선언하면 이름을 분석해 자동으로 쿼리를 생성해줍니다. findByEmail() 메서드는 실제 데이터베이스에 회원 정보를 요청할 때 다음 쿼리를 실행합니다.
FROM users WHERE email = #{email}
자주 사용하는 쿼리 데서드의 명명 규칙은 아래와 같습니다.
코드 | 설명 | 쿼리 |
findByName() | "name" 컬럼의 값 중 파라미터로 들어오는 값과 같은 데이터 반환 | ...WHERE name = ?1 |
findByNameAndAge() | 파라미터로 들어오는 값 중 첫 번째 값은 "name" 컬럼에서 조회하고 두번째 값은 "age" 컬럼에서 조회한 데이터를 반환 | ...WHERE name =?1 AND age = ?2 |
findByNameOrAge() | 파라미터로 들어오는 값 중 첫 번째 값이 "name" 컬럼에서 조회되거나 두번째 값이 "age"에서 조회되는 데이터 반환 | ...WHERE name = ?1 OR age = ?2 |
findByAgeLessThan() | "age" 컬럼의 값 중 파라미터로 들어온 값보다 작은 데이터 반환 | ...WHERE age <?1 |
findByAgeGreaterThan() | "age" 컬럼의 값 중 파라미터로 들어온 값보다 큰 데이터 반환 | ...WHERE age>?1 |
findByName(is)Null() | "name" 컬럼의 값중 null인 데이터 반환 | ...WHERE name IS NULL |
UserDetailService.java
@RequiredArgsConstructor
@Service
//스프링 시큐리티에서 사용자 정보를 가져오는 인터페이스
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
//사용자 이메일로 정보를 가져오는 메서드
@Override
public User loadUserByUsername(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException(email));
}
}
실제 인증 처리를 하는 시큐리티 설정파일인
WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final UserDetailsService userService;
// (1) 스프링 시큐리티 기능 비활성황
@Bean
public WebSecurityCustomizer configure(){
return (web) ->web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers(new AntPathRequestMatcher("/static/**"));
}
// (2) 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests(auth -> auth // (3) 인증, 인가 설정
.requestMatchers(
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/signup"),
new AntPathRequestMatcher("/user")
).permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin // (4) 폼 기반 로그인 설정
.loginPage("/login")
.defaultSuccessUrl("/articles")
)
.logout(logout -> logout // (5) 로그아웃 설정
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
)
.csrf(AbstractHttpConfigurer::disable) // (6) csrf 비활성화
.build();
}
// (7) 인증 관리자 관련 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailsService userDetailsService) throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userService); // (8) 사용자 정보 서비스 설정
authProvider.setPasswordEncoder(bCryptPasswordEncoder);
return new ProviderManager(authProvider);
}
// (9) 패스워드 인코더로 사용할 빈 등록
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
(1) 스프링 시큐리티의 모든 기능을 사용하지 않게 하는 코드입니다. 즉 모든 곳에 인증, 인가 서비스를 적용하지 않습니다.
(2) 특정 HTTP 요청에 대한 웹 기반 보안을 구성합니다. 이 메서드에서 로그인, 로그아웃, 인증, 인가를 설정할 수 있습니다.
(3) 특정 경로에 대한 액세스를 설정합니다.
- requestMatchers(): 특정 요청과 일치하는 url에 대한 액세스를 설정합니다.
- permitAll(): 누구나 접근이 가능하게 설정합니다.
- anyRequest(): 위에서 설장한 url 이외의 요청에 대해서 설정합니다.
- authenticated(): 별도의 인가는 필요하지 않지만 인증이 성공된 상태여야 접근할 수 있습니다.
(4) 폼기반 로그인 설정을 합니다.
- loginPage(): 로그인 페이지 경로를 설정합니다.
- defaultSuccessUrl(): 로그인이 완료되었을 때 이동할 경로를 설정합니다.
(5) 로그아웃 설정입니다.
- logoutSuccessUrl(): 로그아웃이 완료되었을 때 이동할 경로를 설정합니다.
- invaildateHttpSession(): 로그아웃 이후에 세션을 전체 삭제할지 여부를 설정합니다.
(6) CSRF 설정을 비활성화 합니다. CSRF 공격을 방지하기 위해서는 활성화하는게 좋지만 실습을 편리하게 하기 위해 비활성화 해두었습니다.
(7) 인증 관리자 관련 설정입니다. 사용자 정보를 가져올 서비스를 재정의하거나 인증 방법등을 설정할 때 사용합니다.
(8) 사용자 서비스를 정의합니다.
- userDetailsService(): 사용자 정보를 가져올 서비스를 설정합니다. 이때 설정하는 서비스 클래스는 반드시 UserDetailsService를 상속받은 클래스이어야 합니다.
- passwordEncoder(): 비밀번호를 암호화하기 위해 인코더를 설정합니다.
(9): 패스워드 인코더를 빈으로 등록합니다.
8.4 회원가입 구현
AddUserRequeset.java
@Getter
@Setter
public class AddUserRequest {
private String email;
private String password;
}
UserService.java
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public Long save(AddUserRequest dto) {
return userRepository.save(User.builder()
.email(dto.getEmail())
// 패스워드 암호화
.password(bCryptPasswordEencoder.encode(dto.getPassword()))
.build()).getId();
}
AddUserRequest 객체를 인수로 받는 회원 정보 추가 메서드를 작성했습니다.
UserApiController.java
@RequiredArgsConstructor
@Controller
public class UserApiController {
private final UserService userService;
@PostMapping("/user")
public String signup(AddUserRequest request){
userService.save(request);
return "redirect:/login";
}
}
회원가입 처리가 된 다음 로그인 페이지로 이동하기 위해 redirect: 접두사를 붙였습니다.
UserViewController.java
@Controller
public class UserViewController {
@GetMapping("/login")
public String login() {
return "oauthlogin";
}
@GetMapping("/signup")
public String signup() {
return "signup";
}
}
로그인, 회원가입 경로로 접근하면 뷰 파일을 연결하는 컨트롤러입니다.
/login 경로로 접근하면 login() 메서드가 login.html을, /signup 경로에 접근하면 signup() 메서드는 signup.html을 반환합니다.
로그아웃 메서드
UserApiController.java
//생략
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response){
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "redirect:/login";
}
}
/logout GET 요청이 들어오면 로그아웃을 담당하는 핸들러인 SecurityContextLogoutHandler의 logout() 메서드를 호출해서 로그아웃합니다.
요약
1. 인증은 보호된 리소스에 접근하는 것을 허용하기 이전에 등록된 사용자의 신원을 입증하는 과정입니다.
2. 인가는 특정 부분에 접근할 수 있는지 확인하는 작업입니다.
3. 스프링 시큐리티는 스프링 기반의 애플리케이션 보안을 담당하는 스프링 하위 프레임워크입니다.