본문 바로가기
웹 개발/블로그 만들기 프로젝트

Jwt 적용하기

by L3m0n S0ju 2023. 7. 5.

 

 

블로그 만들기 프로젝트에서 Jwt를 어떻게 생성하고 사용하는지를 공유하려고 한다. 처음에는 자바 스프링을 시작하면서 Jwt를 사용하려 했지만 코드를 작성해야하는게 많고 어려워서 쿠키,세션으로 로그인 기능을 구현했었다. 

 

쿠키, 세션으로 로그인을 구현하는 방법부터 설명하겠다.

 

Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);

 

코드는 위와 같이 키 값으로 적당한 변수를 만들어 넣어주고 보내주면 사용자가 해당 쿠키를 받아서 사용자의 권한으로 로그인할 수 있었다. 그러나 요즘에는 쿠키를 잘 사용하지 않는다고 한다. 이유는 다음과 같다.

 

보안 문제 -> https://lemon-soju.tistory.com/86

1. 쿠키 값은 임의로 변경할 수 있다.

    - 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.

2. 쿠키에 보관된 정보는 훔쳐갈 수 있다.

    - 중요한 정보는 넣으면 안된다.

3. 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.

    - 쿠키를 서버에서 관리하지 않는다면 계속 훔쳐간 쿠키를 악의적으로 사용할 수 있다.

 

 

 

 

 


블로그 만들기 프로젝트에서 처음 로그인을 구현할 때는 세션을 통해서 위에 보안 문제를 해결하였다.

 

/**
 * 세션 생성
 */
public void createSession(Object value, HttpServletResponse response) {
    // 세션 id를 생성하고, 값을 세션에 저장
    String sessionId = UUID.randomUUID().toString();
    sessionStore.put(sessionId, value);

    // 쿠키 생성
    Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
    response.addCookie(mySessionCookie);
}

/**
 * 세션 조회
 */
public Object getSession(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    if (sessionCookie == null) {
        return null;
    }
    return sessionStore.get(sessionCookie.getValue());
}

/**
 * 세션 만료
 */
public void expire(HttpServletRequest request) {
    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
    if (sessionCookie != null) {
        sessionStore.remove(sessionCookie.getValue());
    }
}

private Cookie findCookie(HttpServletRequest request, String cookieName) {
    if (request.getCookies() == null) {
        return null;
    }
    
    return Arrays.stream(request.getCookies())
            .filter(cookie -> cookie.getName().equals(cookieName))
            .findAny()
            .orElse(null);
}

 

 

하지만 위 코드는 스프링에서 제공해주는 HttpSession으로 대체할 수 있다. 아래 코드처럼 HttpSession을 사용하면 서버에서 세션을 관리할 수 있다.

 

HttpSession session = request.getSession(); //세션에 로그인 회원 정보 보관
session.setAttribute(세션 ID, loginMember);

 

 

하지만 요즘에는 세션 방식보다는 Jwt 토큰을 가장 많이 사용한다고 한다. 이유는 다음과 같다.

 

 

1. 무상태(Stateless) 특성: JWT는 서버에 세션 정보를 저장하지 않고, 토큰 자체에 필요한 정보를 포함합니다. 이로써 서버는 클라이언트의 상태를 유지할 필요가 없어집니다. 세션 기반 인증에서는 서버가 세션을 유지하고 관리해야 하지만, JWT는 클라이언트가 토큰을 가지고 있으므로 서버의 부담을 줄여줍니다.

-> 요약: jwt는 서버에 부담이 덜 하다.

2. 확장성과 분산 시스템 지원: JWT는 토큰 자체에 필요한 정보를 포함하고 있기 때문에, 여러 서버나 서비스 간에 토큰을 전달하여 인증을 수행할 수 있습니다. 이는 분산 시스템에서 확장성을 높이고 서비스 간의 인증을 편리하게 처리할 수 있도록 해줍니다.

-> 요약: 서버마다 세션을 저장하는 곳이 다르기 때문에 공유가 어렵다. 반면 jwt는 확장이 편하다. 

 

 

jwt 사용법은 아래에서 설명하겠다.

 

 

 

 

 


Jwt 사용법

 

일단 설정 파일에서 인터셉터를 설정할 건데 Jwt 인증이 성공하면 통신이 가능하도록 인터셉터를 등록한다. 인터셉터는 HandlerInterceptor를 구현하였다.

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final JwtService jwtService;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor(jwtService))
                .excludePathPatterns("/error");
    }
}

 

 

@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {

    private final JwtService jwtService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle 동작");
        if(request.getMethod().equals("OPTIONS")) { // preflight 인 경우 허용
            return true;
        }
        if (jwtService.authenticateToken(request)) {
            return true;
        }
        throw new Unauthorized();
    }
}

 

 

 

 

 


JwtService 생성

@Service
@RequiredArgsConstructor
public class JwtService {

    private static final String KEY = {KEY 값};
    private final MemberDataRepository memberDataRepository;

    public boolean authenticateToken(HttpServletRequest request) {
        String accessToken = request.getHeader("accessToken");
        if (accessToken == null || accessToken.equals("")) {
            throw new JwtTokenNull();
        }
        byte[] decodeKey = Base64.decodeBase64(KEY);
        try {
            Jws<Claims> claims = Jwts.parserBuilder()
                    .setSigningKey(decodeKey)
                    .build()
                    .parseClaimsJws(accessToken);

            Date now = new Date();
            if(!now.before(claims.getBody().getExpiration())) {
                throw new Unauthorized();
            }

            // 사용자 존재여부 검사
            Member findMember = memberDataRepository.findByUid(claims.getBody().getSubject()).get();
            if(!findMember.equals(null)) {
                return true;
            }
        } catch (JwtException e) {
            throw new Unauthorized();
        } catch (NoSuchElementException e){
            throw new NonExistMember();
        }
        return false;
    }

    /**
     * 현재 로그인한 사용자 정보가 필요할 때
     */
    public Member findMemberByToken(HttpHeaders request) {
        String accessToken = request.getFirst("accessToken");
        if (accessToken == null || accessToken.equals("")) {
            throw new Unauthorized();
        }

        byte[] decodeKey = Base64.decodeBase64(KEY);
        try {
            Jws<Claims> claims = Jwts.parserBuilder()
                    .setSigningKey(decodeKey)
                    .build()
                    .parseClaimsJws(accessToken);

            Member findMember = memberDataRepository.findByUid(claims.getBody().getSubject()).get();
            return findMember;

        } catch (JwtException e) {
            throw new Unauthorized();
        } catch (NoSuchElementException e){
            throw new NonExistMember();
        }
    }
}

Jwt 사용법은 위 코드처럼 HttpServletRequest을 받아서 Jwt 토큰을 키 값으로 디코딩하고 정상적인 값이 출력되면 true를 반환한다.

 

 

 

 

 

 


Jwt 생성

public MemberLoginResponseDto login(MemberLoginRequestDto memberLoginRequestDto) {

    List<Member> findMember = memberDataRepository.findByUid(memberLoginRequestDto.getUid())
            .stream().filter(m -> m.getPwd().equals(memberLoginRequestDto.getPwd())).collect(Collectors.toList());
    if(findMember.size() != 1) {
        throw new IllegalStateException("존재하지 않는 회원입니다");
    }

    Key key = Keys.hmacShaKeyFor(Base64.getDecoder().decode((KEY)));

    // jwt 설정
    Date now = new Date();
    Date expiration = new Date(now.getTime() + Duration.ofMinutes(30).toMillis()); // 만료기간 30분

    String jws = Jwts.builder()
            .setSubject(findMember.get(0).getUid())
            .setIssuedAt(now) // 발급시간(iat)
            .setExpiration(expiration) // 만료시간(exp)
            .signWith(key) // 키값
            .compact();

    MemberLoginResponseDto memberLoginResponseDto = MemberLoginResponseDto.builder()
            .uid(findMember.get(0).getUid())
            .accessToken(jws)
            .build();
    return memberLoginResponseDto;
}

jwt 토큰 생성은 Subject에 넣고 싶은 값을 넣고 발급시간, 만료시간, 키값을 넣고 jwt 값을 만들어서 response 반환 값에 추가하면 된다.

 

세션, 쿠키로 하던 로그인 인증을 Jwt로 기능을 대체해봤는데 처음에는 뭔가 함수가 많아서 막막했지만 막상 끝내고 나니 별게 아니었구나 생각이 들었다.

댓글