1. 개요
해당 프로젝트에서 관리자 페이지를 만들어 운영하기로 했다. 운영하게 될 서비스는 소셜 로그인만 지원하도록 했지만 관리자 페이지는 username/password 형식으로 구현하도록 할 것이다. 해당 프로젝트는 스프링 시큐리티를 사용하므로 필터를 사용하여 로그인을 구현하고자 한다.
2. 사용한 코드
2-1. LoginFilter
public class JsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_LOGIN_REQUEST_URL = "/api/v1/login/admin"; // /login 으로 오는 요청을 처리할 것이다
private static final String HTTP_METHOD = "POST"; //HTTP 메서드의 방식은 POST 이다.
private static final String CONTENT_TYPE = "application/json";//json 타입의 데이터로만 로그인을 진행한다.
private final ObjectMapper objectMapper;
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD); //=> /login 의 요청에, POST로 온 요청에 매칭
public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) {
super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);
this.objectMapper = objectMapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//json type이 아닌 경우 예외처리
if(request.getContentType() == null || !request.getContentType().equals(CONTENT_TYPE) ) {
throw new AuthenticationServiceException("Authentication Content-Type not supported: " + request.getContentType());
}
//json을 파싱하여 user 인증 진행
String messageBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
LoginDto loginDto = objectMapper.readValue(messageBody, LoginDto.class);
/**
* {
* "username": "username",
* "password": "password"
*/
String username = loginDto.username();
String password = loginDto.password();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);//principal 과 credentials 전달
return this.getAuthenticationManager().authenticate(authRequest);
}
}
필터 코드 전문이다. /api/v1/login/admin 경로로 오는 POST 요청에 대해 username과 password를 받아 토큰을 생성하고, 이를 기반으로 인증을 진행한다.
2-2. LoginSuccessHandler
@Slf4j
@RequiredArgsConstructor
public class LoginSuccessJwtProviderHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
private final RedisUtil redisUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
setResponse(response, StatusCode.ADMIN_LOGIN_SUCCESS);
addToken(response, principal);
}
private void addToken(HttpServletResponse response, UserPrincipal principal) {
Member member = principal.getMember();
LoginCreateJwtDto loginCreateJwtDto = LoginCreateJwtDto.builder()
.id(member.getId())
.username(member.getUsername())
.role(member.getRole().toString())
.requestTimeMs(LocalDateTime.now())
.build();
String accessToken = jwtUtil.createJwt(loginCreateJwtDto, "access");
String refreshToken = jwtUtil.createJwt(loginCreateJwtDto, "refresh");
log.info("[{}} JWT 토큰 생성 access: {}, refresh: {}", Thread.currentThread().getStackTrace()[1].getClassName(), accessToken, refreshToken);
//redis에 refreshToken 저장하기((key, value): (token, username))
//Bearer을 포함하지 않음
redisUtil.setDataExpire(refreshToken, member.getUsername(), 15778800);
//응답에 JWT 추가
response.addHeader("Authorization", "Bearer " + accessToken);
response.addHeader("Refresh_token", "Bearer " + refreshToken);
log.info("[{}} 응답 헤더에 토큰 담기", Thread.currentThread().getStackTrace()[1].getClassName());
}
private void setResponse(
HttpServletResponse response,
StatusCode statusCode
) {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(statusCode.getHttpCode());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
CommonResponse errorResponse = CommonResponse.success(statusCode, null);
try {
log.info("[{}] admin login success, code: {}", Thread.currentThread().getStackTrace()[1].getMethodName(), statusCode);
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
} catch (IOException e) {
e.printStackTrace();
}
}
}
현 프로젝트는 JWT 방식의 인증을 진행한다. 이에 따라 로그인 성공 시 JWT 토큰을 만들어 헤더에 담고, refresh 토큰을 redis에 저장한다.
2-3. LoginFailureHandler
@Slf4j
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
setErrorResponse(response, StatusCode.ADMIN_LOGIN_FAILURE);
}
private void setErrorResponse(
HttpServletResponse response,
StatusCode statusCode
) {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(statusCode.getHttpCode());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
CommonResponse errorResponse = CommonResponse.fail(statusCode);
try {
log.error("[{}] Login fail, code: {}", Thread.currentThread().getStackTrace()[1].getMethodName(), statusCode);
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
} catch (IOException e) {
e.printStackTrace();
}
}
}
로그인 실패 시의 내역을 응답 바디에 담아 반환하도록 한다.
3. SecurityConfig
@Bean
public static PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {//AuthenticationManager 등록
DaoAuthenticationProvider provider = daoAuthenticationProvider();//DaoAuthenticationProvider 사용
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter() throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter = new JsonUsernamePasswordAuthenticationFilter(objectMapper);
jsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
jsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessJWTProvideHandler());
jsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
return jsonUsernamePasswordLoginFilter;
}
@Bean
public LoginSuccessJwtProviderHandler loginSuccessJWTProvideHandler(){
return new LoginSuccessJwtProviderHandler(jwtUtil, redisUtil);
}
@Bean
public LoginFailureHandler loginFailureHandler(){
return new LoginFailureHandler();
}
위에서 만든 클래스들을 스프링 빈으로 등록해야 한다. 로그인 필터를 보면 다음 코드가 있다.
return this.getAuthenticationManager().authenticate(authRequest);
해당 코드를 동작시키도록 커스텀한 필터를 등록하는 과정들이다.
4. 여담
사실 컨트롤러에 있을 코드를 좀 풀어 쓴 느낌이다. 코드가 좀 복잡해보이게 적었는데 다 이해하지 못한 느낌이다. 다음 게시글에서는 authenticate가 어떻게 동작하는 지 알아볼 예정이다.
'Proj > Tripot' 카테고리의 다른 글
| 신고 기능 구현 (0) | 2025.01.04 |
|---|---|
| 로그인 인증 과정 추가해보기 - AuthenticationProvider 커스텀 (2) | 2024.11.30 |
| 스프링부트 테스트코드 작성하기: Controller (0) | 2024.11.22 |
| 스프링부트 테스트 코드 작성하기: Repository, Service (0) | 2024.11.18 |
| 스프링 시큐리티에서 발생하는 예외 처리하기 (0) | 2024.11.10 |