SpringBoot Security - 로그인 기능 (Token 발행)

고관운·2022년 11월 29일
0

SpringBoot Security - 로그인 기능

목적

로그인 기능
userName, password를 입력받고 userName이 DB에 존재하는지, password가 일치하는지 확인한 후, 이상이 없다면 Token 발행

Talend 예상결과

  • 성공했을 경우 : userName 중복 없음, result에는 보안으로 인해 password 제외하고 출력
  • 실패했을 경우
    1. 해당 userName이 DB에 없을 경우
    2. password가 일치하지 않는 경우

구현

Dependencies 추가

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를 인코딩 해줄때 쓰기 위함
    }
}

SercurityConfig 시 주의해야 할 점

🔴 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
🔹 토큰을 생성하는 클래스

  • userName : 토큰에 넣을 userName
  • key : 토큰을 열 수 있는 열쇠 역할
  • expireTime : 토큰 기한
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

@Value("${jwt.token.secret}")

  • yml의 jwt.token.secret 경로에 있는 값을 가져옴
  • 단, 해당 코드에서는 보안으로 인해 환경 변수를 사용함

UserService
🔹 userName가 있는지, password가 일치하는지 확인 후 Token 발행하는 역할

  • userName이 없다면 NOT_FOUND
  • password가 일치하지 않다면 INVALID_PASSWORD
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);
    }
}

UserController Test 구현

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로 처리했음

0개의 댓글