카테고리 없음

user 구현

suh75321 2024. 7. 26. 00:40
  • [ ] JWT
    • [ ] JWT를 이용한 인증/인가를 구현한다.
    • [ ] 위 CRUD 단계에서 인증/인가가 완료된 후에만 기능이 동작하도록 수정한다.
    • [ ] 조건
      • [ ] Access Token 만료시간 60분
      • [ ] Refresh Token 구현은 선택
    • [ ] 예외처리
      • [ ] 공통조건
        • [ ] StatusCode : 400 / client에 반환
      • [ ] 토큰이 필요한 API 요청에서 토큰을 전달하지 않았거나 정상 토큰이 아닐 때
        • [ ] 에러 메세지 : “토큰이 유효하지 않습니다.”
      • [ ] 토큰이 있고, 유효한 토큰이지만 해당 사용자가 작성한 게시글/댓글이 아닐 때
        • [ ] 에러 메세지 : “작성자만 삭제/수정할 수 있습니다.”
      • [ ] DB에 이미 존재하는 username으로 회원가입을 요청할 때
        • [ ] 에러 메세지 : “중복된 username 입니다.”
      • [ ] 로그인 시, 전달된 username과 password 중 맞지 않는 정보가 있을 때
        • [ ] 에러 메시지 : “회원을 찾을 수 없습니다.”
      • [ ] StatusCode 나누기
  • [ ] 회원가입
    • 사용자의 정보를 전달 받아 유저 정보를 저장한다.사용자 필드 데이터 유형
      아이디 bigint
      별명 varchar
      사용자이름 (username) varchar
      비밀번호 (password) varchar
      권한 (일반, 어드민) varchar
      생성일 timestamp
    • [ ] 조건
      • [ ] 패스워드 암호화는 선택적으로 구현
      • [ ] username은 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)로 구성
    • [ ] password는 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성
    • [ ] DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환
  • [ ] 로그인
    • username, password 정보를 client로부터 전달받아 토큰을 반환한다.
    • DB에서 username을 사용하여 저장된 회원의 유무를 확인한다.
      • 저장된 회원이 있다면 password 를 비교하여 로그인 성공 유무를 체크한다
    • [ ] 조건
      • [ ] 패스워드 복호화는 선택적으로 구현 (위 회원가입의 암호화와 동일)
      • [ ] 로그인 성공 시 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급
      • [ ] 발급한 토큰을 Header에 추가하고 성공했다는 메시지 및 상태코드와 함께 client에 반환

 

이제 이것들을 본격적으로 구현하기로 한다.

 

먼저,

package com.teamsparta.task.user.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.sql.Timestamp;

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")//테이블 이름은 users로 user이 아니다. 유저로 하면 오류가 나기 때문이다.
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String nickname; // 별명

    @Column(nullable = false, unique = true)
    private String username; // 사용자 이름 (username)

    @Column(nullable = false)
    private String password; // 비밀번호 (password)

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private UserRoleEnum role; // 권한 (USER 또는 ADMIN)

    @Column(nullable = false, updatable = false)
    private Timestamp createdAt; // 생성일

    // 매개변수를 받는 생성자 추가
    public User(String nickname, String username, String password, UserRoleEnum  role, Timestamp createdAt) {
        this.nickname = nickname;
        this.username = username;
        this.password = password;
        this.role = role;
        this.createdAt = createdAt;
    }

    // 생성일 자동 설정을 위한 메서드
    @PrePersist
    protected void onCreate() {
        this.createdAt = new Timestamp(System.currentTimeMillis());
    }
}

 

이렇게 아이디, 닉네임, 유저네임, 비번, 권한, 생성일을 전부 추가해준다.

 

package com.teamsparta.task.user.model;

public enum UserRoleEnum {
    USER(Authority.USER),  // 사용자 권한
    ADMIN(Authority.ADMIN);  // 관리자 권한

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() {
        return this.authority;
    }

    public static class Authority {
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
}

 

이것도 넣어준다.

그리고 로그인을 위한 dto들도 넣어준다

 

@Setter
@Getter
public class LoginRequestDto {
    private String username;
    private String password;
}

로그인용

@Getter
@Setter
public class LoginResponseDto {
    private String accessToken;

    public LoginResponseDto(String accessToken) {
        this.accessToken = accessToken;
    }

}

로그인 하고 토큰 보여주기용

 

 

 

@Getter
@Setter
public class SignupRequestDto {
    private String nickname; // 별명 추가
    private String username;
    private String password;
}

 

 

회원가입용 

@Getter
@Setter
public class UpdateUserRequestDto {
    private String nickname; // 별명 추가
    private String username;
}

 

유저 수정용

 

package com.teamsparta.task.user.service;

import com.teamsparta.task.jwt.JwtUtil;
import com.teamsparta.task.user.dto.LoginRequestDto;
import com.teamsparta.task.user.dto.SignupRequestDto;
import com.teamsparta.task.user.dto.UpdateUserRequestDto;
import com.teamsparta.task.user.model.User;
import com.teamsparta.task.user.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
import com.teamsparta.task.user.model.UserRoleEnum; // 추가
import java.sql.Timestamp; // 추가

@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil; // 로그인을 위해 추가

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtUtil = jwtUtil; // 여기도
    }

    public void signup(SignupRequestDto requestDto) {
        String username = requestDto.getUsername();
        String password = passwordEncoder.encode(requestDto.getPassword());
        String nickname = requestDto.getNickname();
        UserRoleEnum role = UserRoleEnum.USER;

        // 회원 중복 확인
        if (userRepository.findByUsername(username).isPresent()) {
            throw new IllegalArgumentException("중복된 username 입니다."); // 중복된 username 처리
        }

        // 사용자 등록
        User user = new User(nickname, username, password, role, new Timestamp(System.currentTimeMillis()));
        userRepository.save(user);
    }

    public String login(LoginRequestDto requestDto) {
        String username = requestDto.getUsername();
        String password = requestDto.getPassword();

        // 사용자 조회
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다.")); // 정보 불일치 처리

        // 비밀번호 확인
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new IllegalArgumentException("회원을 찾을 수 없습니다."); // 비밀번호 불일치 처리
        }

        // JWT 토큰 생성
        return jwtUtil.createToken(user.getUsername());
    }

    public void updateUser(UpdateUserRequestDto requestDto, User user) {
        user.setUsername(requestDto.getUsername());
        user.setNickname(requestDto.getNickname());
        userRepository.save(user);
    }

    public void deleteUser(User user) {
        userRepository.delete(user);
    }
}

 

 이걸로 중복된 유저네임, 회원을 찾을수 없습니다를,

 

package com.teamsparta.task.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Component
public class JwtUtil {
    // Header KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    // 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";
    // Token 식별자
    public static final String BEARER_PREFIX = "Bearer ";// Bearer라고 하고 한칸 띄어야함
    // 토큰 만료시간
    private final long TOKEN_TIME =60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey. application.properties에 넣었던 그것과 연결
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 로그 설정
    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

    @PostConstruct//이 메소드는 JwtUtil 객체가 생성된 후에 실행. 이 메소드는 비밀 키를 Base64 디코딩하여
    // java.security.Key 객체를 생성하고, 이 객체를 key 필드에 저장
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // 토큰 생성
    public String createToken(String username) {
        Date date = new Date();//현재 날짜를 가져옴

        return BEARER_PREFIX +
                Jwts.builder()//새로운 JWT를 생성하기 위한 JwtBuilder 객체를 생성
                        .setSubject(username) // 사용자 식별자값(ID)
//                        .claim(AUTHORIZATION_KEY) // 사용자 권한
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();// JWT를 문자열로 변환
    }

    // JWT Cookie 에 저장
    public void addJwtToCookie(String token, HttpServletResponse res) {//
        try {
            token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value를 받아 새 쿠키 생성
            cookie.setPath("/");

            // Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage());//에러 발생시 메시지
        }
    }
    // JWT 토큰 substring
    public String substringToken(String tokenValue) {// tokenValue가 null이 아니고, 공백이 아닌 텍스트를 포함하고 있는지 확인
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);//tokenValue에서 "Bearer " 접두사를 제거하고, 실제 토큰 값을 반환
            // substring(7)은 문자열의 7번째 인덱스부터 끝까지의 부분 문자열을 반환
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");//토큰이 유효하지 않으면 에러
    }//- StringUtils.hasText를 사용하여 공백, null을 확인하고 startsWith을 사용하여 토큰의 시작값이 Bearer이 맞는지 확인
//맞다면 순수 JWT를 반환하기 위해 substring을 사용하여 Bearer을 잘라냅니다.

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);//주어진 JWT를 파싱하고 검증
            return true;//JWT가 유효하면 true 아니면 에러메시지들
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            logger.error("Invalid JWT signature, 토큰이 유효하지 않습니다.");//토큰이 유효하지 않습니다
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }
    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }//주어진 jwt를 검증

    // HttpServletRequest 에서 Cookie Value : JWT 가져오기. 필터를 위해 추가
    public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    try {
                        return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
        }
        return null;
    }

}

 

이안에서의 

 

public boolean validateToken(String token) {
    try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);//주어진 JWT를 파싱하고 검증
        return true;//JWT가 유효하면 true 아니면 에러메시지들
    } catch (SecurityException | MalformedJwtException | SignatureException e) {
        logger.error("Invalid JWT signature, 토큰이 유효하지 않습니다.");//토큰이 유효하지 않습니다
    } catch (ExpiredJwtException e) {
        logger.error("Expired JWT token, 만료된 JWT token 입니다.");
    } catch (UnsupportedJwtException e) {
        logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
    } catch (IllegalArgumentException e) {
        logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
    }
    return false;
}

 

이것으로 토큰 검증을 구현한다.

 

참고로 컨트롤러는

package com.teamsparta.task.user.controller;

import com.teamsparta.task.security.UserDetailsImpl;
import com.teamsparta.task.user.dto.LoginRequestDto;
import com.teamsparta.task.user.dto.LoginResponseDto;
import com.teamsparta.task.user.dto.SignupRequestDto;
import com.teamsparta.task.user.dto.UpdateUserRequestDto;
import com.teamsparta.task.user.model.User;
import com.teamsparta.task.user.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/signup")
    public ResponseEntity<String> signup(@RequestBody SignupRequestDto requestDto) {
        userService.signup(requestDto);
        return ResponseEntity.ok("회원가입이 완료되었습니다.");
    }

    @PostMapping("/login")
    public ResponseEntity<LoginResponseDto> login(@RequestBody LoginRequestDto requestDto) {
        String accessToken = userService.login(requestDto);
        return ResponseEntity.ok(new LoginResponseDto(accessToken));
    }

    @PutMapping("/update")
    public ResponseEntity<String> updateUser(
            @RequestBody UpdateUserRequestDto requestDto,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        User user = userDetails.getUser(); // 현재 로그인한 사용자 정보 가져오기

        // 요청 DTO의 정보를 사용하여 사용자 정보 업데이트
        userService.updateUser(requestDto, user);

        return ResponseEntity.ok("회원 정보가 수정되었습니다.");
    }

    @DeleteMapping("/delete")
    public ResponseEntity<String> deleteUser(
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        User user = userDetails.getUser();
        userService.deleteUser(user);
        return ResponseEntity.ok("회원 탈퇴가 완료되었습니다.");
    }

}

 

그런데 생각해보니 필수구현에 수정, 삭제는 없었으니 그건 지우기로 했다.

 

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdHJpbmciLCJleHAiOjE3MjE5MjcyODksImlhdCI6MTcyMTkyMzY4OX0.HA-bMHy3rOaF70KA6GLvEseY5wU6_F4l3jhHU5ZvDVc