Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT 로그인 완료 #10

Merged
merged 13 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
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