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

[BE] 김경규 로그인 #7

Open
wants to merge 6 commits into
base: koungq
Choose a base branch
from
Open
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
24 changes: 23 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,30 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
// Lombok
implementation('org.projectlombok:lombok')
compileOnly('org.projectlombok:lombok')
annotationProcessor('org.projectlombok:lombok')
testImplementation('org.projectlombok:lombok')
testAnnotationProcessor('org.projectlombok:lombok')

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

// 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'

// MySQL
runtimeOnly 'mysql:mysql-connector-java:8.2.0'
runtimeOnly 'com.mysql:mysql-connector-j'

}

tasks.named('test') {
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/leets/attendance/common/HttpResponseStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package leets.attendance.common;

import lombok.Getter;

import static org.springframework.http.HttpStatus.*;

@Getter
public enum HttpResponseStatus {
/**
* 200 : 요청 성공
*/
SUCCESS(true, OK.value(), "요청에 성공하였습니다."),


/**
* 400 : Request 오류, Response 오류
*/
MISMATCH_PASSWORD(false, BAD_REQUEST.value(), "비밀번호가 일치하지 않습니다."),
UNCHECKED_ID(false, BAD_REQUEST.value(), "아이디 중복 확인을 해주세요."),
ALREADY_DONE(false, BAD_REQUEST.value(), "이미 처리된 작업입니다."),
INVALID_ACCESS(false, NOT_FOUND.value(), "잘못된 접근입니다."),
LOGIN_ERROR(false, NOT_FOUND.value(), "아이디 혹은 비밀번호를 잘못 입력하였습니다."),

/**
* 500 : Database, Server 오류
*/
DATABASE_ERROR(false, INTERNAL_SERVER_ERROR.value(), "오류가 발생했습니다. 잠시 후에 다시 시도해주세요.");

private final boolean isSuccess;
private final int code;
private final String message;

HttpResponseStatus(boolean isSuccess, int code, String message) {
this.isSuccess = isSuccess;
this.code = code;
this.message = message;
}
}

21 changes: 21 additions & 0 deletions src/main/java/leets/attendance/common/ResponseApiMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package leets.attendance.common;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ResponseApiMessage {

HttpResponseStatus httpResponseStatus;
String message;
Object responseData;

@Builder
public ResponseApiMessage(HttpResponseStatus httpResponseStatus, String message, Object responseData) {
this.httpResponseStatus = httpResponseStatus;
this.message = message;
this.responseData = responseData;
}
}
48 changes: 48 additions & 0 deletions src/main/java/leets/attendance/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package leets.attendance.config;

import leets.attendance.jwt.JwtAuthenticationFilter;
import leets.attendance.jwt.JwtTokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtTokenService jwtTokenService;

@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");


http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenService), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/attendances/**").authenticated()
.requestMatchers("/users/**").permitAll())
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable);

return http.build();
}

@Bean // 해시
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
13 changes: 13 additions & 0 deletions src/main/java/leets/attendance/exception/BaseException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package leets.attendance.exception;

import leets.attendance.common.HttpResponseStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
public class BaseException extends Exception {
private HttpResponseStatus status;
}
42 changes: 42 additions & 0 deletions src/main/java/leets/attendance/jwt/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package leets.attendance.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenService jwtTokenService;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 1. Request Header에서 JWT 토큰 추출
String token = resolveToken((HttpServletRequest) request);

// 2. validateToken으로 토큰 유효성 검사
if (token != null && jwtTokenService.validateToken(token)) {
// 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
Authentication authentication = jwtTokenService.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}

// Request Header에서 토큰 정보 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
return bearerToken.substring(7);
}
return null;
}
}
14 changes: 14 additions & 0 deletions src/main/java/leets/attendance/jwt/JwtToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package leets.attendance.jwt;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Builder
@Data
@AllArgsConstructor
public class JwtToken {
private String grantType; // Bearer
private String accessToken;
private String refreshToken;
}
106 changes: 106 additions & 0 deletions src/main/java/leets/attendance/jwt/JwtTokenService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package leets.attendance.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Collections;
import java.util.Date;
import java.util.stream.Collectors;

@Slf4j
@Component
public class JwtTokenService {

private final Key key;

// application.yml에서 secret 값 가져와서 key에 저장
public JwtTokenService(@Value("${application.security.jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}

// Member 정보를 가지고 AccessToken, RefreshToken을 생성하는 메서드
public JwtToken generateToken(Authentication authentication) {
// 권한 가져오기
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));

long now = (new Date()).getTime();

// Access Token 생성
Date accessTokenExpiresIn = new Date(now + 86400000);
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim("auth", authorities)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();

// Refresh Token 생성
String refreshToken = Jwts.builder()
.setExpiration(new Date(now + 86400000))
.signWith(key, SignatureAlgorithm.HS256)
.compact();

return JwtToken.builder()
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}

// Jwt 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
public Authentication getAuthentication(String accessToken) {
// Jwt 토큰 복호화
Claims claims = parseClaims(accessToken);
// GrantedAuthority를 설정해야 믿을만한 토큰으로 간주하여 임시로 USER라는 authority 넣음
UserDetails principal = new User(claims.getSubject(), "", Collections.singleton(new SimpleGrantedAuthority("USER")));
return new UsernamePasswordAuthenticationToken(principal, "", Collections.singleton(new SimpleGrantedAuthority("USER")));
}

// 토큰 정보를 검증하는 메서드
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("유효하지 않은 토큰입니다.", e);
} catch (ExpiredJwtException e) {
log.info("만료된 토큰입니다.", e);
} catch (UnsupportedJwtException e) {
log.info("지원하지 않는 토큰입니다.", e);
} catch (IllegalArgumentException e) {
log.info("비어있는 토큰입니다.", e);
}
return false;
}


// accessToken
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package leets.attendance.src.controller;

import leets.attendance.common.ResponseApiMessage;
import leets.attendance.exception.BaseException;
import leets.attendance.src.domain.User;
import leets.attendance.src.service.AttendanceService;
import leets.attendance.src.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static leets.attendance.common.HttpResponseStatus.SUCCESS;

@RestController
@RequestMapping("/attendances")
@RequiredArgsConstructor
public class AttendanceController extends BaseController {

private final AttendanceService attendanceService;
private final UserService userService;

@PatchMapping()
public ResponseEntity<ResponseApiMessage> attend(@AuthenticationPrincipal UserDetails userDetails) {
try {
// 인증 객체가 아닌 진짜 유저 객체 가져오기 (인증 객체는 다른 테이블에 대한 정보를 담고 있지 않음)
User user = userService.getUser(userDetails.getUsername());
// 출석 처리
return sendResponseHttpByJson(SUCCESS, attendanceService.attend(user));
} catch (BaseException e) {
return sendResponseHttpByJson(e.getStatus());
}
}

@GetMapping()
public ResponseEntity<ResponseApiMessage> getAttendances(@AuthenticationPrincipal UserDetails userDetails) {
try {
// 인증 객체로 유저 정보 조회
User user = userService.getUser(userDetails.getUsername());
// 객체에 매핑된 출석 객체 반환
return sendResponseHttpByJson(SUCCESS, user.getAttendances());
} catch (BaseException e) {
return sendResponseHttpByJson(e.getStatus());
}
}

@GetMapping("/rates")
public ResponseEntity<ResponseApiMessage> getAttendanceRate(@AuthenticationPrincipal UserDetails userDetails) {
try {
// 인증 객체로 유저 정보 조회
User user = userService.getUser(userDetails.getUsername());
// 출석률 구하기
return sendResponseHttpByJson(SUCCESS, attendanceService.getAttendanceRate(user));
} catch (BaseException e) {
return sendResponseHttpByJson(e.getStatus());
}
}
}
Loading