카테고리 없음
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 나누기
- [ ] StatusCode를 다르게 정의하고 싶다면 참고하세요.
- [ ] https://hongong.hanbit.co.kr/http-상태-코드-표-1xx-5xx-전체-요약-정리/
- [ ] 공통조건
- [ ] 회원가입
- 사용자의 정보를 전달 받아 유저 정보를 저장한다.사용자 필드 데이터 유형
아이디 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