본문 바로가기
Study/Spring

Spring Security + JWT #3

by 네빛나래 2024. 1. 7.

출처 : https://blog.naver.com/makeflood/223302260035

 

스프링 시큐리티 JWT : spring security JWT 구현 실습

스프링 시큐리티 6 버전을 통한 JWT 방식의 인증 인가 구현하기 스프링 시큐리티 JSON Web Token ...

blog.naver.com

해당 강의 보고 따라서 하는중입니다.


버전 및 의존성

IntelliJ IDEA 2023.3.2 (Ultimate Edition)
JDK 17
SpringBoot 3.2.1
Spring Security 6.2.1
JWT 0.9.1
Spring Data JPA - MariaDB
LomBok

DB기반의 실질적인 로그인 검증로직을 진행해보도록한다.

 

먼저 UserRepository에 UserEntity를 return하는 findbyusername 메소드를 하나 만들어준다

 

		//username을 받아 DB테이블에서 회원을 조회하는 메소드 작성
		UserEntity findByUsername(String username);

그리고 Service패키지에 CustomUserDetailsService 클래스를 생성해서 검증하는 서비스를 생성한다.

package com.security.springjwt.service;

import com.security.springjwt.dto.CustomUserDetails;
import com.security.springjwt.entity.UserEntity;
import com.security.springjwt.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

	private final UserRepository userRepository;

	public CustomUserDetailsService(UserRepository userRepository) {

		this.userRepository = userRepository;
	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		//DB에서 조회
		UserEntity userData = userRepository.findByUsername(username);

		if (userData != null) {

			//UserDetails에 담아서 return하면 AutneticationManager가 검증 함
			return new CustomUserDetails(userData);
		}

		return null;
	}
}

앞서 만들었던 findbyusername을 이용하기 위해 userrepository를 받아와주고 implements로 userdetailservice를 심게 되면 loaduserbyusername 메서드를 만들게된다.

 

그 이후 return new CustomUserDetails에 해당하는 클래스(dto)를 만들어서 활용해야합니다.

 

따라서 dto패키지안에 CustomUserDetails라는 클래스를 생성해줍니다. 해당 클래스는 마찬가지로 UserDetails를 상속받습니다.

 

package com.security.springjwt.dto;

import com.security.springjwt.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class CustomUserDetails implements UserDetails {

	private final UserEntity userEntity;

	public CustomUserDetails(UserEntity userEntity) {

		this.userEntity = userEntity;
	}


	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {

		Collection<GrantedAuthority> collection = new ArrayList<>();

		collection.add(new GrantedAuthority() {

			@Override
			public String getAuthority() {

				return userEntity.getRole();
			}
		});

		return collection;
	}

	@Override
	public String getPassword() {

		return userEntity.getPassword();
	}

	@Override
	public String getUsername() {

		return userEntity.getUsername();
	}

	@Override
	public boolean isAccountNonExpired() {

		return true;
	}

	@Override
	public boolean isAccountNonLocked() {

		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {

		return true;
	}

	@Override
	public boolean isEnabled() {

		return true;
	}
}

UserEntity를 사용하기 위해 생성자를 만들어주고Role를 리턴해주는 부분을 수정해준뒤 getPassword, getUsername같은 메서드들도 전부 구현해준뒤, 만료되는 부분이나 여타 부분들을 막히지 않도록 전부 true로 바꿔줍니다.

 


JWT 발급 및 검증

  • 로그인 -> 성공 -> JWT 발급
  • 접근 -> JWT 검증

JWT 생성원리

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

JWT(JSON WEB TOKEN)는 JSON형태의 웹 토큰입니다.

 

header

  • JWT임을 명시
  • 사용된 암호화 알고리즘

Payload

  • 정보

Signautre

  • 암호화 알고리즘((BASE64(header)+(BASE64(Payload)) + 암호화키

JWT의 특징은 해당 정보들이 단순히 base64로 인코딩하기때문에 외부에서 쉽게 디코딩할수있다.

따라서 비밀번호같은 정보를 담으면 안됩니다. 해당 JWT를 사용하는 이유는 발급처를 확인하기 위해서 사용합니다.

 

JWT 암호화 방식

 

암호화 종류

  • 양방향 (대칭키(현재 프로젝트에서는 양방향 대칭키 사용 : HS256), 비대칭키)
  • 단방향

JWT 발급하는 클래스를 만들기 위해선 application.properties에 임의의 값을 설정해준다.

spring.jwt.secret=vmfhaltmskdlstkfkdgodyroqkfwkdbalroqkfwkdbalaaaaaaaaaaaaaaaabbbbb

다음과같이 최대한 긴값을 설정해주고 

jwt패키지 안에 JWTUtil이라는 클래스를 생성해준다. 해당 클래스에서 JWT의 발급과 검증을 모두 담당한다.

 

package com.security.springjwt.jwt;

import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JWTUtil {

	private SecretKey secretKey;

	public JWTUtil (@Value("${spring.jwt.secret}") String secret) {


		secretKey = new SecretKeySpec (secret.getBytes (StandardCharsets.UTF_8), Jwts.SIG.HS256.key ().build ().getAlgorithm ());
	}

	//검증 메서드
	public String getUsername (String token) {

		return Jwts.parser ().verifyWith (secretKey).build ().parseSignedClaims (token).getPayload ().get ("username", String.class);
	}

	public String getRole (String token) {

		return Jwts.parser ().verifyWith (secretKey).build ().parseSignedClaims (token).getPayload ().get ("role", String.class);
	}

	public Boolean isExpired (String token) {

		return Jwts.parser ().verifyWith (secretKey).build ().parseSignedClaims (token).getPayload ().getExpiration ().before (new Date ());
	}

	public String createJWT (String username, String role, Long expiredMS) {
		return Jwts.builder ()
				.claim ("username", username)
				.claim ("role", role)
				.issuedAt (new Date (System.currentTimeMillis ()))
				.expiration (new Date (System.currentTimeMillis () + expiredMS))
				.signWith (secretKey)
				.compact ();
	}
}

처음에 해당 클래스를 생성할때 @Value를 import할때 lombok의 @Value를 import해와서 계속 빨간줄이 떠서 당황스러웟었다. 다시 지우고 입력하니까 제대로 import시킬수 있엇다.

 

아무튼 처음에앞서 입력했던 properties의 값을 가져오고 그뒤엔 토큰을 이용해서 username, role 그리고 만료되었는지를 확인하고 JWT를 생성해준다

builder를 이용해서

username, role 그리고 만료시간을 만들어준다.

 

이제 로그인 성공시 JWT 발급해야하기때문에 Loginfilter에 JWTUtil을 추가해준다.

	private final AuthenticationManager authManager;
	private final JWTUtil jwtUtil;

	public LoginFilter (AuthenticationManager authManager, JWTUtil jwtUtil) {
		this.authManager = authManager;
		this.jwtUtil = jwtUtil;
	}

해당 부분을 작성하게 되면 SecurityConfig에서 오류가 발생하게됩니다.

왜냐하면

//로그인 필터 추가
		http
				.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

해당 부분에서 Loginfilter를 생성하는 부분에 JWTUtil이 추가되었기 떄문입니다.

따라서 SecurityConfig에도 JWTUtil을 주입해줍니다.

private final AuthenticationConfiguration authenticationConfiguration;
	private final JWTUtil jwtUtil;

	public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) {

		this.authenticationConfiguration = authenticationConfiguration;
		this.jwtUtil = jwtUtil;
	}

그리고 로그인필터를 추가하는 부분에도 만들어진 객체를 추가해줍니다.

http
				.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

그러면 더이상 오류가 발생하지 않습니다.

 

그 다음에는 로그인 성공 메서드를 완성 시켜보겠습니다.

	//로그인 성공시 실행하는 메소드 (여기서 JWT를 발급하면 됨)
	@Override
	protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
		CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal ();

		String username = customUserDetails.getUsername ();
		Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities ();
		Iterator<? extends GrantedAuthority> iterator = authorities.iterator ();
		GrantedAuthority auth = iterator.next ();

		String role = auth.getAuthority ();

		//username과 role을 이용해서 Token생성
		String token = jwtUtil.createJWT (username, role, 60*60*10L);

		//접두사를 붙이고 한칸을 띄워줘야함
		response.addHeader ("Authorization", "Bearer " + token);
	}

CustomUserDetails에서 username과 role을 받아오고 해당 정보를 이용해서 토큰을 생성해줍니다.

끝에 60*60*10L은 해당 토큰의 유효기간 입니다.

 

그리고 응답헤더에 해당 토큰을 실어보내줍니다. 그때 Authoriztion이라는 속성명에 토큰 자체에는 앞에 접두사를 붙이 한칸 띄워준뒤 token을 보내줍니다.

 

해서 해당 정보를 실행하면

 

이런식으로 Authorization에 Bearer 뒤에 토큰이 붙어서 받을수 있게됩니다.

 

발급받은 토큰을 이용해서 검증필터를 생성해서 이용해보도록 하겠습니다.

 

먼저 jwt패키지안에 JwtFilter 클래스를 하나 생성해줍니다.

package com.security.springjwt.jwt;

import com.security.springjwt.dto.CustomUserDetails;
import com.security.springjwt.entity.UserEntity;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JWTFilter extends OncePerRequestFilter {
	private final JWTUtil jwtUtil;

	public JWTFilter (JWTUtil jwtUtil) {
		this.jwtUtil = jwtUtil;
	}

	@Override
	protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

		String authoerization = request.getHeader ("Authorization");

		//Authoerization 검증
		if (authoerization == null || !authoerization.startsWith ("Bearer ")) {
			filterChain.doFilter (request, response);
			return;
		}

		//순수 토큰만 획득
		String token = authoerization.split (" ")[1];

		//토큰 소멸시간 검증
		if (jwtUtil.isExpired (token)) {
			filterChain.doFilter (request, response);
			return;
		}

		//토큰에서 username과 role 획득
		String username = jwtUtil.getUsername (token);
		String role = jwtUtil.getRole (token);

		//userEntity를 생성하여 값 set
		UserEntity userEntity = new UserEntity ();
		userEntity.setUsername (username);
		//임시비밀번호
		userEntity.setPassword ("temppassword");
		userEntity.setRole (role);

		CustomUserDetails customUserDetails = new CustomUserDetails (userEntity);
		Authentication authToken = new UsernamePasswordAuthenticationToken (customUserDetails, null, customUserDetails.getAuthorities ());


		//세션에 사용자 등록
		SecurityContextHolder.getContext ().setAuthentication (authToken);

		filterChain.doFilter (request, response);
	}
}

해당 클래스가 검증과정에서 이용되는 클래스입니다.

authorization이 null이아닌지, 그리고 접두사가 옳게 되어있는지 검증한뒤 

split을 이용해서 접두사를 떼내고 토큰부분만 얻어냅니다.

그리고 해당 토큰의 소멸시간을 검증한뒤 토큰에서 username과 role을 얻어내고 해당 값을 Entity에 넣어줍니다.

 

그리고 stateless한 session에 해당 사용자를 등록해서 사용합니다.

POSTMAN을 이용해서 확인해보면

 

200OK가 나오면서 admin에 get요청이 옳게 가게 됩니다.

'Study > Spring' 카테고리의 다른 글

Spring Security + JWT #5  (0) 2024.01.11
Spring Security + JWT #4  (0) 2024.01.08
Spring Security + JWT #2  (0) 2024.01.07
Spring Security + JWT Token #1  (0) 2024.01.07
Spring 3일차 @RestController / @Query  (0) 2024.01.04