RestAPI 명세
HTTP Method 분류 URI 비고
POST | 회원 가입 | /api/v1/auth/signup | 인증 필요 X |
POST | 로그인 | /api/v1/auth/signin | 인증 필요 X |
GET | 토큰 갱신 | /api/v1/auth/renew | RefreshToken 사용 |
GET | 담당 고객 조회 | /api/v1/customers |
회원가입 RestAPI 만들기
회원가입을 위한 DTO 생성
알아보면 좋은 토픽 ⇒ DTO는 왜 쓰는건가요?
회원 가입 시 필요한 정보는 name, email, password 정보입니다.
따라서 이를 위한 SignUpRequestDTO, SignUpResponseDTO를 만듭시다.
dto 패키지 생성 후 그 안에 만들어주세요
SignUpRequestDTO.java
package com.toy.filterloginpjt.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class SignUpRequestDTO {
private String email;
private String name;
private String password;
@Builder
public SignUpRequestDTO(String email, String name, String password) {
this.email = email;
this.name = name;
this.password = password;
}
}
SignUpResponseDTO.java
package com.toy.filterloginpjt.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class SignUpResponseDTO {
private String email;
private String name;
@Builder
private SignUpResponseDTO(String email, String name) {
this.email = email;
this.name = name;
}
}
UserRepository.java 작성
package com.toy.filterloginpjt.repository;
import com.toy.filterloginpjt.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
}
UserService.java
package com.toy.filterloginpjt.service;
import com.toy.filterloginpjt.dto.SignUpRequestDTO;
import com.toy.filterloginpjt.dto.SignUpResponseDTO;
import com.toy.filterloginpjt.entity.User;
import com.toy.filterloginpjt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public SignUpResponseDTO signUp(SignUpRequestDTO requestDTO) {
if(userRepository.existsByEmail(requestDTO.getEmail())) {
throw new IllegalArgumentException("이미 존재하는 이메일입니다");
}
User user = User.builder()
.email(requestDTO.getEmail())
.name(requestDTO.getName())
.password(requestDTO.getPassword())
.deletedYn(false)
.authority("ROLE_USER")
.build();
userRepository.save(user);
SignUpResponseDTO responseDTO = SignUpResponseDTO.builder()
.email(requestDTO.getEmail())
.name(requestDTO.getName())
.build();
return responseDTO;
}
}
- 토픽 ⇒ @Transactional
Controller 작성
controller 패키지 생성 후 다음과 같이 UserController.java 클래스를 생성합시다.
package com.toy.filterloginpjt.controller;
import com.toy.filterloginpjt.dto.SignUpRequestDTO;
import com.toy.filterloginpjt.dto.SignUpResponseDTO;
import com.toy.filterloginpjt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/api/v1/auth/signup")
public ResponseEntity<SignUpResponseDTO> signUp(@RequestBody SignUpRequestDTO requestDTO) {
SignUpResponseDTO responseDTO = userService.signUp(requestDTO);
return new ResponseEntity<SignUpResponseDTO>(responseDTO, HttpStatus.CREATED);
}
}
UnitTest
package com.toy.filterloginpjt;
import com.toy.filterloginpjt.dto.SignUpRequestDTO;
import com.toy.filterloginpjt.dto.SignUpResponseDTO;
import com.toy.filterloginpjt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@SpringBootTest
@AutoConfigureMockMvc
class FilterloginpjtApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
void contextLoads() {
}
@Test
public void testSignUp() throws Exception {
// given
String jsonRequest = "{\\"email\\": \\"test@example.com\\", " +
"\\"name\\": \\"testName\\", " +
"\\"password\\": \\"testPassword\\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andExpect(MockMvcResultMatchers.status().isCreated());
}
}
Postman
Signin(로그인) 구현하기
JWT 생성을 위해 필요한 값 설정
토큰 암호 키와 토큰 수명을 추가해줍니다.
screkt-key의 경우 실제 배포 시에는 외부에서 주입시켜 주어야 합니다(Ci/CD툴 사용)!!
application.yml
spring:
# DB설정
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:filterdb
username: sa
password:
# JPA
jpa:
defer-datasource-initialization: true
show-sql: true
hibernate:
ddl-auto: create
generate-ddl: true
dialect: org.hibernate.dialect.MariaDBDialect
jwt:
secret-key: asdasdwd@asd@DasdDADWsdadsd@wg@dhjgadhasgdd@wgSDAwSadsbdjhA@SDwdSADSAWwdwasDWDASdwdaSDWdasdwdas@DNwdASNdAdawdjkahsdjkhwuadhwkdh2djkhda
life:
atk: 300000
rtk: 3000000
JWT 토큰 생성 로직
- 사용자가 회원가입한 계정을 사용해 로그인을 할 경우 토큰을 발급하여 Json에 담아 클라이언트 서버로 반환하는 로직이 필요합니다.
- JWT관련 비지니스 로직을 처리하기 위한 JwtProvider를 만들겠습니다.
PayLoad.java 클래스 만들기
- Jwt에는 유저 정보와 타입(AccessToken, RefreshToken)에 대한 정보가 필요합니다
- 따라서 이를 담기 위한 PayLoad 클래스를 하나 생성하겠습니다.
package com.toy.filterloginpjt.util;
import lombok.Builder;
import lombok.Getter;
/**
* JWT PayLoad에 담기 위한 데이터 객체입니다.
*/
@Getter
public class PayLoad {
private final Long id; // user_id
private final String email; // user email
private final String type; // ATK(AccessToken) or (RTK)(RefreshToken)
private final String authority;
@Builder
public PayLoad(Long id, String email, String type, String authority) {
this.id = id;
this.email = email;
this.type = type;
this.authority = authority;
}
}
DTO 만들기 (SignInRequestDTO / SignInResponseDTO)
SignInRequestDTO
package com.toy.filterloginpjt.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class SignInRequestDTO {
private final String email;
private final String password;
@Builder
public SignInRequestDTO(String email, String password) {
this.email = email;
this.password = password;
}
}
SignInResponseDTO
package com.toy.filterloginpjt.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class SignInResponseDTO {
private final String atk;
private final String rtk;
@Builder
public SignInResponseDTO(String atk, String rtk) {
this.atk = atk;
this.rtk = rtk;
}
}
JwtProvider.java
package com.toy.filterloginpjt.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.toy.filterloginpjt.dto.SignInRequestDTO;
import com.toy.filterloginpjt.redis.RedisDao;
import com.toy.filterloginpjt.repository.UserRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class JwtProvider {
private final UserRepository userRepository;
private final RedisDao redisDao;
private final ObjectMapper objectMapper;
@Value("${spring.jwt.secret-key}")
private String JWT_KEY;
@Value("${spring.jwt.life.atk}")
private Long ATK_LIFE;
@Value("${spring.jwt.life.rtk}")
private Long RTK_LIFE;
// Jwt 생성
public String createToken(PayLoad payLoad) {
SecretKey secretKey = Keys.hmacShaKeyFor(JWT_KEY.getBytes(StandardCharsets.UTF_8)); // UTF-8로 인코딩
Long lifeTime = payLoad.getType().equals("ATK") ? ATK_LIFE : RTK_LIFE;
String jwt = Jwts.builder()
.setIssuer("JwtProject")
.setSubject("JWT Token")
.claim("id", payLoad.getId()) // 클레임과 PayLoad 멤버변수 명이 같아야합니다.
.claim("email", payLoad.getEmail())
.claim("type", payLoad.getType())
.claim("authority", payLoad.getAuthority())
.setExpiration(new Date(new Date().getTime() + lifeTime))
.signWith(secretKey).compact();
if (payLoad.getType().equals("RTK")) {
redisDao.setValues(payLoad.getEmail(), jwt, Duration.ofMillis(lifeTime));
}
return jwt;
}
// Jwt 유효성 검사를 위해 토큰에서 PayLoad 추출하기
public PayLoad getPayLoad(String jwt) throws JsonProcessingException {
SecretKey secretKey = Keys.hmacShaKeyFor(JWT_KEY.getBytes(StandardCharsets.UTF_8));
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
System.out.println(claims);
PayLoad payLoad = PayLoad.builder()
.id(Long.valueOf((Integer) claims.get("id")))
.email((String) claims.get("email"))
.type((String) claims.get("type"))
.authority((String) claims.get("authority"))
.build();
return payLoad;
}
}
SignIn 컨트롤러 추가하기
UserController.java
package com.toy.filterloginpjt.controller;
import com.toy.filterloginpjt.dto.SignInRequestDTO;
import com.toy.filterloginpjt.dto.SignInResponseDTO;
import com.toy.filterloginpjt.dto.SignUpRequestDTO;
import com.toy.filterloginpjt.dto.SignUpResponseDTO;
import com.toy.filterloginpjt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.apache.coyote.Response;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
...
@PostMapping("/api/v1/auth/signin")
public ResponseEntity<SignInResponseDTO> signIn(@RequestBody SignInRequestDTO requestDTO) {
SignInResponseDTO responseDTO = userService.signIn(requestDTO);
return new ResponseEntity<SignInResponseDTO>(responseDTO, HttpStatus.ACCEPTED);
}
...
}
확인
- 사전에 먼저 SignUp API사용해서 DB에 유저 정보를 저장해놔야 합니다.
Redis에 RefreshToken 추가하기
이제 토큰 생성까지 완료가 되었습니다. 그러나 아직 RefreshToken을 Redis에 저장하는 로직이 없습니다. 이를 위해 우선 스프링부트 앱과 Redis를 연동시켜야 합니다.
해당 실습을 위해 미리 Redis를 설치해주세요.
application.yml
spring:
...
data:
redis:
host: redis
port: 6379
logging:
level:
org:
springframework:
security=DEBUG:
- redis 연동을 위해 위와 같이 application.yml파일을 수정해줍시다!
- port 번호는 redis의 경우 디폴트 값이 6379입니다.
RedisDao.java
- RedisTemplate을 위한 클래스입니다.
package com.toy.filterloginpjt.redis;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class RedisDao {
private final RedisTemplate<String, String> redisTemplate;
public RedisDao(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setValues(String key, String data) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(key, data);
}
public void setValues(String key, String data, Duration duration) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(key, data, duration);
}
public String getValues(String key) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
return values.get(key);
}
public void deleteValues(String key) {
redisTemplate.delete(key);
}
}
RedisConfig.java
package com.toy.filterloginpjt.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
Redis에 RefreshToken저장하기
package com.toy.filterloginpjt.util;
import com.toy.filterloginpjt.dto.SignInRequestDTO;
import com.toy.filterloginpjt.redis.RedisDao;
import com.toy.filterloginpjt.repository.UserRepository;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class JwtProvider {
private final UserRepository userRepository;
private final RedisDao redisDao;
@Value("${spring.jwt.secret-key}")
private String JWT_KEY;
@Value("${spring.jwt.life.atk}")
private Long ATK_LIFE;
@Value("${spring.jwt.life.rtk}")
private Long RTK_LIFE;
// Jwt 생성
public String createToken(PayLoad payLoad) {
SecretKey secretKey = Keys.hmacShaKeyFor(JWT_KEY.getBytes(StandardCharsets.UTF_8)); // UTF-8로 인코딩
Long lifeTime = payLoad.getType().equals("ATK") ? ATK_LIFE : RTK_LIFE;
String jwt = Jwts.builder()
.setIssuer("JwtProject")
.setSubject("JWT Token")
.claim("user_id", payLoad.getId())
.claim("email", payLoad.getEmail())
.claim("type", payLoad.getType())
.claim("authority", payLoad.getAuthority())
.setExpiration(new Date(new Date().getTime() + lifeTime))
.signWith(secretKey).compact();
if (payLoad.getType().equals("RTK")) {
redisDao.setValues(payLoad.getEmail(), jwt, Duration.ofMillis(lifeTime));
}
return jwt;
}
// Jwt 유효성 검사
}
Unit Test
package com.toy.filterloginpjt;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.toy.filterloginpjt.dto.SignUpRequestDTO;
import com.toy.filterloginpjt.dto.SignUpResponseDTO;
import com.toy.filterloginpjt.entity.User;
import com.toy.filterloginpjt.redis.RedisDao;
import com.toy.filterloginpjt.repository.UserRepository;
import com.toy.filterloginpjt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@AutoConfigureMockMvc
class FilterloginpjtApplicationTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Autowired
private RedisDao redisDao;
@Test
void contextLoads() {
}
@Test
public void testSignUp() throws Exception {
// given
String jsonRequest = "{\\"email\\": \\"test@example.com\\", " +
"\\"name\\": \\"testName\\", " +
"\\"password\\": \\"testPassword\\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andExpect(MockMvcResultMatchers.status().isCreated());
User user = userRepository.findByEmail("test@example.com");
userRepository.delete(user);
}
@Test
public void testSignIn() throws Exception {
// given
String SignUpRequest = "{\\"email\\": \\"test@example.com\\", " +
"\\"name\\": \\"testName\\", " +
"\\"password\\": \\"testPassword\\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(SignUpRequest))
.andExpect(MockMvcResultMatchers.status().isCreated());
// when
String SignInRequest = "{\\"email\\": \\"test@example.com\\", " +
"\\"password\\": \\"testPassword\\"}";
MockHttpServletResponse response = mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/signin")
.contentType(MediaType.APPLICATION_JSON)
.content(SignInRequest))
.andExpect(MockMvcResultMatchers.status().isAccepted())
.andReturn()
.getResponse();
System.out.println(redisDao.getValues("test@example.com"));
System.out.println("================================================");
// then
assertThat(redisDao.getValues("test@example.com")).isNotEmpty();
}
}
'SPRING > SECURITY' 카테고리의 다른 글
스프링 시큐리티 인증/인가 리팩토링 (0) | 2024.06.20 |
---|---|
[SpringSecurity6] 메서드 수준 보안 ( with JWT ) (0) | 2024.05.24 |
[토이프로젝트] SpringSecurity6 + JWT + Redis 인증/인가 구현 (3) (0) | 2024.05.14 |
[토이프로젝트] SpringSecurity6 + JWT + Redis 인증/인가 구현 (1) (0) | 2024.05.14 |