Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6a6f00c
Feat #3 Auth 구현 파일 추가
CromNEXT Jan 27, 2026
13b39a6
Feat #3 Id타입 String으로 변환 및 Auth 오류 수정
CromNEXT Jan 28, 2026
82197d7
Feat #3 Status추가
CromNEXT Jan 28, 2026
a729543
Feat #3 gradle springdoc-openapi 버전 3.0.0으로 업그레이드
CromNEXT Feb 3, 2026
2f14867
Feat #3 ExceptionHandler가 상세 에러를 반환하도록 수정
CromNEXT Feb 3, 2026
db85b74
Feat #3 GeneralException의 BaseStatus 캐스팅 문제 해결
CromNEXT Feb 3, 2026
4d86cfc
Feat #3 JWT 필터 분기조건 수정
CromNEXT Feb 3, 2026
e8a565c
Feat #3 필드 boolean 명칭 수정
CromNEXT Feb 3, 2026
f074d8a
Feat #3 로그인 조건 수정
CromNEXT Feb 3, 2026
27d7f4d
Feat #3 SignupRequest 비밀번호 마스킹
CromNEXT Feb 3, 2026
fc80592
Feat #3 Swagger 제목 & Token저장하는 곳 추가
CromNEXT Feb 3, 2026
3fbfe4e
Feat #3 Swagger 반환값 Docs 수정
CromNEXT Feb 3, 2026
98f944b
Feat #3 isDelete제거 deleted로 수정
CromNEXT Feb 3, 2026
d2b5085
[Refactor] #4 Penalty Entity 변경
Todom2 Jan 25, 2026
574f8ea
Feat #3 Id타입 String으로 변환 및 Auth 오류 수정
CromNEXT Jan 28, 2026
d696047
Feat #3 충돌 해결
CromNEXT Feb 3, 2026
616de42
[Refactor] #4 Penalty Entity 변경
Todom2 Jan 25, 2026
c4c0c86
Feat #3 Id타입 String으로 변환 및 Auth 오류 수정
CromNEXT Jan 28, 2026
006f3d8
Feat #3 Id타입 String으로 변환 및 Auth 오류 수정
CromNEXT Jan 28, 2026
596d6c7
Feat #3 충돌 해결2
CromNEXT Feb 3, 2026
49d31f7
Feat #3 JWT 에러코드 단순화, 에러로깅 추가 + gitignore 추가
CromNEXT Feb 3, 2026
2217158
Feat #3 PanaltyTypes 필드의 @Enumerated 지정
CromNEXT Feb 3, 2026
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
Binary file modified .gradle/9.2.1/fileHashes/fileHashes.lock
Binary file not shown.
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.13'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

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

tasks.named('test') {
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/ssurent/ssurentbe/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ssurent.ssurentbe.common.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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 ssurent.ssurentbe.common.jwt.JwtAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/swagger-ui/**", "/api-docs/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ssurent.ssurentbe.common.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String token = resolveToken(request);

if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
return null;
}
}
97 changes: 97 additions & 0 deletions src/main/java/ssurent/ssurentbe/common/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package ssurent.ssurentbe.common.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
public class JwtTokenProvider {

private final SecretKey secretKey;
private final long accessTokenValidity;
private final long refreshTokenValidity;
private final UserDetailsService userDetailsService;

public JwtTokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.access-token-validity}") long accessTokenValidity,
@Value("${jwt.refresh-token-validity}") long refreshTokenValidity,
@Lazy UserDetailsService userDetailsService) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
this.accessTokenValidity = accessTokenValidity;
this.refreshTokenValidity = refreshTokenValidity;
this.userDetailsService = userDetailsService;
}

public String createAccessToken(String studentNum) {
return createToken(studentNum, accessTokenValidity, "access");
}

public String createRefreshToken(String studentNum) {
return createToken(studentNum, refreshTokenValidity, "refresh");
}

private String createToken(String studentNum, long validity, String tokenType) {
Date now = new Date();
Date expiration = new Date(now.getTime() + validity);

return Jwts.builder()
.subject(studentNum)
.claim("type", tokenType)
.issuedAt(now)
.expiration(expiration)
.signWith(secretKey)
.compact();
}

public Authentication getAuthentication(String token) {
String studentNum = getStudentNum(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(studentNum);
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

public String getStudentNum(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}

public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

public boolean isRefreshToken(String token) {
try {
String type = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("type", String.class);
return "refresh".equals(type);
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ssurent.ssurentbe.common.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import ssurent.ssurentbe.domain.users.entity.Users;
import ssurent.ssurentbe.domain.users.repository.UserRepository;

import java.util.Collections;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String studentNum) throws UsernameNotFoundException {
Users user = userRepository.findByStudentNum(studentNum)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + studentNum));

return new User(
user.getStudentNum(),
user.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ssurent.ssurentbe.domain.users.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ssurent.ssurentbe.domain.users.dto.LoginRequest;
import ssurent.ssurentbe.domain.users.dto.SignupRequest;
import ssurent.ssurentbe.domain.users.dto.TokenResponse;
import ssurent.ssurentbe.domain.users.service.AuthService;

@Tag(name = "Auth", description = "인증 API")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@Operation(summary = "회원가입", description = "학번, 이름, 전화번호, 비밀번호로 회원가입합니다.")
@PostMapping("/signup")
public ResponseEntity<TokenResponse> signup(@RequestBody SignupRequest request) {
TokenResponse response = authService.signup(request);
return ResponseEntity.ok(response);
}

@Operation(summary = "로그인", description = "학번과 비밀번호로 로그인합니다.")
@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest request) {
TokenResponse response = authService.login(request);
return ResponseEntity.ok(response);
}

@Operation(summary = "토큰 갱신", description = "리프레시 토큰으로 새로운 액세스 토큰을 발급받습니다.")
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refresh(@RequestHeader("Authorization") String authorization) {
String refreshToken = authorization.replace("Bearer ", "");
TokenResponse response = authService.refresh(refreshToken);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ssurent.ssurentbe.domain.users.dto;

public record LoginRequest(
String studentNum,
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ssurent.ssurentbe.domain.users.dto;

public record SignupRequest(
String studentNum,
String name,
String phoneNum,
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ssurent.ssurentbe.domain.users.dto;

public record TokenResponse(
String accessToken,
String refreshToken,
String tokenType
) {
public static TokenResponse of(String accessToken, String refreshToken) {
return new TokenResponse(accessToken, refreshToken, "Bearer");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public class Users extends BaseEntity{
@Column(name = "ph_num")
private String phoneNum;

@Column(name = "password", nullable = false)
private String password;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private Status status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ssurent.ssurentbe.domain.users.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ssurent.ssurentbe.domain.users.entity.Users;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<Users, String> {
Optional<Users> findByStudentNum(String studentNum);
boolean existsByStudentNum(String studentNum);
}
Loading