국비학원 49일차 : Spring Boot_9

Digeut·2023년 5월 8일
0

국비학원

목록 보기
41/44

Auth API

build.gradle

dependency {
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation group: 'io.jsonwebtoken', name:'jjwt', version:'0.9.1'
}

provider - JwtProvider

@Component
public class JwtProvider {
    @Value("${jwt.secret-key}")
    private String SECRET_KEY;
    //스프링 부트의 @Value 어노테이션을 사용하여 application.properties 파일에서 
    //jwt.secret-key 프로퍼티 값을 가져와서 SECRET_KEY 필드에 할당합니다.

    public String create(String email){
	//email 매개변수를 받아서 JWT 문자열을 생성하는 메소드입니다.
    
        Date expirdeDate = 
            Date.from(Instant.now().plus(1, ChronoUnit.HOURS));
        //현재 시간에서 1시간을 더한 시간값을 계산합니다. 
        //이 시간값은 JWT의 만료 시간으로 사용됩니다.

        String jwt = 
            Jwts.builder() //JWT를 생성하기 위한 빌더 객체를 생성합니다.
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
           //생성할 JWT에 대한 서명 알고리즘과 서명에 사용될 비밀 키를 설정합니다.
                .setSubject(email)
           //JWT의 주체(Subject) 값을 설정합니다. 
           //이 값은 일반적으로 사용자의 식별자를 나타냅니다.
                .setIssuedAt(new Date())
                //JWT의 발급 시간을 설정합니다.
                .setExpiration(expirdeDate)  
                //JWT의 만료 시간을 설정합니다.
                .compact();
                //설정된 정보를 바탕으로 JWT 문자열을 생성합니다.
        return jwt; //jwt생성
    }

    public String validate(String jwt){
	//JWT 문자열을 검증하는 메소드입니다. 
	//jwt 매개변수로 받은 JWT 문자열이 유효한지 확인하고, 
    //주체(Subject) 값을 반환합니다.
    
        Claims claims =
            Jwts.parser()
            // JWT 문자열을 파싱하기 위한 파서 객체를 생성합니다.
                .setSigningKey(SECRET_KEY)
                //JWT 문자열의 서명에 사용된 비밀 키를 설정합니다.
                .parseClaimsJws(jwt)
                // 파싱할 JWT 문자열을 설정합니다.
                .getBody();
                 JWT에 포함된 클레임(Claim) 정보를 가져옵니다.
        
        return claims.getSubject();
		//JWT의 주체(Subject) 값을 반환합니다.
    }
}

filter-JwtAuthenticationFilter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter{
    
    private JwtProvider jwtProvider;
    
    //토큰 파싱
    private String parseToken(HttpServletRequest request){
	//HttpServletRequest 객체를 받아서 JWT를 추출하는 메소드
    
        String token = request.getHeader("Authorization");
       //HttpServletRequest에서 Authorization 헤더를 가져옵니다.
       
        ⭐boolean hasToken = 
            token != null && 
            !token.equalsIgnoreCase("null");
       //Authorization 헤더가 null이 아니고, 
        //"null"이 아닌 경우 true를 반환하는 변수를 선언합니다. 			
        //Authorization 헤더가 "null"일 경우를 체크하는 이유는, 
        //일부 서버에서 Authorization 헤더에 "null"을 지정하여 
        //JWT를 전달하는 경우가 있기 때문입니다.     
        
        if(!hasToken) return null;
        //Authorization 헤더가 없거나 "null"일 경우 null을 반환합니다.

        boolean isBearer = token.startsWith("Bearer ");
        //Authorization 헤더가 "Bearer "로 시작하는지 
        //체크하는 변수를 선언합니다. JWT의 일반적인 전송 방식은 				
        //"Bearer " + JWT 값입니다.
        
        if(!isBearer) return null;
		//Authorization 헤더가 "Bearer "로 시작하지 않는 경우 null을 반환합니다.
        
        String jwt = token.substring(7);
        //Authorization 헤더에서 "Bearer " 이후의 JWT 값을 추출합니다. 
        //이때, "Bearer "은 7글자이므로, 인덱스 7부터 추출합니다.
        return jwt;

    }

    @Autowired //의존성 주입
    public JwtAuthenticationFilter(JwtProvider jwtProvider){
        this.jwtProvider = jwtProvider;
    }

    @Override //빠른 수정 이용해서 작성함
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {

            String jwt = parseToken(request); //jwt를 가져옴

            boolean hasJwt = jwt != null;
            //JWT 문자열이 null인 경우에는 hasJwt 변수에 false가 할당됩니다. 
            //JWT 문자열이 null이 아닌 경우에는 hasJwt 변수에 true가 할당됩니다.
            
            if(!hasJwt) {
                filterChain.doFilter(request, response);
                return; //hasJwt가 false인 경우 함수 종료
            }

            String email = jwtProvider.validate(jwt); //서브젝트의 이메일 가져옴
            AbstractAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(email, null,AuthorityUtils.NO_AUTHORITIES);
            authenticationToken.setDetails(new WebAuthenticationDetailsSource());

            SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
            securityContext.setAuthentication(authenticationToken);
            SecurityContextHolder.setContext(securityContext);
            
        } catch (Exception exception) {
            exception.printStackTrace();
        }

        filterChain.doFilter(request, response);
        
    }
}

config - WebSecurityConfig

@EnableWebSecurity
@Configuration
public class WebSecurityConfig {
    
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    public WebSecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter){
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception{

        httpSecurity.cors().and()
                    .csrf().disable()
                    .httpBasic().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    .authorizeHttpRequests().antMatchers("/api/v1/**","/api/v2/auth/**","/api/v2/board/list","/api/v2/board/top3").permitAll()
                    .antMatchers(HttpMethod.GET,"/api/v2/board/*").permitAll()
                    .anyRequest().authenticated().and()
                    .exceptionHandling().authenticationEntryPoint(new FailedAuthenticationEntiryPoint());
		//HttpSecurity 객체를 통해 Spring Security 설정을 하고 있습니다. 
        cors() 메소드는 CORS 설정을 허용하도록 합니다. csrf() 메소드는 
        CSRF 공격 방지를 비활성화합니다. httpBasic() 메소드는 HTTP Basic 인증 
        방식을 사용하지 않도록 설정합니다. sessionCreationPolicy() 메소드는 세션을
        생성하지 않도록 설정합니다. antMatchers() 메소드는 지정한 경로에 대해 
        접근 권한을 설정합니다. anyRequest() 메소드는 다른 요청에 대해서는 인증을 
        필요로 하도록 설정합니다. exceptionHandling() 메소드는 예외 처리 핸들러를 
        설정합니다.
        
        httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //addFilterBefore() 메소드는 JwtAuthenticationFilter를
        UsernamePasswordAuthenticationFilter 전에 실행되도록 등록합니다. 
        JwtAuthenticationFilter는 헤더에 포함된 JWT 토큰을 이용해 인증을 처리합니다.

        return httpSecurity.build();
    }
}

controller - AuthController

@RestController
@RequestMapping("api/v2/auth")
public class AuthController {
    
    private AuthService authService;

    @Autowired //의존성 주입
    public AuthController(AuthService authService){
        this.authService = authService;
    }

    @PostMapping("sign-up")
    public ResponseEntity<ResponseDto> signUp(
        @Valid @RequestBody SignUpRequestDto requestBody
    ){
        ResponseEntity<ResponseDto> response =
        				authService.signUp(requestBody);
        return response;
    }

    @PostMapping("sign-in")
    public ResponseEntity<? super SignInResponseDto> signIn(
        @Valid @RequestBody SignInRequestDto requestBody
    ){
        ResponseEntity<? super SignInResponseDto> response = 
        				authService.signIn(requestBody);
        return response;
    }
}

SignUpRequestDto

@Data
@NoArgsConstructor
public class SignUpRequestDto { 
		//postUserRequestDto에서 그대로 가져옴
    @NotBlank
    @Email
    private String userEmail;
    @NotBlank
    private String userPassword;
    @NotBlank
    private String userNickname;
    @NotBlank
    @Pattern(regexp = "^\\d{3}-\\d{3,4}-\\d{4}$") 
    		//전화번호에 대한 패턴 커스텀 작업
    private String userPhoneNumber;
    @NotBlank
    private String userAddress;
    private String userProfileImageUrl;
}

SignInRequestDto

@Data
@NoArgsConstructor
public class SignInRequestDto {
    @NotBlank
    @Email
    private String userEmail;

    @NotBlank
    private String userPassword;

}

SignInResponseDto

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SignInResponseDto extends ResponseDto{
    
    private String token;
    private int expirationDate;

    public SignInResponseDto(String token){
        super("SU", "Success");
        this.token = token;
        this.expirationDate = 3600; //초단위 한시간
    }
}

CustomExceptionHandler

@RestControllerAdvice
public class CustomExceptionHandler {
    
    @ExceptionHandler(HttpMessageNotReadableException.class) 
    //특정한 예외 발생시 밑에 메서드 실행되게 해준다.
    public ResponseEntity<ResponseDto> handelrHttpMessageNotReadableException(HttpMessageNotReadableException exception){
        // ResponseDto responseBody = new ResponseDto("VF", "Request Parameter Validation Failed");
        // return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseBody);
        return CustomResponse.validationFaild();
    }

    @ExceptionHandler(MethodArgumentNotValidException.class) 
    //특정한 예외 발생시 밑에 메서드 실행되게 해준다.
    public ResponseEntity<ResponseDto> handelrMethodArgumentNotValidException(MethodArgumentNotValidException exception){

        return CustomResponse.validationFaild();
    }
}

service - AuthService

public interface AuthService {
    
    public ResponseEntity<ResponseDto> signUp(SignUpRequestDto dto);
    public ResponseEntity<? super SignInResponseDto> signIn(SignInRequestDto dto);
}

service - implement - AuthImplementService

@Service //입력하지 않으면 인식하지 못해서 오류가 발생할 수 있다.
public class AuthServiceImplement implements AuthService {
    
    //사용할 레포 올려두기
    private UserRepository userRepository;

    private JwtProvider jwtProvider;

    //BCryptPasswordEncoder 사용
    private PasswordEncoder passwordEncoder;

    @Autowired
    public AuthServiceImplement(UserRepository userRepository,⭐/*추가*/JwtProvider jwtProvider){
        this.userRepository = userRepository;
        this.passwordEncoder = new BCryptPasswordEncoder(); //이건 외부에서 받아오지 않고 직접만든다
        this.jwtProvider = jwtProvider;
    }

passwordEncoder 사용후 비밀번호는 암호화 처리된다

    //밑에 2개 빠른 수정으로 생성
    @Override
    public ResponseEntity<ResponseDto> signUp(SignUpRequestDto dto) {
        
        String email = dto.getUserEmail();
        String nickname = dto.getUserNickname();
        String phoneNumber = dto.getUserPhoneNumber();
        String password = dto.getUserPassword();

        try {

            // 존재하는 유저 이메일 반환
            boolean existedUserEmail = userRepository.existsByEmail(email);
            if(existedUserEmail) return CustomResponse.existUserEmail();

            // 존재하는 유저 닉네임 반환
            boolean existedUserNickname = userRepository.existsByNickname(nickname);
            if(existedUserNickname) return CustomResponse.existUserNickname();
            // 존재하는 유저 휴대폰 번호 반환
            boolean existedUserPhoneNumber = userRepository.existsByPhoneNumber(phoneNumber);
            if(existedUserPhoneNumber) return CustomResponse.existUserPhoneNumber();

            //암호화 작업
            String encodedPassword = passwordEncoder.encode(password);
            dto.setUserPassword(encodedPassword);

            //유저 레코드 삽입
            UserEntity userEntity = new UserEntity(dto);
            userRepository.save(userEntity);

        } catch (Exception exception) {
            exception.printStackTrace();
            return CustomResponse.databaseError();
        }

        return CustomResponse.success();
    }

    @Override
    public ResponseEntity<? super SignInResponseDto> signIn(SignInRequestDto dto) {
        
        SignInResponseDto body = null;

        String email = dto.getUserEmail();
        String password = dto.getUserPassword();


        try {

            // 로그인 실패 (이메일 틀림)
            UserEntity userEntity = userRepository.findByEmail(email);
            if(userEntity == null) return CustomResponse.signInFailed();

            // 로그인 실패 (패스워드 틀림)
            String encordedPassword = userEntity.getPassword();
            boolean equaledPassword = passwordEncoder.matches(password, encordedPassword);
            if(!equaledPassword) return CustomResponse.signInFailed(); //여기까지 성공이면 로그인 성공

로그인 실패시 화면

암호화 시키지 않는 아이디로 로그인시 화면
암호화 시키지 않은 아이디는 postUSer로 진행했으므로 로그인 되지 않는다.

암호화 시킨 아이디로 로그인시 성공으로 뜬다

암호화가 된 경우의 비밀번호 저장이 다르게 된다.

            //데이터 반환, 토큰을 생성해서 넣어줌
            String jwt = jwtProvider.create(email); //토큰 생성?
            body = new SignInResponseDto(jwt);
            
        } catch (Exception exception) {
            exception.printStackTrace();
            return CustomResponse.databaseError();
        }
        return ResponseEntity.status(HttpStatus.OK).body(body);
    }
}

인증 관련 다 진행 후 로그인 성공 시

post request의 key 입력 잘못했을때 화면

UserEntity에 Auth과정 추가

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "User")
@Table(name = "User") //DB랑 맵핑시켜줄때 사용
public class UserEntity {
    @Id
    private String email;
    private String password;
    private String nickname;
    private String phoneNumber;
    private String address;
    private boolean consentPersonalInformation;
    private String profileImageUrl;

    public UserEntity(PostUserRequestDto dto){
        this.email = dto.getUserEmail();
        this.password = dto.getUserPassword();
        this.nickname = dto.getUserNickname();
        this.phoneNumber = dto.getUserPhoneNumber();
        this.address = dto.getUserAddress();
        this.consentPersonalInformation = true;
        this.profileImageUrl = dto.getUserProfileImageUrl();
    }

    public UserEntity(⭐SignUpRequestDto dto){
        this.email = dto.getUserEmail();
        this.password = dto.getUserPassword();
        this.nickname = dto.getUserNickname();
        this.phoneNumber = dto.getUserPhoneNumber();
        this.address = dto.getUserAddress();
        this.consentPersonalInformation = true;
        this.profileImageUrl = dto.getUserProfileImageUrl();
    }
}
profile
개발자가 될 거야!

0개의 댓글