Skip to content

Commit

Permalink
Merge pull request kakao-tech-campus-2nd-step3#23 from yooonwodyd/weekly
Browse files Browse the repository at this point in the history
로그인 일부 구현
  • Loading branch information
yooonwodyd authored Sep 25, 2024
2 parents dab8a48 + 5cc28b6 commit e598d57
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
HELP.md
.gradle

build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/main/resource/application.yaml
!**/src/test/**/build/

### STS ###
Expand Down
32 changes: 27 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
buildscript {
ext {
spring_boot_version = '3.3.3'
spring_dependency_management = '1.1.6'
}

repositories {
mavenCentral()
}
}

plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.springframework.boot' version "${spring_boot_version}"
id 'io.spring.dependency-management' version "${spring_dependency_management}"
}

group = 'com.helpmeCookies'
Expand All @@ -24,17 +35,28 @@ repositories {
}

dependencies {

// Spring
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-security'

// Lombok
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'

// DB
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Spring docs
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('test') {
Expand Down
125 changes: 125 additions & 0 deletions src/main/java/com/helpmeCookies/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.helpmeCookies.global.jwt;

import java.security.Key;
import java.util.Date;

import javax.crypto.spec.SecretKeySpec;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Component
public class JwtProvider implements InitializingBean {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expire-time}")
private long accessTokenExpireTime;
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;
private Key secretKey;
private static final String ROLE = "role";
private static final String IS_ACCESS_TOKEN = "isAccessToken";
private static final String HEADER_PREFIX = "Bearer ";

public String parseHeader(String header) {
if (header == null || header.isEmpty()) {
throw new IllegalArgumentException("Authorization 헤더가 없습니다.");
} else if (!header.startsWith(HEADER_PREFIX)) {
throw new IllegalArgumentException("Authorization 올바르지 않습니다.");
} else if (header.split(" ").length != 2) {
throw new IllegalArgumentException("Authorization 올바르지 않습니다.");
}

return header.split(" ")[1];
}

public JwtToken createToken(JwtUser jwtUser) {
String accessToken = generateToken(jwtUser, true);
String refreshToken = generateToken(jwtUser, false);
return JwtToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}

// 유요한 토큰인지 확인
public boolean validateToken(String rawToken, boolean isAccessToken) {
try {
// 엑세스 토큰인지 확인
Claims claims = extractClaims(rawToken);
if (claims.get(IS_ACCESS_TOKEN, Boolean.class) != isAccessToken) {
return false;
}
// 만료시간 확인
return !claims.getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}

/**
* refreshToken을 통해, accessToken을 재발급하는 메서드.
* refreshToken의 유효성을 검사하고, isAccessToken이 true일때만 accessToken을 재발급한다.
* TODO: refreshToken을 저장하고, 저장된 refreshToken과 비교하는 로직 필요
*/
public String reissueAccessToken(String refreshToken) {
Claims claims = extractClaims(refreshToken);
if (claims.get(IS_ACCESS_TOKEN, Boolean.class)) {
throw new IllegalArgumentException("리프레시 토큰이 아닙니다.");
}
JwtUser jwtUser = claimsToJwtUser(claims);
return generateToken(jwtUser, true);
}

/**
* [validateToken] 이후 호출하는 메서드.
* rawToken을 통해 JwtUser를 추출한다.
* [jwtUser]는 userId와 role을 가지고 있다. 즉 JWT에 저장된 정보를 추출한다.
*/
public JwtUser getJwtUser(String rawToken) {
Claims claims = extractClaims(rawToken);
return claimsToJwtUser(claims);
}

private JwtUser claimsToJwtUser(Claims claims) {
String userId = claims.getSubject();
return JwtUser.of(Long.parseLong(userId));
}

/**
* Jwt 토큰생성
* accessToken과 refreshToken의 다른점은 만료시간과, isAccessToken이다.
*/
private String generateToken(JwtUser jwtUser, boolean isAccessToken) {
long expireTime = isAccessToken ? accessTokenExpireTime : refreshTokenExpireTime;
Date expireDate = new Date(System.currentTimeMillis() + expireTime);
return Jwts.builder()
.signWith(secretKey)
.claim(IS_ACCESS_TOKEN, isAccessToken)
.setSubject(jwtUser.getId().toString())
.setExpiration(expireDate)
.compact();
}


private Claims extractClaims(String rawToken) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(rawToken)
.getBody();
}

/**
* HS256방식의 키를 생성한다.
*/
@Override
public void afterPropertiesSet() {
secretKey = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/helpmeCookies/global/jwt/JwtToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.helpmeCookies.global.jwt;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class JwtToken {
private String accessToken;
private String refreshToken;
}
57 changes: 57 additions & 0 deletions src/main/java/com/helpmeCookies/global/jwt/JwtUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.helpmeCookies.global.jwt;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class JwtUser implements UserDetails {
private Long id;

public static JwtUser of(Long id) {
return JwtUser.builder()
.id(id)
.build();
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return null;
}

@Override
public boolean isAccountNonExpired() {
return false;
}

@Override
public boolean isAccountNonLocked() {
return false;
}

@Override
public boolean isCredentialsNonExpired() {
return false;
}

@Override
public boolean isEnabled() {
return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.helpmeCookies.global.security;

import java.io.IOException;
import java.io.PrintWriter;

import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) {
log.error("Token : {}", request.getHeader("Authorization"));
// TODO: 에러코드 추가
response.setStatus(403);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.helpmeCookies.global.security;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.debug("Token : {}", request.getHeader("Authorization"));
response.setStatus(401);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.helpmeCookies.global.security;

import java.io.IOException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.helpmeCookies.global.jwt.JwtProvider;
import com.helpmeCookies.global.jwt.JwtUser;

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;

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

private static final String AUTHORIZATION_HEADER = "Authorization";

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.info("JwtAuthenticationFilter");
String rawToken;

// 토큰 추출
try {
rawToken = jwtProvider.parseHeader(request.getHeader(AUTHORIZATION_HEADER));
} catch (Exception e) {
filterChain.doFilter(request, response);
return;
}

// TODO: UserDetailsService를 통해 사용자 정보를 가져와 인증을 진행한다.
if (jwtProvider.validateToken(rawToken, true)) {
JwtUser jwtUser = jwtProvider.getJwtUser(rawToken);
Authentication authentication = new UsernamePasswordAuthenticationToken(jwtUser, null,
jwtUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}
}
Loading

0 comments on commit e598d57

Please sign in to comment.