Skip to content

Commit

Permalink
sync branch from main
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondanythings committed Jul 5, 2024
2 parents 283a8ef + 3792b42 commit 2214c04
Show file tree
Hide file tree
Showing 19 changed files with 491 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# auth
layer-api/src/main/resources/application-auth.properties

HELP.md
.gradle
build/
Expand Down
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ project(":layer-api") {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

//== jwt ==//
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

// oauth2-client 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
28 changes: 28 additions & 0 deletions layer-api/src/main/java/org/layer/auth/api/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.layer.auth.api;

import lombok.RequiredArgsConstructor;
import org.layer.auth.jwt.JwtToken;
import org.layer.auth.service.JwtService;
import org.layer.member.MemberRole;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class AuthController {
private final JwtService jwtService;

// 테스트용 임시 컨트롤러입니다. (토큰 없이 접속 가능)
// "/create-token?id=멤버아이디" uri로 get 요청을 보내면 토큰이 발급됩니다.
@GetMapping("/create-token")
public JwtToken authTest(@RequestParam("id") Long memberId) {
return jwtService.issueToken(memberId, MemberRole.USER);
}

// header에 액세스 토큰을 넣어 요청을 보내면 인증됩니다.
@GetMapping("/authentication-test")
public String authTest() {
return "인증 성공";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.layer.auth.exception;

// 임시로 만들어놓은 Exception입니다
public class TokenException extends RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.layer.auth.jwt;

import com.nimbusds.oauth2.sdk.Role;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.layer.member.Member;
import org.layer.member.MemberRole;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final JwtValidator jwtValidator;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String accessToken = getJwtFromRequest(request);

if(isValidToken(accessToken)) {
Long memberId = jwtValidator.getMemberIdFromToken(accessToken);
List<String> role = jwtValidator.getRoleFromToken(accessToken);
setAuthenticationToContext(memberId, MemberRole.valueOf(role.get(0)));
}
filterChain.doFilter(request, response);
}

// Spring Security Context에 저장
private void setAuthenticationToContext(Long memberId, MemberRole memberRole) {
Authentication authentication = MemberAuthentication.create(memberId, memberRole);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

// 요청 헤더에서 액세스 토큰을 가져오는 메서드
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
return (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) ? bearerToken.replace("Bearer ", ""): null;
}

// 정상적인 토큰인지 판단하는 메서드
private boolean isValidToken(String token) {
return StringUtils.hasText(token) && jwtValidator.validateToken(token) == JwtValidationType.VALID_JWT;
}
}
31 changes: 31 additions & 0 deletions layer-api/src/main/java/org/layer/auth/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.layer.auth.jwt;

import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@RequiredArgsConstructor
@Component
public class JwtProvider {
private final SecretKeyFactory secretKeyFactory;

public String createToken(Authentication authentication, Long tokenExpirationTime) {
Map<String, Object> claims = new HashMap<>();
claims.put("memberId", authentication.getPrincipal());
claims.put("role", authentication.getAuthorities());

Date now = new Date();

return Jwts.builder()
.issuedAt(now)
.expiration(new Date(now.getTime() + tokenExpirationTime))
.claims(claims)
.signWith(secretKeyFactory.createSecretKey())
.compact();
}
}
16 changes: 16 additions & 0 deletions layer-api/src/main/java/org/layer/auth/jwt/JwtToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.layer.auth.jwt;

import lombok.Builder;
import lombok.Getter;

@Getter
public class JwtToken {
private final String accessToken;
private final String refreshToken;

@Builder
public JwtToken(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.layer.auth.jwt;

public enum JwtValidationType {
VALID_JWT,
INVALID_JWT;
}

48 changes: 48 additions & 0 deletions layer-api/src/main/java/org/layer/auth/jwt/JwtValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.layer.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.layer.auth.exception.TokenException;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.List;

import static org.layer.auth.jwt.JwtValidationType.*;

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtValidator {
private final SecretKeyFactory secretKeyFactory;

public JwtValidationType validateToken(String token) {
try {
getClaims(token);
return VALID_JWT;
} catch(TokenException e) {
return INVALID_JWT;
}
}

public long getMemberIdFromToken(String token) {
Claims claims = getClaims(token);
return Long.parseLong(claims.get("memberId").toString());
}

public List<String> getRoleFromToken(String token) throws TokenException {
Claims claims = getClaims(token);
return (List<String>) (claims.get("role"));

}

private Claims getClaims(String token) {
return Jwts.parser()
.verifyWith(secretKeyFactory.createSecretKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.layer.auth.jwt;

import lombok.Builder;
import lombok.RequiredArgsConstructor;
import org.layer.member.MemberRole;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;
import java.util.Collections;

public class MemberAuthentication extends UsernamePasswordAuthenticationToken {
@Builder
public MemberAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}

public static MemberAuthentication create(Object principal, MemberRole role) {
return MemberAuthentication.builder()
.principal(principal)
.credentials(null)
.authorities(Collections.singleton(role))
.build();
}

}
18 changes: 18 additions & 0 deletions layer-api/src/main/java/org/layer/auth/jwt/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.layer.auth.jwt;

import lombok.Builder;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;

@Builder
@Getter
@RedisHash(value = "refreshToken")
public class RefreshToken {
@Id
private Long memberId;
private String token;
@TimeToLive
private long ttl;
}
25 changes: 25 additions & 0 deletions layer-api/src/main/java/org/layer/auth/jwt/SecretKeyFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.layer.auth.jwt;

import io.jsonwebtoken.security.Keys;
import java.util.Base64;
import javax.crypto.SecretKey;

import lombok.RequiredArgsConstructor;
import org.layer.config.AuthValueConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import static java.util.Base64.getEncoder;

@RequiredArgsConstructor
@Component
public class SecretKeyFactory {
private final AuthValueConfig authValueConfig;

SecretKey createSecretKey() {
String encodedKey = getEncoder().encodeToString(authValueConfig.getJWT_SECRET().getBytes());
return Keys.hmacShaKeyFor(encodedKey.getBytes());
}

}
53 changes: 53 additions & 0 deletions layer-api/src/main/java/org/layer/auth/service/JwtService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.layer.auth.service;

import lombok.RequiredArgsConstructor;
import org.layer.auth.exception.TokenException;
import org.layer.auth.jwt.*;
import org.layer.config.AuthValueConfig;
import org.layer.member.MemberRole;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Objects;

import static org.layer.config.AuthValueConfig.*;

@RequiredArgsConstructor
@Service
public class JwtService {
private final JwtProvider jwtProvider;
private final RedisTemplate<String, Object> redisTemplate;

public JwtToken issueToken(Long memberId, MemberRole memberRole) {
String accessToken = jwtProvider.createToken(MemberAuthentication.create(memberId, memberRole), ACCESS_TOKEN_EXPIRATION_TIME);
String refreshToken = jwtProvider.createToken(MemberAuthentication.create(memberId, memberRole), REFRESH_TOKEN_EXPIRATION_TIME);

saveRefreshTokenToRedis(memberId, memberRole, refreshToken);

return JwtToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}

private void saveRefreshTokenToRedis(Long memberId, MemberRole memberRole, String refreshToken) {
redisTemplate.opsForValue().set(refreshToken, memberId, Duration.ofDays(14));
}

private Long getMemberIdFromRefreshToken(String refreshToken) throws TokenException {
Long memberId = null;
try {
memberId = Long.parseLong((String) Objects.requireNonNull(redisTemplate.opsForValue().get(refreshToken)));
} catch(Exception e) {
throw new TokenException();
}
return memberId;
}

public void deleteRefreshToken(String refreshToken) {
redisTemplate.delete(refreshToken);
}

}
27 changes: 27 additions & 0 deletions layer-api/src/main/java/org/layer/config/AuthValueConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.layer.config;

import jakarta.annotation.PostConstruct;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Getter
@PropertySource("classpath:application-auth.properties")
@Configuration
public class AuthValueConfig {
@Value("${jwt.secret}")
private String JWT_SECRET;

public static final Long ACCESS_TOKEN_EXPIRATION_TIME = 1000 * 60 * 30L; // 30분
public static final Long REFRESH_TOKEN_EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 14L; // 2주


@PostConstruct
protected void init() {
JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
}
}
Loading

0 comments on commit 2214c04

Please sign in to comment.