본문 바로가기

스프링boot/정리

Spring MVC) 사용자를 만들어 보자 2

사용자를 만들었으나 회원가입과 로그인 기능이 필요하다.

로그인을 하여 게시글, 댓글 등의 접근권한도 얻을 것이다.

 

그래서 오늘은 Spring Securty + JWT를 사용하여 인증/인가 하는걸 보여주겠다.

 

곰곰히 생각해 봤는데 블로그에 장대하게 쓰는것 보단 주석을 보는게 가독성이 좋을 것 같아 코드마다 전부 주석처리를 해서 이게 왜 필요한지 적어 놨으니 코드를 살펴 보자.

 

@Configuration //설정파일 만드는 거다.
@EnableWebSecurity(debug = true) //Spring security 라고 선언
public class SecurityConfiguration {
    private final JwtTokenizer jwtTokenizer; // jwt 생성 검증 할때 사용할꺼임
    private final CustomAuthorityUtils authorityUtils; // 권한 처리용으로 가져옴 해당 클래스 보는게 도움될꺼임

    public SecurityConfiguration(JwtTokenizer jwtTokenizer,
                                 CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http //아래는 필터 처리인데 내가 원하는 동작을 집어 넣는거다.
                .headers().frameOptions().sameOrigin()
                .and()
                .csrf().disable() // 프론트엔드에서 리엑트로 같이 협업할때는 활성화 해야됨(위조체크임)
                .cors(withDefaults()) //동일출처라고 해킹방지임. 같은 브라우저에서 요청이 왔는지 체크하는것임.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                //이건 세션 활성화 하는건데 나는 JWT를 사용할꺼라 STATELESS로 설정했음(그럼 jwt 사용함)
                .and()
                .formLogin().disable() //스프링Security는 기본 로그인 폼(프론트엔드)이 있는데 H2쓸때 걸리적 거려서 disable 했음.
                .httpBasic().disable()  //http 인증 방식인데 나는 jwt인증 방식을 쓸거라서 비활성화 했다.
                .exceptionHandling()//인가 할때 아래 필터들 쓸꺼다.
                .authenticationEntryPoint(new MemberAuthenticationEntryPoint())
                //위에는 성공하면 위의 MemberAuthenticationEntryPoint() 호출할꺼고
                .accessDeniedHandler(new MemberAccessDeniedHandler())
                //실패하면 MemberAccessDeniedHandler() 호출할꺼임.
                .and()
                .apply(new CustomFilterConfigurer())
                //맨 밑에있는 CustomFilterConfigurer() 불러올때 쓸꺼다.
                .and()
                .authorizeHttpRequests(authorize -> authorize //여기는 인가 하는 곳인데 hasRole이 역할인데 나는
                        //회원가입한 사람을 MEMBER라고 역할 부여 했다. 다 중복이라 몇개만 설명하면
                        .antMatchers(HttpMethod.DELETE, "/**").hasRole("MEMBER") //모든 DELETE는 MEMBER만
                        .antMatchers(HttpMethod.PATCH, "/**").hasRole("MEMBER") //마찬가지
                        .antMatchers("/api/members/signup").permitAll() //누구나 해당 api 접속 가능
                        .antMatchers("/api/members/login").permitAll()
                        .antMatchers("/members/**").hasRole("MEMBER")
                        .antMatchers("/api/boards/post").hasRole("MEMBER")
                        .antMatchers(HttpMethod.GET, "/boards/**").permitAll()
                        .antMatchers(HttpMethod.POST, "/api/replies/**").hasRole("MEMBER")
                        .antMatchers(HttpMethod.GET, "/api/replies/**").permitAll()
                        .antMatchers(HttpMethod.POST, "/api/likes/**").hasRole("MEMBER")
                        .anyRequest().permitAll()


                );
        return http.build();
    }

    @Bean // 이걸 넣어줘야 비밀번호를 암호화 해줌
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean //아까 위에서 언급한 CORS 설정하는 부분이다.
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration(); //CORS는 configuration 여기에 담길꺼다.
        configuration.setAllowedOrigins(Arrays.asList("*")); //모든 도메인에서 요청 가능하고
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE")); //요청 방식은 4가지로 정의하고
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();//이걸 source 에 담는다.
        source.registerCorsConfiguration("/**", configuration); //어플의 모든곳에 CORS가 적용될꺼다.
        return source; //이걸 다시 configuration에 담을꺼다.
    }


    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
        //여기는 스프링 security에서 내 어플 요구에 따라 커스텀을 해서 사용할려고 만들었고 HttpSecurity의 기본설정을 조작할꺼다.
        @Override
        public void configure(HttpSecurity builder) throws Exception {
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
            //위에는 인증관리자임 가져오고
            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
            //JWT 토큰을 생성하고 반환하는 역할임
            jwtAuthenticationFilter.setFilterProcessesUrl("/members/login");
            //필터가 "/members/login" 에서 작동하게 할꺼임.
            jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
            //인증 성공시 작동
            jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
            //인증 실패시 작동

            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);
            //아래 까지 JwtVerificationFilter를 JwtAuthenticationFilter 다음에 실행되도록 하는거임 인증 - 인가 순서니깐


            builder
                    .addFilter(jwtAuthenticationFilter)
                    .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
        }
    }
}

 

 

 

@Data//로그인 할때 쓸꺼고 Data는 getter setter 다 담고 있다해서 써봄 별 의미는 없음.
public class LoginDto {
    private String email;
    private String password;
}

 

 

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    //로그인 요청 처리 할려고 만들었음
    private final AuthenticationManager authenticationManager;
    private final JwtTokenizer jwtTokenizer;

    @SneakyThrows //자동 예외 처리 도구임 알아서 만들고 처리함.
    @Override //여기는 HttpServletRequest 에서 로그인 정보를 읽어오는 메서드다.
    //UsernamePasswordAuthenticationToken 생성하고 인증 시도할꺼임.
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        ObjectMapper objectMapper = new ObjectMapper();
        LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());

        return authenticationManager.authenticate(authenticationToken);
    }

    @Override// JWT 액세스 토큰과 리프레시 토큰을 생성하고, HTTP 응답 헤더에 토큰을 추가할꺼임 포스트맨으로 보여드림
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws ServletException, IOException {
        Member member = (Member) authResult.getPrincipal();

        String accessToken = delegateAccessToken(member);
        String refreshToken = delegateRefreshToken(member);

        response.setHeader("Authorization", "Bearer " + accessToken);
        response.setHeader("Refresh", refreshToken);

        this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);  // 추가
    }


    //아래는 엑세스 토큰 만들때 쓸꺼고
    private String delegateAccessToken(Member member) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("memberId", member.getMemberId());  // 식별자도 포함할 수 있다.
        claims.put("username", member.getEmail());
        claims.put("roles", member.getRoles());

        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());

        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);

        return accessToken;
    }

    // 아래는 리프레시 토큰 만들때 쓸꺼다.
    private String delegateRefreshToken(Member member) {
        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);

        return refreshToken;
    }
}

 

 

@RequiredArgsConstructor //여기는 만든 토큰 검증하는 곳임
public class JwtVerificationFilter extends OncePerRequestFilter {
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;

    @Override //JWT 토큰을 검증하고 토큰에서 사용자 정보와 권한을 가져올때 쓸꺼임
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // System.out.println("# JwtVerificationFilter");

        try {
            Map<String, Object> claims = verifyJws(request);
            setAuthenticationToContext(claims);
        } catch (SignatureException se) {
            request.setAttribute("exception", se);
        } catch (ExpiredJwtException ee) {
            request.setAttribute("exception", ee);
        } catch (Exception e) {
            request.setAttribute("exception", e);
        }

        filterChain.doFilter(request, response);
    }

    @Override //인증 헤더가 없거나 Bearer로 시작하지 않는 경우 필터를 건너뛸꺼임 이따 토큰 보여드림
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String authorization = request.getHeader("Authorization");

        return authorization == null || !authorization.startsWith("Bearer");
    }

    //HttpServletRequest에서 로그인 정보를 읽어오는데 JWT 토큰 추출 할려고 만든거임 그리고 검증까지 해주고
    private Map<String, Object> verifyJws(HttpServletRequest request) {
        String jws = request.getHeader("Authorization").replace("Bearer ", "");
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
        Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();

        return claims;
    }


    //위에서 claims로 반환했잖음 그걸로 인증정보를 생성할꺼임.
    private void setAuthenticationToContext(Map<String, Object> claims) {
        String username = (String) claims.get("username");
        List<GrantedAuthority> authorities = authorityUtils.createAuthorities((List)claims.get("roles"));
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
}

 

 

 

@Slf4j //로그인 했는데 접근 권한 없는곳에 갈때 (다른 사람 게시글 지우기)
public class MemberAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ErrorResponder.sendErrorResponse(response, HttpStatus.FORBIDDEN);
        log.warn("Forbidden error happened: {}", accessDeniedException.getMessage());

    }
}

 

 

@Slf4j
@Component //로그인도 안했는데 글 작성할때.
public class MemberAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Exception exception = (Exception) request.getAttribute("exception");
        ErrorResponder.sendErrorResponse(response, HttpStatus.UNAUTHORIZED);

        logExceptionMessage(authException, exception);
    }

    private void logExceptionMessage(AuthenticationException authException, Exception exception) {
        String message = exception != null ? exception.getMessage() : authException.getMessage();
        log.warn("Unauthorized error happened: {}", message);
    }
}

 

@Slf4j //나는 로그인할때 이메일이랑 비번을 사용하게 만들었는데 잘못입력하면 사용될꺼다.
public class MemberAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        // 인증 실패 시, 에러 로그를 기록하거나 error response를 전송할 수 있다.
        log.error("# Authentication failed: {}", exception.getMessage());

        sendErrorResponse(response);
    }

    private void sendErrorResponse(HttpServletResponse response) throws IOException {
        Gson gson = new Gson();
        ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
    }
}

 

 

@Slf4j //인증 성공하면 뜰꺼다.
public class MemberAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        // 인증 성공 후, 로그를 기록하거나 사용자 정보를 response로 전송하는 등의 추가 작업을 할 수 있다.
        log.info("# Authenticated successfully!");
    }
}

 

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenizer {//jwt 생성, 검증 및 토큰 정보를 처리하는 클래스
    @Getter
    @Value("${jwt.key}")
    private String secretKey;

    @Getter
    @Value("${jwt.access-token-expiration-minutes}")
    private int accessTokenExpirationMinutes;

    @Getter
    @Value("${jwt.refresh-token-expiration-minutes}")
    private int refreshTokenExpirationMinutes;

    public String encodeBase64SecretKey(String secretKey) { //비밀키를 Base64로 인코딩
        return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    public String generateAccessToken(Map<String, Object> claims, //주어진 정보를 사용하여 액세스 토큰을 생성
                                      String subject,
                                      Date expiration,
                                      String base64EncodedSecretKey) {
        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(Calendar.getInstance().getTime())
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    //주어진 정보를 사용하여 리프레시 토큰을 생성
    public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey) {
        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(Calendar.getInstance().getTime())
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    // 검증 후, Claims을 반환 하는 용도
    public Jws<Claims> getClaims(String jws, String base64EncodedSecretKey) {
        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        Jws<Claims> claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jws);
        return claims;
    }

    // JWS를 사용하여 토큰의 서명을 검증
    public void verifySignature(String jws, String base64EncodedSecretKey) {
        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jws);
    }

    //토큰의 만료 시간을 설정
    public Date getTokenExpiration(int expirationMinutes) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, expirationMinutes);
        Date expiration = calendar.getTime();

        return expiration;
    }

    // Base64로 인코딩된 비밀키를 Key 객체로 변환
    private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) {
        byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
        Key key = Keys.hmacShaKeyFor(keyBytes);

        return key;
    }
}

 

 

@Service
@RequiredArgsConstructor //사용자 인증 정보를 로드하려고 만듬
public class MemberDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final CustomAuthorityUtils authorityUtils;

    @Override //이메일 사용해서 사용자 정보 가져옴
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Optional<Member> optionalMember = memberRepository.findByEmail(email);
        Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

        return new MemberDetails(findMember);
    }


    //UserDetails라고 엔티티랑 물려서 UserDetails 타입으로 저장해 주는거임 스프링 security는 UserDetails 폼으로 사용자를 저장함.
    private final class MemberDetails extends Member implements UserDetails {
        MemberDetails(Member member) {
            setMemberId(member.getMemberId());
            setEmail(member.getEmail());
            setNickname(member.getNickname());
            setPassword(member.getPassword());
            setRoles(member.getRoles());
        }

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorityUtils.createAuthorities(this.getEmail());
        }

        @Override
        public String getUsername() {
            return getEmail();
        }

        @Override
        public boolean isAccountNonExpired() {
            return true;
        }

        @Override
        public boolean isAccountNonLocked() {
            return true;
        }

        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }

        @Override
        public boolean isEnabled() {
            return true;
        }
    }
}

 

 

@Component //이건 권한 처리 할려고 만듬
public class CustomAuthorityUtils {
    //GrantedAuthority가 인증된 사용자 정보 나타낼때 쓰이는 인터페이스다.
    private final List<GrantedAuthority> MEMBER_ROLES =
            AuthorityUtils.createAuthorityList("ROLE_MEMBER");//약속임 ROLE_MEMBER라고 서버내 에서 이름 짓는게


    //이메일 기준으로 권한 넣어줄꺼임
    public List<GrantedAuthority> createAuthorities(String email) {
        return MEMBER_ROLES;
    }

    ///권한을 스프링이 인식 하는데 사용될꺼임.
    public List<GrantedAuthority> createAuthorities(List<String> roles) {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
    }

    //여기서 원래는 ROLE_MEMBER로 DB에 저장되어야 하는데 깔끔하게 앞에 ROLE 삭제시킴 이유는 위에 설명
    public List<String> createRoles(String email) {
        return MEMBER_ROLES.stream()
                .map(a -> a.getAuthority()
                        .replaceAll("ROLE_", ""))
                .collect(Collectors.toList());
    }
}

 

 

public class ErrorResponder { //에러 응답 생성기임 만들었던 Handler들이랑 쓰일꺼임
    public static void sendErrorResponse(HttpServletResponse response, HttpStatus status) throws IOException {
        Gson gson = new Gson();
        ErrorResponse errorResponse = ErrorResponse.of(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(status.value());
        response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
    }
}

 

 

@Getter //전역 예외 처리 할려고 만들었다 여기말고 GlobalExceptionAdvice를 봐야한다.
public class ErrorResponse {
    private int status;
    private String message;
    private List<FieldError> fieldErrors;
    private List<ConstraintViolationError> violationErrors;

    private ErrorResponse(int status, String message) {
        this.status = status;
        this.message = message;
    }

    private ErrorResponse(final List<FieldError> fieldErrors,
                          final List<ConstraintViolationError> violationErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }

    public static ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of(bindingResult), null);
    }

    public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
        return new ErrorResponse(null, ConstraintViolationError.of(violations));
    }

    public static ErrorResponse of(ExceptionCode exceptionCode) {
        return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
    }

    public static ErrorResponse of(HttpStatus httpStatus) {
        return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase());
    }

    public static ErrorResponse of(HttpStatus httpStatus, String message) {
        return new ErrorResponse(httpStatus.value(), message);
    }

    @Getter
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;

        private FieldError(String field, Object rejectedValue, String reason) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<FieldError> of(BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors =
                    bindingResult.getFieldErrors();
            return fieldErrors.stream()
                    .map(error -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ?
                                    "" : error.getRejectedValue().toString(),
                            error.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }

    @Getter
    public static class ConstraintViolationError {
        private String propertyPath;
        private Object rejectedValue;
        private String reason;

        private ConstraintViolationError(String propertyPath, Object rejectedValue,
                                         String reason) {
            this.propertyPath = propertyPath;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<ConstraintViolationError> of(
                Set<ConstraintViolation<?>> constraintViolations) {
            return constraintViolations.stream()
                    .map(constraintViolation -> new ConstraintViolationError(
                            constraintViolation.getPropertyPath().toString(),
                            constraintViolation.getInvalidValue().toString(),
                            constraintViolation.getMessage()
                    )).collect(Collectors.toList());
        }
    }
}

 

 

@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {

    @ExceptionHandler //이메일 이상하게 입력하면 뜰꺼임
    public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ErrorResponse.of(e.getBindingResult()));
    }

    @ExceptionHandler // 글작성을 최소 얼마 이상 해야하는데 안하면 뜰꺼임
    public ResponseEntity handleConstraintViolationException(ConstraintViolationException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ErrorResponse.of(e.getConstraintViolations()));
    }

    @ExceptionHandler // 로그인했는데 또 로그인 하면 이거 뜰꺼다.
    public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
        return ResponseEntity.status(HttpStatus.valueOf(e.getExceptionCode().getStatus()))
                .body(ErrorResponse.of(e.getExceptionCode()));
    }

    @ExceptionHandler //POST 해야 되는데 PATCH나 DELETE 같은거 하면 뜬다.
    public ResponseEntity handleHttpRequestMethodNotSupportedException(
            HttpRequestMethodNotSupportedException e) {
        return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
                .body(ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED));
    }

    @ExceptionHandler //비번 입력 안하고 전송하면 뜰꺼다.
    public ResponseEntity handleMissingServletRequestParameterException(
            MissingServletRequestParameterException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ErrorResponse.of(HttpStatus.BAD_REQUEST, e.getMessage()));
    }

    @ExceptionHandler //DB 오류 뜨면 뜰꺼다.
    public ResponseEntity handleException(Exception e) {
        log.error("# handle Exception", e);

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR));
    }
}

 

 

 

Security + JWT 부분은 여기까지다. 여기서 중요한건 토큰을 만드는것인데 글로 아무리 설명해도 이해가 어려울테니 바로 Postman 화면으로 보자.

 

 

 

 

Postman에서 회원가입을 해보았다.

 

DB에 정상적으로 값이 들어간다.

 

 

 

로그인을 하니 토큰 생성이 되었다. 이젠 이 토큰으로 사용자 인증정보를 내가 가지고 있으며 인가를 얻는것이다.

 

토큰을 넣지 않고 게시판 Post 요청을 해보았다 당연히 토큰 기입이 안되었기에 인가를 못받고 작성이 안된다 하지만.

 

 

헤더에 토큰을 넣어주니 게시글 생성이 정상적으로 이루어 진다. (참고로 게시판 DB에서 Count 부분들은 새로 추가한거라 나중에 설명하겠다.)

 

 

 

토큰을 추가하니깐 댓글이 정상 등록 되는걸 확인할수 있다.

 

여기까지는 Member1이였고 Member2도 만들어서 댓글을 달아볼까

 

혹시 궁금한 사람 있을까봐 스샷 올리면 당연하지만 비번이 틀리면 로그인이 안된다.

 

이또한 이미 예상가겠지만 다른 사람 토큰을 넣어도 안된다. 로그인한 사용자의 토큰과 서버에서 유저 식별정보가 완벽히 일치해야 등록을 하던 삭제를 하던 할수 있다.

Bearer eyJhbGciOiJIUzM4NCJ9.eyJyb2xlcyI6WyJNRU1CRVIiXSwibWVtYmVySWQiOjIsInVzZXJuYW1lIjoidXNlcjJAZ21haWwuY29tIiwic3ViIjoidXNlcjJAZ21haWwuY29tIiwiaWF0IjoxNjgyMDQ3Mzc4LCJleHAiOjE2ODIwNDkxNzh9.oJxlNlWoo-FUbd5DrO_w5yIEh41Wl7755RD8mJhsPf4cMzf2DTP-htfqXWCnvRX2

 

Bearer eyJhbGciOiJIUzM4NCJ9.eyJyb2xlcyI6WyJNRU1CRVIiXSwibWVtYmVySWQiOjMsInVzZXJuYW1lIjoidXNlcjNAZ21haWwuY29tIiwic3ViIjoidXNlcjNAZ21haWwuY29tIiwiaWF0IjoxNjgyMDQ3NzU3LCJleHAiOjE2ODIwNDk1NTd9.FD2cN97Ym4alqu3A4afOiyYnhUaOpjO1kv7XMnJ4AxiRvYUEk7yso2Pj6JtkN7ZB

 

헷갈리는 사람 있을수 있다. Postman에서 생략되어 토큰들이 비슷해 보이지만 위처럼 다르다.

 

댓글이 정상적으로 두명이 작성한것으로 보인다.

 

참고로 Security.config 에서 설정한 내용처럼 로그인, 회원가입, 게시판, 댓글 확인은 누구나 접근 가능하게 설정하였다. 

 

새로 회원가입을 하면 자동적으로 MEMBER 라는 권한을 부여하는데 이게 있어야 글쓰기, 좋아요, 삭제 등등 다른 기능들을 이용할수 있다.

 

확인해 볼까

 

 

 

아무런 토큰을 넣지 않아도 댓글 목록이 페이지네이션 처리되어 정상 호출하는걸 확인할수 있다.

 

당연히 게시글 목록도 인증없이 누구에게나 인가된다.

 

 

게시글을 수정도 해보자.

 

이것도 마찬가지로 토큰이 안맞으면 안되는데.

 

토큰을 정상적으로 넣으니 된다.

 

 

 

 

다음에는 Count 코드를 보여주겠다. 그리고 전체적으로 빠진 설명들을 채워가는 시간을 가지겠다.