로그인 기능
userName, password를 입력받고 userName이 DB에 존재하는지, password가 일치하는지 확인한 후, 이상이 없다면 Token 발행
Talend 예상결과
EncrypterConfig
🔹 비밀번호 암호화를 위해 사용
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class EncrypterConfig {
@Bean
public BCryptPasswordEncoder encodePwd(){
return new BCryptPasswordEncoder(); // password를 인코딩 해줄때 쓰기 위함
}
}
🔴 SpringBoot가 3.0.0에서는 authorizeRequests()
, antMatchers()
의 기능을 사용하지 못하므로 다른 방법 필요 ➡ 해당 방법은 찾지 못하여 2.7.5 버전으로 디그레이드함.
SecurityConfig
🔹 비밀번호 암호화를 위해 사용
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.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers("/api/**").permitAll()
.antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll() // join, login은 언제나 가능
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt사용하는 경우 씀
.and()
// .addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
.build();
}
}
UserLoginRequest
🔹 userName, password를 입력받기 위해 만든 클래스
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class UserLoginRequest {
private String userName;
private String password;
}
UserLoginResponse
🔹 토큰을 리턴하기 위해 만든 클래스
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class UserLoginResponse {
private String token;
}
JwtTokenUtil
🔹 토큰을 생성하는 클래스
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtTokenUtil {
public static String createToken(String userName, String key, long expireTime) {
Claims claims = Jwts.claims(); // 일종의 map
claims.put("userName", userName);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
.signWith(SignatureAlgorithm.HS256, key)
.compact();
}
}
UserController
🔹 login 기능 추가
(userName, password를 입력받고 이상이 없다면 Response의 success 메소드를 통해 Token 리턴)
import com.hospitalreview.domain.dto.*;
import com.hospitalreview.service.UserService;
import com.hospitalreview.domain.Response;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
...
@PostMapping("/login")
public Response<UserLoginResponse> login(@RequestBody UserLoginRequest userLoginRequest) {
String token = userService.login(userLoginRequest.getUserName(), userLoginRequest.getPassword());
return Response.success(new UserLoginResponse(token));
}
}
@Value("${jwt.token.secret}")
jwt.token.secret
경로에 있는 값을 가져옴UserService
🔹 userName가 있는지, password가 일치하는지 확인 후 Token 발행하는 역할
import com.hospitalreview.domain.User;
import com.hospitalreview.domain.dto.UserDto;
import com.hospitalreview.domain.dto.UserJoinRequest;
import com.hospitalreview.exception.ErrorCode;
import com.hospitalreview.exception.HospitalReviewException;
import com.hospitalreview.repository.UserRepository;
import com.hospitalreview.utils.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder encoder;
@Value("${jwt.token.secret}")
private String secretKey;
private long expireTimeMs = 1000 * 60 * 60; // 1시간
...
public String login(String userName, String password) {
// userName 있는지 여부 확인
// 없으면 NOT_FOUND 에러 발생
User user = userRepository.findByUserName(userName)
.orElseThrow(() -> new HospitalReviewException(ErrorCode.NOT_FOUND, String.format("%s는 가입된 적이 없습니다.", userName)));
// password 일치 하는지 여부 확인
if(!encoder.matches(password, user.getPassword())) {
throw new HospitalReviewException(ErrorCode.INVALID_PASSWORD, "userName 또는 password가 잘못됐습니다.");
}
// 두가지 확인 중 예외 안났으면 Token 발행
return JwtTokenUtil.createToken(userName, secretKey, expireTimeMs);
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hospitalreview.domain.dto.UserDto;
import com.hospitalreview.domain.dto.UserJoinRequest;
import com.hospitalreview.exception.ErrorCode;
import com.hospitalreview.exception.HospitalReviewException;
import com.hospitalreview.service.UserService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Autowired
ObjectMapper objectMapper;
UserJoinRequest userJoinRequest = UserJoinRequest.builder()
.userName("naver")
.password("naver123")
.emailAddress("email.@naver.com")
.build();
...
@Test
@DisplayName("로그인 실패 - id 없음")
@WithMockUser
void login_fail1() throws Exception {
when(userService.login(any(), any())).thenThrow(new HospitalReviewException(ErrorCode.NOT_FOUND, ""));
// 무엇을 보내서 : id, pw
// 무엇을 받을까? : NOT_FOUND
mockMvc.perform(post("/api/v1/users/login")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userJoinRequest)))
.andDo(print())
.andExpect(status().isNotFound());
}
}
🔸 userService.login(any(), any())
: userName, password를 받아야하므로 2개
🔸 new HospitalReviewException(ErrorCode.NOT_FOUND, "")
, andExpect(status().isNotFound())
: ErrorCode 클래스에서 NOT_FOUND로 처리했음