JWT - (2) Java에서 JWT 사용하기

단비·2023년 5월 11일
0

학습

목록 보기
44/66

의존성 추가

// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'



간단한 토큰 생성과 검증

토큰 생성

key = 256bit 난수;

//Header 부분 설정
Map<String, Object> headers = new HashMap<>();
// 토큰의 유형
headers.put("typ", "JWT");
// 서명 알고리즘
headers.put("alg", "HS256");

//payload 부분 설정
Map<String, Object> payloads = new HashMap<>();

// 저장할 데이터 설정(AES암호화)
payloads.put("userKey", aes256.encoding(userKey));
payloads.put("name", aes256.encoding(name));

Long expiredTime = 1000 * 60L * 3L; // 토큰 유효 시간 (3분)

Date ext = new Date(); // 토큰 만료 시간
// 현재 시간 + 유효 시간
ext.setTime(ext.getTime() + expiredTime);

// 토큰 Builder
String jwt = Jwts.builder()
	.setHeader(headers) // Headers 설정
	.setClaims(payloads) // Claims 설정
	.setSubject("user") // 토큰 용도(제목)
	.setExpiration(ext) // 토큰 만료 시간 설정
	.signWith(SignatureAlgorithm.HS256, key.getBytes()) // HS256과 Key로 Sign
	.compact(); // 토큰 생성

토큰 검증

  • get을 통해 데이터 가져오기
Claims claims = Jwts.parser()
	.setSigningKey(key.getBytes("UTF-8")) // Set Key
	.parseClaimsJws(jwt) // 파싱 및 검증, 실패 시 에러
	.getBody();

String userKey = claims.get("userKey", String.class);
String name = claims.get("name", String.class);
String hp = claims.get("hp", String.class);






Spring Security를 이용한 JWT

Spring Security 5.7.0 부터는 더이상 WebSecurityConfigurerAdapter를 확장해서 사용하지 않고 Bean을 주입하는 방식으로 사용

User Entity의 경우 UserDetails 를 통합 구현할 필요 없이 별도의 클래스를 생성하여 구현할 수 있음

(role의 경우 별도로 필드를 추가하거나 테이블을 추가해도 되지만 테스트용이기 때문에 따로 만들지 않았음)


CustomUserDetails

  • getAuthorities 메소드
    • 해당 user의 role 리스트를 가져오는 부분인데
      따로 필드나 테이블을 생성하지 않았기 때문에 임의로 USER 권한을 부여함
public class CustomUserDetails implements UserDetails {
    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }

    public final User getUser() {
        return user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
//        return user.getRoles().stream().map(o -> new SimpleGrantedAuthority(
//                o.getName()
//        )).collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPw();
    }

    @Override
    public String getUsername() {
        return user.getUserKey();
    }

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

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

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

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

JpaUserDetailsService

  • UserDetails를 설정해주는 클래스로 리턴
@Service
@RequiredArgsConstructor
public class JpaUserDetailsService implements UserDetailsService {

    private final UserRepository repository;

    @Override
    public UserDetails loadUserByUsername(String userKey) throws UsernameNotFoundException {
        User user = repository.findByUserKey(userKey).orElseThrow(
                () -> new UsernameNotFoundException("Invalid authentication!")
        );

        return new CustomUserDetails(user);
    }
}

JwtProvider

  • JWT의 생성, 검증 등을 구현하는 클래스
@RequiredArgsConstructor
@Component
public class JwtProvider {
    private final ApplicationSetting setting;
    private final AES256 aes256;
    private final UserRepository repository;

    private byte[] secretKey;

    // 만료시간 : 1시간
    private final long exp = 1000L * 60 * 60;

    private final JpaUserDetailsService userDetailsService;

    @PostConstruct
    protected void init() {
        secretKey = setting.getJwt_key().getBytes(StandardCharsets.UTF_8);
    }

    // 토큰 생성
    public String createToken(String userKey) throws Exception {
        User user = repository.findByUserKey(userKey).get();

        Claims claims = Jwts.claims().setSubject(aes256.encoding(userKey));

        claims.put("name", aes256.encoding(user.getName()));
        claims.put("pw", user.getPw());
        
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + exp))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // 권한정보 획득
    // Spring Security 인증과정에서 권한확인을 위한 기능
    public Authentication getAuthentication(String token) throws Exception {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getAccount(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에 담겨있는 UserKey(제목) 획득
    // aes256 인코딩하여 삽입했기 때문에 디코딩 필수
    public String getAccount(String token) throws Exception {
        return aes256.decoding(
                Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject()
        );
    }

    // 토큰에 담겨있는 데이터 획득
    public UserResponse getBody(String token) throws Exception {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(secretKey) // Set Key
                    .parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
                    .getBody();

            String name = claims.get("name", String.class);
            String hp = claims.get("hp", String.class);

            return UserResponse.of(new HashMap<String, String>(){{
                put("userKey", aes256.decoding(claims.getSubject()));
                put("name", aes256.decoding(name));
                put("hp", aes256.decoding(hp));
            }});
        } catch (SignatureException e){ // 기존 서명을 확인하지 못했을 경우
            throw new SignatureException("ERROR_SIGN_JWT");
        } catch (ExpiredJwtException e) { // 토큰이 만료되었을 경우
            throw new JwtException("ERROR_EXP_JWT");
        }
    }

    // Authorization Header를 통해 인증을 한다.
    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("Authorization");
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            // Bearer 검증
            if (!token.substring(0, "BEARER ".length()).equalsIgnoreCase("BEARER ")) {
                return false;
            } else {
                token = token.split(" ")[1].trim();
            }
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            // 만료되었을 시 false
            return !claims.getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

JwtAuthenticationFilter

  • 토큰의 유효성을 검증하는 클래스
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtProvider jwtProvider;

    public JwtAuthenticationFilter(JwtProvider jwtProvider) {
        this.jwtProvider = jwtProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    	// 헤더의 토큰값을 가져옴
        String token = jwtProvider.resolveToken(request);

		// 토큰의 유효성 검사를 통해 boolean값을 가져옴
        if (token != null && jwtProvider.validateToken(token)) {
            // check access token
            token = token.split(" ")[1].trim();
            Authentication auth = null;
            try {
            	// 권한을 가져옴
                auth = jwtProvider.getAuthentication(token);
                System.out.println(auth);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        filterChain.doFilter(request, response);
    }
}

SecurityConfig

  • Spring Security의 설정 클래스
  • cors
    • addAllowedOrigin() : 허용할 URL
    • addAllowedHeader() : 허용할 Header
    • addAllowedMethod() : 허용할 Http Method
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {

    private final JwtProvider jwtProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // ID, Password 문자열을 Base64로 인코딩하여 전달하는 구조
                .httpBasic().disable()
                // 쿠키 기반이 아닌 JWT 기반이므로 사용하지 않음
                .csrf().disable()
                // CORS 설정
                .cors(c -> {
                            CorsConfigurationSource source = request -> {
                                // Cors 허용 패턴
                                CorsConfiguration config = new CorsConfiguration();
                                config.setAllowedOrigins(
                                        Arrays.asList("*")
                                );
                                config.setAllowedMethods(
                                        Arrays.asList("*")
                                );
                                return config;
                            };
                            c.configurationSource(source);
                        }
                )
                // Spring Security 세션 정책 : 세션을 생성 및 사용하지 않음
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 조건별로 요청 허용/제한 설정
                .authorizeRequests()
                // 회원가입과 로그인은 모두 승인
                .antMatchers("/register", "/login").permitAll()
                // /admin으로 시작하는 요청은 ADMIN 권한이 있는 유저에게만 허용
                .antMatchers("/admin/**").hasRole("ADMIN")
                // /user 로 시작하는 요청은 USER 권한이 있는 유저에게만 허용
                .antMatchers("/user/**").hasRole("USER")
                // denyAll: 접근을 전부 제한
                // permitAll: 접근을 전부 허용
                .anyRequest().denyAll()
                .and()
                // JWT 인증 필터 적용
                .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class)
                // 에러 핸들링
                .exceptionHandling()
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                        // 권한 문제가 발생했을 때 이 부분을 호출한다.
                        response.setStatus(403);
                        response.setCharacterEncoding("utf-8");
                        response.setContentType("text/html; charset=UTF-8");
                        response.getWriter().write("권한이 없는 사용자입니다.");
                    }
                })
//                AuthenticationEntryPoint
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                        // 인증문제가 발생했을 때 이 부분을 호출한다.
                        response.setStatus(401);
                        response.setCharacterEncoding("utf-8");
                        response.setContentType("text/html; charset=UTF-8");
                        response.getWriter().write("인증되지 않은 사용자입니다.");
                    }
                });

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

포스트맨의 경우 Header - Bearer Token에 유효한 토큰값을 실어 보내야 오류가 발생하지 않음






참고사이트

[JAVA] jjwt library 사용방법 - JWT(Java Web Token)-밤둘레
JWT payload (claims, body) 부 암호화 및 복호화 방법-호형

SpringBoot 로그인 구현하기 (with. SpringSecurity, JWT)-juno.log

profile
tistory로 이전! https://sweet-rain-kim.tistory.com/

0개의 댓글