728x90

목표

Spring Boot에서 JPA 사용하기

 

환경

  • Framework : Spring Boot 2.6.7
  • Build : Gradle 6.9.2
  • JDK : JDK11

 

할 것 요약

1. build.gradle에 JPA dependency 추가하기

2. application.yml에 JPA 설정 추가하기

3. JPA Entity 생성

4. JPA Repository 생성

5. JPA CRUD API 만들어보기

 

해보기

1. build.gradle에 JPA dependency 추가하기

dependencies {
    // JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

 

2. application.yml에 JPA 설정 추가하기

spring:
  ## JPA Setting Info
  jpa:
    hibernate:
      ddl-auto: create  # option type: create, create-drop, update, validate, none
    properties:
      hibernate:
        diarect: org.hibernate.dialect.H2Dialect  # 쿼리의 기준이 되는 데이터베이스 엔진을 설정합니다.
        show_sql: true  # sql 쿼리를 보여줍니다.
        format_sql: true  # sql query formatting
        use_sql_comments: true  # sql 쿼리의 추가정보를 보여줍니다.

logging:
  level:
    org:
      hibernate:
        type:
          descriptor:
            sql: trace  # query의 ?에 어떤 값이 들어가는지 추적할 수 있는 설정입니다. TRACE Log에 값을 보여줍니다.

spring.jpa.hibernate.ddl-auto 옵션

create 애플리케이션 실행 시 테이블을 모두 제거하고 다시 생성합니다. (drop & create)
create-drop 애플리케이션 실행 시 테이블을 모두 제거하고 다시 생성합니다. 그리고 애플리케이션 종료 시점에 테이블을 모두 제거합니다. (drop & create & drop)
update 애플리케이션 실행 시 변경점만 반영됩니다.
validate 현재 테이블 정보가 entity에 정의된 내용과 동일한지 체크합니다. 다를 경우 경고를 출력하며 애플리케이션을 실행시키지 않습니다.
none 자동생성을 사용하지 않습니다.

 

spring.jpa.properties.hibernate.show_sql: true

 

spring.jpa.properties.hibernate.format_sql: true

 

spring.jpa.properties.hibernate.use_sql_comments: true

 

logging.level.org.hibernate.type.descriptor.sql: trace 설정

 

3. JPA Entity 생성

// Member.java

package com.herojoon.jpaproject.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.sun.istack.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

import javax.persistence.*;
import java.util.Date;

@SuperBuilder
@NoArgsConstructor
@Setter
@Getter
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})  // JPA에서 lazy관련 에러 날 경우 사용
@Entity  // 객체와 테이블 매핑
@Table(name = "MEMBER")  // 테이블 지정
public class Member {
    @Id  // Primary Key 지정
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // AUTO_INCREMENT 설정 (id값이 null일 경우 자동 생성)
    @Column(name = "ID")  // 컬럼 지정
    private Long id;

    @NotNull
    @Column(name = "NAME")
    private String name;

    @NotNull
    @Column(name = "email")
    private String email;

    @Column(name = "NICKNAME")
    private String nickname;

    @Column(name = "AGE")
    private Integer age;

    @Column(name = "BIRTHDAY")
    private Date birthday;
}

Entity에 정의한 내용으로 테이블이 생성됩니다.

 

4. JPA Repository 생성

// MemberRepository.java

package com.herojoon.jpaproject.repository;

import com.herojoon.jpaproject.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {  // JpaRepository를 상속하여 사용. <객체, ID>
}

 

5. JPA CRUD API 만들어보기

// MemberController.java

package com.herojoon.jpaproject.controller;

import com.herojoon.jpaproject.entity.Member;
import com.herojoon.jpaproject.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

@RequiredArgsConstructor
@RequestMapping("api/member")
@RestController
public class MemberController {

    private final MemberService memberService;

    /**
     * Member 생성
     *
     * @return
     * @throws ParseException
     */
    @PostMapping("create")
    public ResponseEntity<Member> createMember() throws ParseException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        Date date = format.parse("2011-12-03");
        Member member = Member.builder()
                .name("herojoon")
                .email("herojoon432@gmail.com")
                .nickname("heroble")
                .age(10)
                .birthday(date)
                .build();
        Member savedMember = memberService.createMember(member);
        return new ResponseEntity<>(savedMember, HttpStatus.OK);
    }

    /**
     * Member 수정
     *
     * @return
     * @throws ParseException
     */
    @PutMapping("update")
    public ResponseEntity<Member> updateMember() throws ParseException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        Date date = format.parse("2011-12-03");
        Member member = Member.builder()
                .id(1l)
                .name("herojoon2")
                .email("herojoon432@gmail.com")
                .nickname("heroble2")
                .age(10)
                .birthday(date)
                .build();
        Member updatedMember = memberService.updateMember(member);
        if (!ObjectUtils.isEmpty(updatedMember)) {
            return new ResponseEntity<>(updatedMember, HttpStatus.OK);
        } else {
            return new ResponseEntity<>(member, HttpStatus.NOT_FOUND);
        }
    }

    /**
     * Member List 조회
     *
     * @return
     */
    @GetMapping("list")
    public ResponseEntity<List<Member>> getMembers() {
        List<Member> members = memberService.getMembers();
        return new ResponseEntity<>(members, HttpStatus.OK);
    }

    /**
     * Id에 해당하는 Member 조회
     *
     * @param id
     * @return
     */
    @GetMapping("{id}")
    public ResponseEntity<Member> getMember(
            @PathVariable("id") Long id) {
        Member member = memberService.getMember(id);
        return new ResponseEntity<>(member, HttpStatus.OK);
    }

    /**
     * Id에 해당하는 Member 삭제
     *
     * @param id
     * @return
     */
    @DeleteMapping("{id}")
    public ResponseEntity<Long> deleteMember(
            @PathVariable("id") Long id) {
        memberService.deleteMember(id);
        return new ResponseEntity<>(id, HttpStatus.OK);
    }
}
// MemberService.java

package com.herojoon.jpaproject.service;

import com.herojoon.jpaproject.entity.Member;
import com.herojoon.jpaproject.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    /**
     * Member 생성
     *
     * @param member
     * @return
     */
    public Member createMember(Member member) {
        Member savedMember = memberRepository.save(member);  // JpaRepository에서 제공하는 save() 함수
        return savedMember;
    }

    /**
     * Member 수정
     * JPA Repository의 save Method를 사용하여 객체를 업데이트 할 수 있습니다.
     * Entity Member에 @Id로 설정한 키 값이 존재할 경우 해당하는 데이터를 업데이트 해줍니다.
     * 만약 수정하려는 Entity Member 객체에 @Id 값이 존재하지 않으면 Insert 되기 때문에
     * 아래와 같이 업데이트 하고자 하는 Member가 존재하는지 체크하는 로직을 추가하였습니다.
     *
     * @param member
     * @return
     */
    public Member updateMember(Member member) {
        Member updatedMember = null;
        try {
            Member existMember = getMember(member.getId());
            if (!ObjectUtils.isEmpty(existMember)) {
                updatedMember = memberRepository.save(member);  // JpaRepository에서 제공하는 save() 함수
            }
        } catch (Exception e) {
            log.info("[Fail] e: " + e.toString());
        } finally {
            return updatedMember;
        }
    }

    /**
     * Member List 조회
     * 
     * @return
     */
    public List<Member> getMembers() {
        return memberRepository.findAll();  // JpaRepository에서 제공하는 findAll() 함수
    }

    /**
     * Id에 해당하는 Member 조회
     * 
     * @param id
     * @return
     */
    public Member getMember(Long id) {
        return memberRepository.getById(id);  // JpaRepository에서 제공하는 getById() 함수
    }

    /**
     * Id에 해당하는 Member 삭제
     * 
     * @param id
     */
    public void deleteMember(Long id) {
        memberRepository.deleteById(id);  // JpaRepository에서 제공하는 deleteById() 함수
    }
}
 
728x90

기존 데스크탑에서 자바어플리케이션 개발 및 배포를 해오다 얼마전부터 데스크탑의 맛탱이가는 증세를 보고 개발환경을 노트북으로 바꾸는 과정이었다.

jdk최신버전 설치후 STS4설치하고 war import해서 로컬에서 빌드하니 정상적으로 나왔다. 서버쪽 수정 후 서버에 배포하니 웹서비스들이 미쳐돌아가고 있었다. 하나는 500에러를 내뿜고 하나는 고양이만 나오고 있었다. 분명 심각한 문제가 발생한 것이다.

톰캣 로그를 보았다.

Caused by: java.lang.UnsupportedClassVersionError: kr/pe/innu/stock/dao/StockDaoImpl has been compiled by a more recent version of the Java Runtime (class file version 61.0), 
this version of the Java Runtime only recognizes class file versions up to 52.0 
(클래스 [kr.pe.innu.stock.dao.StockDaoImpl]을(를) 로드할 수 없습니다)
...
org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [kr.pe.innu.stock.service.StockServiceImpl] for bean with name 'stockService' defined in ServletContext resource 
[/WEB-INF/spring/appServlet/servlet-context.xml]: problem with class file or dependent class; nested exception is java.lang.UnsupportedClassVersionError: kr/pe/innu/stock/service/StockServiceImpl has been compiled by a more recent version of the Java Runtime 
(class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0 
(클래스 [kr.pe.innu.stock.service.StockServiceImpl]을(를) 로드할 수 없습니다)

대충보니 클래스파일은 version 61로 되어 있는데 이 시스템은 version 52까지 인식하는거 같다.

노트북에 jdk 최신버전(J2SE 17)을 설치했었는데 아무래도 이게 문제인거 같다. 우선 개발환경과 배포환경의 jdk버전을 맞추는게 필요했다. 최신버전보다는 이전버전으로 개발환경의 jdk(J2SE8)를 다시 설치했다.

그리고 STS의 컴파일러 적용버전을 바꿔주었다.

STS를 다시 켜서 빌드를 하고 클래스버전을 확인하니 배포서버의 버전과 같았고 정상 배포됨을 확인했다.

자바 클래스 버전은 jdk1.1이 메이저버전 45 이고 이후 1씩 증가한다. 나의 배포서버는 Centos7이며 J2SE8(메이저버전 52)이고 현재 최신버전은 J2SE17(메이저버전 61)이다.

728x90

아니 뭐 그래.. 이걸 가지고 생색낼 건 아니다만.. 그래도 짜는데 얼마 안걸렸어?

다만 이게 대용량 트래픽 고려하고 그럴꺼 같으면 캐싱도 고려하고 그럴려면 Redis 도 넣고 이것저것 해야했단 말이야?

 

근데 In-memory DB 하라그래서 일단 H2 하긴 했는데 Redis 를 언제 하냐고.. 그래서 한 3~4시간만에 구현하고 사전과제는 제출했는데 ... 아 카카오뱅크 사전과제 너란녀석..

 

그래 카뱅은 안녕하는거로 하자

'Java > Spring Boot JPA' 카테고리의 다른 글

Spring Boot에서 JPA 사용하기  (0) 2023.05.15
JAVA CLASS 버전차이에 의한 오류 및 해결  (0) 2023.05.15
Spring JWT (Json Web Token)  (0) 2022.08.15
Hibernate vs JPA vs Spring Data JPA  (0) 2022.06.07
JPA의 DTO와 Entity  (0) 2022.01.10
728x90

개발 스펙

  • Java(11)
  • Amazon Corretto JDK(11)
  • Spring Boot(2.5.3)
  • Spring Security(boot-starer)
  • JWT(0.9.1)

Config

1. Dependency 추가

Spring Security와 JWT의 Dependency 추가

dependencies {
	...
    
	// Spring Security
    implementation "org.springframework.boot:spring-boot-starter-security"
    // Spring Security Test
	testImplementation 'org.springframework.security:spring-security-test'
    // JWT
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    
	...
}

2. 정적 자원 제공 클래스 생성

정적 자원을 제공하는 클래스를 생성하여 아래와 같이 설정한다.

@Configuration 
public class WebMvcConfig implements WebMvcConfigurer {

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/static/",
            "classpath:/public/",
            "classpath:/",
            "classpath:/resources/",
            "classpath:/META-INF/resources/",
            "classpath:/META-INF/resources/webjars/"
    };

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 경로에 해당하는 url을 forword
//        registry.addViewController("/loginPage").setViewName("login");
        // 우선순위를 가장 높게 잡는다.
//        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 정적 자원의 경로를 허용
        registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
    }

}

3. SpringSecurity 설정

스프링 시큐리티의 웹 보안 기능을 초기화 및 설정

WebSecurityConfigurerAdapter를 상속받아 HttpSecurity를 이용해 설정한다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // JWT 제공 클래스
    private final JwtProvider jwtProvider;
    // 인증 실패 또는 인증헤더가 전달받지 못했을때 핸들러
    private final AuthenticationEntryPoint authenticationEntryPoint;
    // 인증 성공 핸들러
    private final AuthenticationSuccessHandler authenticationSuccessHandler;
    // 인증 실패 핸들러
    private final AuthenticationFailureHandler authenticationFailureHandler;
    // 인가 실패 핸들러
    private final AccessDeniedHandler accessDeniedHandler;

    /**
     * Security 적용 무시
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                .mvcMatchers("/docs/**");
    }

    /**
     * 보안 기능 초기화 및 설정
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        final String[] GET_WHITELIST = new String[]{
                "/login",
                "/user/login-id/**",
                "/user/email/**",
                "/affiliate"
        };

        final String[] POST_WHITELIST = new String[]{
                "/client-user"
        };

        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint) // 인증 실패
                .accessDeniedHandler(accessDeniedHandler) // 인가 실패
                .and().authorizeRequests()
                .antMatchers(HttpMethod.GET, GET_WHITELIST).permitAll() // 해당 GET URL은 모두 허용
                .antMatchers(HttpMethod.POST, POST_WHITELIST).permitAll() // 해당 POST URL은 모두 허용
                .antMatchers("/client-user/**").hasAnyRole(UserType.CL.getRoll()) // 권한 적용
                .anyRequest().authenticated() // 나머지 요청에 대해서는 인증을 요구
                .and() // 로그인하는 경우에 대해 설정함
                .formLogin().disable() // 로그인 페이지 사용 안함
//                .loginPage("/user/loginView") // 로그인 성공 URL을 설정함
//                .successForwardUrl("/index") // 로그인 실패 URL을 설정함
//                .failureForwardUrl("/index").permitAll()
                .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 사용자 요청 정보로 UserPasswordAuthenticationToken 발급하는 필터
     */
    @Bean
    public CustomAuthenticationFilter authenticationFilter() throws Exception {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
        // 필터 URL 설정
        customAuthenticationFilter.setFilterProcessesUrl("/login");
        // 인증 성공 핸들러
        customAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        // 인증 실패 핸들러
        customAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        // BeanFactory에 의해 모든 property가 설정되고 난 뒤 실행
        customAuthenticationFilter.afterPropertiesSet();
        return customAuthenticationFilter;
    }

    /**
     * JWT의 인증 및 권한을 확인하는 필터
     */
    @Bean
    public JwtFilter jwtFilter() {
        return new JwtFilter(jwtProvider);
    }

}

회원 인증

1. UsernamePasswordAuthenticationFilter 구현

사용자 요청 정보로 UserPasswordAuthenticationToken 발급 후 AuthenticationManager에게 전달하고 AuthenticationProvider의 인증 메서드를 실행하는 UsernamePasswordAuthenticationFilter를 구현

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        final UsernamePasswordAuthenticationToken authRequest;

        final LoginDTO loginDTO;
        try {
            // 사용자 요청 정보로 UserPasswordAuthenticationToken 발급
            loginDTO = new ObjectMapper().readValue(request.getInputStream(), LoginDTO.class);
            authRequest = new UsernamePasswordAuthenticationToken(loginDTO.getLogin(), loginDTO.getPass());
        } catch (IOException e) {
            throw new NotValidException();
        }
        setDetails(request, authRequest);

        // AuthenticationManager에게 전달 -> AuthenticationProvider의 인증 메서드 실행
        return this.getAuthenticationManager().authenticate(authRequest);
    }

}

2. AuthenticationProvider 구현

AuthenticationManager 하위에 실제로 인증을 처리할 AuthenticationProvider를 구현

@Component
@RequiredArgsConstructor
public class AuthenticationProviderImpl implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    /**
     * 인증 구현
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 전달 받은 UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;

        // AuthenticaionFilter에서 생성된 토큰으로부터 아이디와 비밀번호를 추출
        String username = token.getName();
        String password = (String) token.getCredentials();
        // 해당 회원 Database 조회
        UserDetailsImpl userDetail = (UserDetailsImpl) userDetailsService.loadUserByUsername(username);

        // 비밀번호 확인
        if (!passwordEncoder.matches(password, userDetail.getPassword()))
            throw new BadCredentialsException(userDetail.getUsername() + "Invalid password");

        // 인증 성공 시 UsernamePasswordAuthenticationToken 반환
        return new UsernamePasswordAuthenticationToken(userDetail.getUsername(), "", userDetail.getAuthorities());
    }

    /**
     * provider의 동작 여부를 설정
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

3. UserDetailsService 구현

인증 과정 중 실제 Database에 회원을 데이터를 조회하는UserDetailsService를 구현

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findFirstUserByLoginOrderByIdAsc(username).orElseThrow(() -> new NotFoundDataException("User"));
        return new UserDetailsImpl(
                user.getLogin(),
                user.getPass(),
                user.getEmail(),
                Collections.singleton(new SimpleGrantedAuthority("ROLE_" + user.getType().getRoll()))
        );
    }

}

4. UserDetails 구현

회원 데이터를 조회하고 해당 정보와 권한을 저장하는 UserDetails를 구현

@AllArgsConstructor
@Getter
@ToString
public class UserDetailsImpl implements UserDetails {

    private final String username;
    private final String password;
    private final String email;
    private final Collection<? extends GrantedAuthority> authorities;

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

    @Override
    public String getPassword() {
        return password;
    }

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

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

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

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

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

5. PasswordEncoder 구현

기본적으로 제공해주는 PasswordEncoder를 구현한 클래스를 사용할 경우 Bean 등록만 하면된다.  
(BCryptPasswordEncoder OR DelegatingPasswordEncoder)

@Bean
public PasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder();
}

 

 

Spring Password Encoder

Spring에서는 인증/권한인가 등의 처리가 필요할 때 사용하라고 만든 Spring Security 패키지가 존재한다. 그 중 유저가 입력하는 Password를 암호화해서 저장하는 방법에 대해서 알아보자 아, 그 전에

gompangs.tistory.com

 

다른 로직의 비밀번호 인증이 필요할 경우는 PasswordEncoder를 구현해서 사용한다.

@Component
public class PasswordEncoderImpl implements PasswordEncoder {

    /**
     * 비밀번호 해시
     */
    @Override
    public String encode(CharSequence rawPassword) {
        return passwordEncode((String) rawPassword); // 커스텀 메서드
    }

    /**
     * 비밀번호 확인
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return passwordMatches((String) rawPassword, encodedPassword); // 커스텀 메서드
    }

}

6. AuthenticationSuccessHandler 구현

인증 성공 시 핸들링하는 AuthenticationSuccessHandler를 구현

@Component
@RequiredArgsConstructor
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {

    private final JwtProvider jwtProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        // 전달받은 인증정보 SecurityContextHolder에 저장
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // JWT Token 발급
        final String token = jwtProvider.generateJwtToken(authentication);
        // Response
        ApiResponse.token(response, token);
    }

}

7. AuthenticationFailureHandler 구현

인증 실패 시 핸들링하는 AuthenticationFailureHandler를 구현

@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        ApiResponse.error(response, ApiResponseType.UNAUTHORIZED_RESPONSE);
    }

}

JWT 인증

1. JwtProvider 생성

JWT를 발급하고 확인하는 클래스 생성

@Component
@RequiredArgsConstructor
public final class JwtProvider {

    private final UserDetailsService userDetailsService;

    // secret key
    @Value("${jwt.secret-key}")
    private String secretKey;

    // access token 유효시간
    private final long accessTokenValidTime = 2 * 60 * 60 * 1000L;

    // refresh token 유효시간
    private final long refreshTokenValidTime = 2 * 7 * 24 * 60 * 60 * 1000L;

    @PostConstruct
    private void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    /**
     * 토큰에서 Claim 추출
     */
    private Claims getClaimsFormToken(String token) {
        return Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(secretKey)).parseClaimsJws(token).getBody();
    }

    /**
     * 토큰에서 인증 subject 추출
     */
    private String getSubject(String token) {
        return getClaimsFormToken(token).getSubject();
    }

    /**
     * 토큰에서 인증 정보 추출
     */
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getSubject(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    /**
     * 토큰 발급
     */
    public String generateJwtToken(Authentication authentication) {
        Claims claims = Jwts.claims().setSubject(String.valueOf(authentication.getPrincipal()));
        claims.put("roles", authentication.getAuthorities());
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + accessTokenValidTime))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    /**
     * 토큰 검증
     */
    public boolean isValidToken(String token) {
        try {
            Claims claims = getClaimsFormToken(token);
            return !claims.getExpiration().before(new Date());
        } catch (JwtException | NullPointerException exception) {
            return false;
        }
    }

}

2. JwtFilter 생성

Header를 통해 JWT의 인증 요청이 왔을때 처리하는 Filter 생성

@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";
    private final JwtProvider jwtProvider;

    /**
     * 토큰 인증 정보를 현재 쓰레드의 SecurityContext 에 저장하는 역할 수행
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        // Request Header에서 토큰 추출
        String jwt = resolveToken(request);

        // Token 유효성 검사
        if (StringUtils.hasText(jwt) && jwtProvider.isValidToken(jwt)) {
            // 토큰으로 인증 정보를 추출
            Authentication authentication = jwtProvider.getAuthentication(jwt);
            // SecurityContext에 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    /**
     * Request Header에서 토큰 추출
     */
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }
    
}

3. AuthenticationEntryPoint 구현

토큰 인증이 실패하거나 인증 헤더를 정상적으로 받지 못했을때 핸들링하는 AuthenticationEntryPoint를 구현

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ApiResponse.error(response, ApiResponseType.UNAUTHORIZED_RESPONSE);
    }

}

4. AccessDeniedHandler 구현

토큰 인가 실패 시 핸들링하는 AccessDeniedHandler를 구현

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ApiResponse.error(response, ApiResponseType.FORBIDDEN_RESPONSE);
    }

}

Flowchart


전체 소스는 github에서 확인하실 수 있습니다.

 

GitHub - devbeekei/SpringSecurityJwt: SpringSecurity + JWT example

SpringSecurity + JWT example. Contribute to devbeekei/SpringSecurityJwt development by creating an account on GitHub.

github.com

 

+ Recent posts