Skip to content

Commit

Permalink
JWT 로그인 완료 (#10)
Browse files Browse the repository at this point in the history
* feat(User): 회원 정보에 권한 추가

* feat(User): API Docs 설정 추가

* feat(User): 회원가입 API 추가

* feat(Login): Redis에 JWT 정보를 기록

* feat(Login): JWT 로그인 구현

* feat(Login): JWT 재발급 구현

* refactor(Login): Device Tag를 이용하여 자동 회원가입 및 로그인이 가능하도록 처리

* refactor(User): 회원가입 API 제거

* refactor(Login): User 테이블 컬럼명(id->device_tag) 변경, 회원가입시 UUID를 통해 랜덤 비밀번호를 생성하도록 설정

* refactor(Login): 토큰을 헤더에 담아 반환하도록 변경

* refactor(User): 회원가입시 비밀번호를 지정 가능하게 변경

* refactor(User): 비밀번호 생성 로직 강화

* refactor(Test): 미사용 테스트 코드 제거
  • Loading branch information
limehee authored Jul 21, 2024
1 parent 7809b28 commit 97c941c
Show file tree
Hide file tree
Showing 35 changed files with 949 additions and 17 deletions.
7 changes: 4 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ dependencies {

// Security
implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security
implementation 'io.jsonwebtoken:jjwt-api:0.11.5' // JWT 라이브러리
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' // JWT 구현체
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JWT Jackson 모듈
implementation 'io.jsonwebtoken:jjwt-api:0.12.6' // JWT 라이브러리
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' // JWT 구현체
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' // JWT Jackson 모듈

// DB
implementation 'org.mariadb.jdbc:mariadb-java-client:3.4.1' // MariaDB JDBC Driver
implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Redis
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-validation' // Hibernate Validator
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' // Bean Validation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.presentation.dto.response.TokenInfo;
import jakarta.servlet.http.HttpServletRequest;

public interface LoginService {

TokenInfo loginOrRegister(String deviceTag, String password);

TokenInfo reissueToken(HttpServletRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.domain.model.RedisToken;
import com.stempo.api.domain.domain.model.User;
import com.stempo.api.domain.presentation.dto.response.TokenInfo;
import com.stempo.api.global.auth.exception.TokenForgeryException;
import com.stempo.api.global.auth.jwt.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService {

private final UserService userService;
private final RedisTokenService redisTokenService;
private final JwtTokenProvider jwtTokenProvider;

@Override
public TokenInfo loginOrRegister(String deviceTag, String password) {
User user = userService.findById(deviceTag)
.orElseGet(() -> userService.registerUser(deviceTag, password));
return generateAndSaveToken(user);
}

@Override
public TokenInfo reissueToken(HttpServletRequest request) {
String refreshToken = jwtTokenProvider.resolveToken(request);
Authentication authentication = jwtTokenProvider.getAuthentication(refreshToken);
RedisToken redisToken = redisTokenService.findByRefreshToken(refreshToken);

validateUserExistence(authentication);

TokenInfo newTokenInfo = jwtTokenProvider.generateToken(redisToken.getId(), redisToken.getRole());
redisTokenService.saveToken(redisToken.getId(), redisToken.getRole(), newTokenInfo);
return newTokenInfo;
}

private TokenInfo generateAndSaveToken(User loginUser) {
TokenInfo tokenInfo = jwtTokenProvider.generateToken(loginUser.getDeviceTag(), loginUser.getRole());
redisTokenService.saveToken(loginUser.getDeviceTag(), loginUser.getRole(), tokenInfo);
return tokenInfo;
}

private void validateUserExistence(Authentication authentication) {
String id = authentication.getName();
if (!userService.existsById(id)) {
throw new TokenForgeryException("Non-existent user token.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stempo.api.domain.application.service;

public interface PasswordService {
String encodePassword(String password);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.stempo.api.domain.application.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class PasswordServiceImpl implements PasswordService {

private final PasswordEncoder passwordEncoder;

@Override
public String encodePassword(String rawPassword) {
return passwordEncoder.encode(rawPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.domain.model.RedisToken;
import com.stempo.api.domain.domain.model.Role;
import com.stempo.api.domain.presentation.dto.response.TokenInfo;

public interface RedisTokenService {

RedisToken findByAccessToken(String token);

RedisToken findByRefreshToken(String token);

void saveToken(String id, Role role, TokenInfo tokenInfo);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.domain.model.RedisToken;
import com.stempo.api.domain.domain.model.Role;
import com.stempo.api.domain.domain.repository.RedisTokenRepository;
import com.stempo.api.domain.presentation.dto.response.TokenInfo;
import com.stempo.api.global.auth.exception.TokenNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RedisTokenServiceImpl implements RedisTokenService {

private final RedisTokenRepository redisTokenRepository;

@Override
public RedisToken findByAccessToken(String token) {
return redisTokenRepository.findByAccessToken(token)
.orElseThrow(() -> new TokenNotFoundException("존재하지 않는 토큰입니다."));
}

@Override
public RedisToken findByRefreshToken(String token) {
return redisTokenRepository.findByRefreshToken(token)
.orElseThrow(() -> new TokenNotFoundException("존재하지 않는 토큰입니다."));
}

@Override
public void saveToken(String id, Role role, TokenInfo tokenInfo) {
RedisToken redisToken = RedisToken.create(id, role, tokenInfo);
redisTokenRepository.save(redisToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.domain.model.User;

import java.util.Optional;

public interface UserService {

User registerUser(String deviceTag, String password);

Optional<User> findById(String id);

boolean existsById(String id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.stempo.api.domain.application.service;

import com.stempo.api.domain.domain.model.User;
import com.stempo.api.domain.domain.repository.UserRepository;
import com.stempo.api.global.util.PasswordUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

private final UserRepository userRepository;
private final PasswordService passwordService;
private final PasswordUtil passwordUtil;

@Override
public User registerUser(String deviceTag, String password) {
String rawPassword = password != null ? password : passwordUtil.generateStrongPassword();
User user = User.create(deviceTag, rawPassword);
String encodedPassword = passwordService.encodePassword(user.getPassword());
user.updatePassword(encodedPassword);
return userRepository.save(user);
}

@Override
public Optional<User> findById(String id) {
return userRepository.findById(id);
}

@Override
public boolean existsById(String id) {
return userRepository.existsById(id);
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/stempo/api/domain/domain/model/RedisToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.stempo.api.domain.domain.model;

import com.stempo.api.domain.presentation.dto.response.TokenInfo;
import jakarta.persistence.Column;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@RedisHash(value = "refresh", timeToLive = 60 * 60 * 24 * 14)
public class RedisToken {

@Id
@Column(name = "user_id")
private String id;

private Role role;

@Indexed
private String accessToken;

@Indexed
private String refreshToken;

public static RedisToken create(String id, Role role, TokenInfo tokenInfo) {
return RedisToken.builder()
.id(id)
.role(role)
.accessToken(tokenInfo.getAccessToken())
.refreshToken(tokenInfo.getRefreshToken())
.build();
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/stempo/api/domain/domain/model/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.stempo.api.domain.domain.model;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum Role {

USER("ROLE_USER", "Normal User"),
ADMIN("ROLE_ADMIN", "Administrator");

private final String key;
private final String description;
}
28 changes: 28 additions & 0 deletions src/main/java/com/stempo/api/domain/domain/model/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.stempo.api.domain.domain.model;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class User {

private String deviceTag;
private String password;
private Role role;

public static User create(String deviceTag, String password) {
return new User(deviceTag, password, Role.USER);
}

public void updatePassword(String encodedPassword) {
setPassword(encodedPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stempo.api.domain.domain.repository;

import com.stempo.api.domain.domain.model.RedisToken;

import java.util.Optional;

public interface RedisTokenRepository {

Optional<RedisToken> findByAccessToken(String token);

Optional<RedisToken> findByRefreshToken(String token);

void save(RedisToken redisToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stempo.api.domain.domain.repository;

import com.stempo.api.domain.domain.model.User;

import java.util.Optional;

public interface UserRepository {

User save(User user);

Optional<User> findById(String id);

boolean existsById(String id);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.stempo.api.domain.persistence.entity;

import com.stempo.api.domain.domain.model.Role;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
Expand All @@ -21,8 +24,11 @@
public class UserEntity extends BaseEntity {

@Id
private String id;
private String deviceTag;

@Column(nullable = false)
private String password;

@Enumerated(EnumType.STRING)
private Role role;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.stempo.api.domain.persistence.mappper;

import com.stempo.api.domain.domain.model.User;
import com.stempo.api.domain.persistence.entity.UserEntity;

public class UserMapper {

public static UserEntity toEntity(User user) {
return UserEntity.builder()
.deviceTag(user.getDeviceTag())
.password(user.getPassword())
.role(user.getRole())
.build();
}

public static User toDomain(UserEntity entity) {
return User.builder()
.deviceTag(entity.getDeviceTag())
.password(entity.getPassword())
.role(entity.getRole())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stempo.api.domain.persistence.repository;

import com.stempo.api.domain.domain.model.RedisToken;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface RedisTokenJpaRepository extends CrudRepository<RedisToken, String> {

Optional<RedisToken> findByAccessToken(String token);

Optional<RedisToken> findByRefreshToken(String token);
}
Loading

0 comments on commit 97c941c

Please sign in to comment.