인증 구현 - 정책 설정
/**
* <pre>
* UserDetailsService 와 PasswordEncoder을 이용해서
* 로그인을 요청한 사용자의 인증을 처리한다.
*
* UserDetailsService : SecurityUserDetailsService
* PasswordEncoder : SecuritySHA
*
* 인증에 성공한 경우, SecurityContext에 인증정보를 저장해야 한다.
*
* AuthenticationProvider 인터페이스를 구현해야 한다.
* </pre>
*/
@Component
public class SecurityAuthenticationProvider implements AuthenticationProvider {
/**
* SecurityUserDetailsService -> DI
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* SecuritySHA -> DI
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* Spring Security 인증 수행.
* @param authentication 로그인에 사용된 인증 정보들.
* @return 아이디와 비밀번호가 일치하는 회원의 정보 (UsernamePasswordAuthenticationToken)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 인증 시작.
// 로그인에 사용된 아이디(이메일)
String email = authentication.getName();
// 로그인에 사용된 비밀번호
String password = authentication.getCredentials().toString();
// UserDetailsService에서 이메일 정보로 회원의 정보를 조회한다.
// 만약, 회원의 정보가 존재하지 않다면, UsernameNotFoundException이 던져진다.
// UserDetails ==> SecurityUser
UserDetails userDetails = this.userDetailsService.loadUserByUsername(email);
// 비밀번호 암호화를 위해서 DB에 저장되어있던 salt를 조회한다.
// salt 값은 MemberVO 내부에 있고
// Spring Security 과정 내부에서는 UserDetails에 있고
// UserDetails를 상속한 SecurityUser 가 있다!
// UserDetails를 SecurityUser로 변환하여 salt를 얻어온다.
String salt = ((SecurityUser) userDetails).getMemberVO().getSalt();
// PasswordEncoder로 로그인에 사용된 비밀번호를 암호화.
// Spring Security 과정 내부에서 PasswordEncoder는 SecuritySHA로 정의.
// SecuritySHA에는 암호화를 위한 salt 변수를 가지고 있다.
// 조회된 salt 값을 SecuritySHA에 할당을 해야한다.
// PasswordEncoder를 SecuritySHA로 변환하여 salt를 할당한다.
((SecuritySHA) this.passwordEncoder).setSalt(salt);
// PasswordEncoder를 이용해서 암호화된 비밀번호와, 입력한 비밀번호가 같은지 비교.
boolean isMatchPassword = this.passwordEncoder.matches(password, userDetails.getPassword());
// 만약 같지 않다면, BadCredentialsException을 던진다.
if ( ! isMatchPassword ) {
throw new BadCredentialsException("아이디 또는 비밀번호가 일치하지 않습니다.");
}
// 같다면, UsernamePasswordAuthenticationToken을 반환.
// Security Context에 인증 정보를 저장!
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}
/**
* <pre>
* 인증 요청을 처리할 인증 방식 저장.
* 인증 요청을 처리할 인증 필터 타입 지정.
* </pre>
*/
@Override
public boolean supports(Class<?> authentication) {
// UsernamePasswordAuthenticationToken : 아이디 / 비밀번호 인증 방식
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
@Configuration
@EnableWebSecurity // Spring Security Filter 정책 설정을 위한 Annotation
public class SecurityConfig {
...생략
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(httpRequest -> httpRequest
// /board/list 는 Security 인증 여부와 관계없이 모두 접근이 가능하다.
.requestMatchers(AntPathRequestMatcher.antMatcher("/board/search"))
.permitAll()
}
...생략
// 이 애플리케이션은 Form 기반으로 로그인을 하여
// 로그인이 완료되면, /board/search로 이동을 해야한다.
// 로그인 페이지 변경.
http.formLogin(formLogin -> formLogin
// Spring Security 인증이 성공할 경우, LoginSuccessHandler가 동작되도록 설정.
.successHandler(new LoginSuccessHandler())
// Spring Security 인증이 실패할 경우, LoginFailureHandler가 동작되도록 설정.
.failureHandler(new LoginFailureHandler())
// Spring Security Login URL 변경
.loginPage("/member/login")
// Spring Security Login 처리 URL 변경
// SecurityAuthenticationProvider 실행 경로 지정
.loginProcessingUrl("/member/login-proc")
// 로그인ID가 전달될 파라미터 이름
.usernameParameter("email")
// 로그인PW가 전달될 파라미터 이름
.passwordParameter("password"));
// CSRF 방어로직 무효화.
http.csrf(csrf -> csrf.disable());
return http.build();
로그인 실패 처리
com.example.demo.beans.security.handler 패키지 생성
LoginFailureHandler.java 파일 생성
package com.hello.forum.beans.security.handler;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* <pre>
* Spring Security Login 에 실패했을 경우
* 해당 이벤트를 감지해서 자동으로 실행되는 클래스.
*
* AuthenticationFailureHandler 인터페이스를 구현!
* </pre>
*/
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// 인증 실패 메세지를 로그인 페이지에 노출 시키는 코드.
String authenticationFailureMessage = exception.getMessage();
// 로그인 페이지를 보여주고
// 인증 실패 메세지를 보내준다.
String loginPagePath = "/WEB-INF/Views/member/memberlogin.jsp";
RequestDispatcher rd = request.getRequestDispatcher(loginPagePath);
request.setAttribute("message", authenticationFailureMessage);
rd.forward(request, response);
}
}
import com.example.demo.beans.security.handler.LoginFailureHandler;
@Configuration
@EnableWebSecurity // Spring Security Filter 정책 설정을 위한 Annotation
public class SecurityConfig {
... 생략 ...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 로그인에 성공하면 "/board/list"로 이동하도록 함.
http.formLogin(formLogin ->
formLogin.defaultSuccessUrl("/board/list")
// 로그인 실패했을 때 처리 방법
.failureHandler(new LoginFailureHandler())
.loginPage("/member/login")
.loginProcessingUrl("/member/login-proc")
.usernameParameter("email")
.passwordParameter("password"));
return http.build();
}
}
세션 관련 Interceptor 모두 제거 - 비 Security 요소 제거