패스워드 암호화 방식 설정
package com.example.my.config.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
// 함수를 IoC 컨테이너로 넘겨줌
// 클래스에는 @Component
// 다른 곳에서
// @Autowired
// PasswordEncoder passwordEncoder시 자동으로 PasswordEncoder 타입의 클래스를 주입해줌
// passwordEncoder 를 구현한 impl클래스들 중 어떤 클래스를 사용할 지 설정
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
로그인 / 로그아웃, 접근 세팅
package com.example.my.config.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationSuccessHandler authenticationSuccessHandler;
private final AuthenticationFailureHandler authenticationFailureHandler;
private final LogoutSuccessHandler logoutSuccessHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf(config -> config.disable());
// httpSecurity.headers(config -> config
// .frameOptions(frameOptionsConfig -> frameOptionsConfig
// .disable()
// )
// );
//
// httpSecurity.authorizeHttpRequests(config -> config
// .requestMatchers(PathRequest.toH2Console())
// .permitAll()
// );
// 정적 파일들의 접근 세팅
httpSecurity.authorizeHttpRequests(config -> config
.requestMatchers("/css/**", "/js/**", "/assets/**", "/favicon.ico")
.permitAll()
// admin*.js 는 admin이 들어간 js파일
.requestMatchers("/js/admin*.js", "/h2/**", "/temp/**")
.hasRole("ADMIN")
);
httpSecurity.authorizeHttpRequests(config -> config
// 이 요청들은
.requestMatchers("/auth/**", "/api/*/auth/**")
// 모두 허용
.permitAll()
// 이 요청들은
.requestMatchers("/admin/**", "/api/*/admin/**")
// "ADMIN" 이라는 Role을 가진 사용자만 허용
.hasRole("ADMIN")
// 다른 모든 요청들은
.anyRequest()
// 인증된 사람(로그인 된 사용자)만 접근 허용
.authenticated()
);
httpSecurity.formLogin(config -> config
// 로그인 페이지 매핑
.loginPage("/auth/login")
// 로그인 기능(API) 매핑
.loginProcessingUrl("/api/v1/auth/login")
// Security 기본 id 변수명 / form안의 input태그들의 name속성으로 접근
//<input type="text" id="id" ***이 속성*** name="id" class="form-control" aria-describedby="idAddOn" />
.usernameParameter("id")
// Security 기본 pw 변수명
.passwordParameter("password")
// 처리 성공 시 유저에게 보낼 내용
.successHandler(authenticationSuccessHandler)
// 처리 실패 시 유저에게 보낼 내용
.failureHandler(authenticationFailureHandler)
// 모든 유저 접근 허용
.permitAll()
);
httpSecurity.logout(config -> config
// 로그아웃 페이지 매핑
.logoutUrl("/auth/logout")
// 세션 초기화(?)
.invalidateHttpSession(true)
// 쿠키 삭제
.deleteCookies("JSESSIONID")
// 로그아웃 성공 시 유저에게 보낼 내용
.logoutSuccessHandler(logoutSuccessHandler)
// 모든 유저 접근 허용
.permitAll()
);
return httpSecurity.getOrBuild();
}
}
Config에서 로그인 성공 / 실패 시 유저에게 보낼 내용을 지정
로그인 성공 시 핸들러
package com.example.my.config.security.auth;
import com.example.my.common.dto.ResponseDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(objectMapper.writeValueAsString(
ResponseDTO.builder()
.code(0)
.message("로그인에 성공하였습니다.")
.build()
));
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(objectMapper.writeValueAsString(
ResponseDTO.builder()
.code(0)
.message("로그인에 성공하였습니다.")
.build()
));
}
}
로그인 실패 시 핸들러
package com.example.my.config.security.auth;
import com.example.my.common.dto.ResponseDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(objectMapper.writeValueAsString(
ResponseDTO.builder()
.code(1)
.message("아이디와 비밀번호를 정확히 입력해주세요.")
.build()
));
}
}
로그아웃 성공 시
package com.example.my.config.security.auth;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/auth/login");
}
}
유저의 정보를 저장할 UserDeatils 객체 생성
package com.example.my.config.security.auth;
import com.example.my.common.dto.LoginUserDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@AllArgsConstructor
@Getter
public class CustomUserDetails implements UserDetails {
private LoginUserDTO loginUserDTO;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return loginUserDTO.getUser().getRoleList()
.stream()
.map(role -> (GrantedAuthority) () -> "ROLE_" + role)
.toList();
}
@Override
public String getPassword() {
return loginUserDTO.getUser().getPassword();
}
@Override
public String getUsername() {
return loginUserDTO.getUser().getId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Repository에서 유저 정보를 받아와 UserDetails 유형으로 변환 후 반환
package com.example.my.config.security.auth;
import com.example.my.common.dto.LoginUserDTO;
import com.example.my.model.user.entity.UserEntity;
import com.example.my.model.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserEntity> userEntityOptional = userRepository.findByIdAndDeleteDateIsNull(username);
if (userEntityOptional.isEmpty()) {
throw new UsernameNotFoundException("아이디를 정확히 입력해주세요.");
}
return new CustomUserDetails(LoginUserDTO.of(userEntityOptional.get()));
}
}
설정을 토대로, Controller의 매개변수로 UserDetails를 받아와 Security에서 처리
package com.example.my.domain.todo.controller;
import com.example.my.common.dto.LoginUserDTO;
import com.example.my.common.exception.InvalidSessionException;
import com.example.my.config.security.auth.CustomUserDetails;
import com.example.my.domain.todo.dto.ReqTodoTableInsertDTO;
import com.example.my.domain.todo.dto.ReqTodoTableUpdateDoneYnDTO;
import com.example.my.domain.todo.service.TodoServiceApiV1;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/todo")
public class TodoControllerApiV1 {
private final TodoServiceApiV1 todoServiceApiV1;
@GetMapping
public ResponseEntity<?> getTodoTableData(@AuthenticationPrincipal CustomUserDetails customUserDetails) {
return todoServiceApiV1.getTodoTableData(customUserDetails.getLoginUserDTO());
}
@PostMapping
public ResponseEntity<?> insertTodoTableData(
@Valid @RequestBody ReqTodoTableInsertDTO dto,
// 유저 정보 받아옴
@AuthenticationPrincipal CustomUserDetails customUserDetails
) {
return todoServiceApiV1.insertTodoTableData(dto, customUserDetails.getLoginUserDTO());
}
@PutMapping("/{todoIdx}")
public ResponseEntity<?> updateTodoTableData(
@PathVariable Long todoIdx,
@Valid @RequestBody ReqTodoTableUpdateDoneYnDTO dto,
@AuthenticationPrincipal CustomUserDetails customUserDetails
) {
return todoServiceApiV1.updateTodoTableData(todoIdx, dto, customUserDetails.getLoginUserDTO());
}
@DeleteMapping("/{todoIdx}")
public ResponseEntity<?> deleteTodoTableData(
@PathVariable Long todoIdx,
@AuthenticationPrincipal CustomUserDetails customUserDetails
) {
return todoServiceApiV1.deleteTodoTableData(todoIdx, customUserDetails.getLoginUserDTO());
}
}