1. 개발환경
카테고리 | 도구/기술 |
프로그래밍 언어 | Java 17 |
프레임워크 | Spring Boot 3.4.2 |
빌드 도구 | gradle |
데이터베이스 | MySQL |
ORM 도구 | Spring Data JPA |
IDE | IntelliJ |
버전 관리 | Git, GitHub, GitLab |
패키지 관리 | Maven Central |
API 문서화 | Swagger |
로깅 | SLF4J |
2. build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.2'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// MySQL db
runtimeOnly 'com.mysql:mysql-connector-j'
// JPA 사용
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Web 스타터 - 아파치 톰켓, Web MVC, JSON 직렬화&역직렬화, validation
implementation 'org.springframework.boot:spring-boot-starter-web'
// getter, setter 등의 어노테이션
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// 테스트 관련
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
3. 디렉토리 구조
📂 src
├── 📂 main
│ ├── 📂 java
│ │ └── 📂 com.example.springboot3template
│ │ ├── 📄 Springboot3templateApplication.java
│ │ │
│ │ ├── 📂 auth
│ │ │ ├── 📂 application # 🌟 애플리케이션 서비스 계층
│ │ │ │ ├── 📂 dto
│ │ │ │ │ ├── 📂 mapper
│ │ │ │ │ ├── 📂 req
│ │ │ │ │ │ ├── 📄 LoginReq.java
│ │ │ │ │ │ ├── 📄 SignUpReq.java
│ │ │ │ │ ├── 📂 res
│ │ │ │ ├── 📂 service
│ │ │ │ │ ├── 📄 AuthService.java
│ │ │ │
│ │ │ ├── 📂 domain # 🌟 도메인 계층 (엔티티)
│ │ │ │ ├── 📂 entity
│ │ │ │ │ ├── 📄 User.java
│ │ │ │ │ ├── 📄 UserRoleEnum.java
│ │ │ │
│ │ │ ├── 📂 infrastructure # 🌟 인프라 계층 (데이터 저장소)
│ │ │ │ ├── 📂 repository
│ │ │ │ │ ├── 📄 UserRepository.java
│ │ │ │
│ │ │ ├── 📂 presentation # 🌟 프레젠테이션 계층 (컨트롤러)
│ │ │ │ ├── 📂 controller
│ │ │ │ │ ├── 📄 AuthController.java
│ │ │ │
│ │ ├── 📂 common
│ │ │ ├── 📂 security
│ │ │ │ ├── 📄 JwtProvider.java
│ │ │ │ ├── 📄 SecurityConfig.java
│ │
│ ├── 📂 resources
│ │ ├── 📄 application-local.yml
│ │ ├── 📄 application.yml
│ │ ├── 📂 static
│ │ ├── 📂 templates
4. 코드 흐름
1. postman
포스트맨으로 아래와 같이 로그인 요청
POST
http://localhost:8080/api/v1/auth/login
application/json
{
"username" : "testuser",
"password" : "test1234"
}
2. AuthController
컨트롤러에서 @RequestBody 어노테이션을 통해 JSON을 LoginReq라는 DTO로 변환한다.
// 로그인
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginReq req, HttpServletResponse res) {
authService.login(req, res);
return ResponseEntity.ok().build();
}
3. LoginReq
@Data
public class LoginReq {
private String username;
private String password;
}
4. AuthService
받아온 정보를 주입해 로그인 서비스 로직 실행 repository를 통해 사용자가 있는지 비밀번호가 일치하는지 조회한 후 문제가 없다면 jwtProvider를 통해 토큰을 생성하고 생성된 토큰을 쿠키에 저장한다.
// 로그인
@Transactional
public void login(LoginReq req, HttpServletResponse res) {
String username = req.getUsername();
String password = req.getPassword();
// 사용자 확인
User user = userRepository.findByUsername(username).orElseThrow(
() -> new IllegalArgumentException("등록된 사용자가 없습니다.")
);
// 비밀번호 확인
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
// JWT 생성 및 쿠키에 저장 후 Response 객체에 추가
String token = jwtProvider.createToken(user.getUsername(), user.getRole());
jwtProvider.addJwtToCookie(token, res);
}
5. JwtProvider
.setSubject(username) : 누구에 대한 토큰인지 명시
.claim(AUTHORIZATION_KEY, role) : key - value 형태로 원하는 값을 저장할 수 있다.
.setIssuedAt() : 발급 시간
.setExpiration() : 만료 시간
.signWith(key, signatureAlgorithm) : 암호화 알고리즘
.compact() : JWT문자열 생성
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한
.setIssuedAt(date) // 발급일
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
JWT 토큰은 변조를 방지하기 위해 서명을 하는데 이 때 인코딩된 시크릿키를 디코딩한 후 서명 알고리즘에 사용할 수 있는 키 객체를 생성한다.
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
if (secretKey == null || secretKey.isEmpty()) {
throw new IllegalStateException("Secret key is not set, in JwtProvider");
}
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
생성한 토큰은 쿠키에 저장해 사용한다.
.setPath("/") : 쿠키의 유효 경로를 설정
.setSecure(true) : 쿠키가 HTTPS 연결을 통해서만 전송되도록 설정
.setMaxAge(3600 * 1000) : 쿠키의 만료시간을 설정, 일반적으로 토큰과 같은 시간으로 설정한다.
// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
token = substringToken(token);
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge(3600 * 1000); // 60분
// Response 객체에 Cookie 추가
res.addCookie(cookie);
log.info("쿠키가 response에 추가되었습니다.");
}
토큰 앞의 bearer 은 OAuth 2.0 인증 프로토콜에서 사용되는 인증 방식 중 하나로 "소지자가 권한을 가진" 이라는 뜻으로 HTTP 헤더에 포함되어 요청을 인증하는데 사용된다. 하지만 쿠키에서는 실질적인 토큰 값만이 필요하기 때문에 bearer과 뒤의 공백 1자리까지 총 7자리가 필요없기 때문에 쿠키에 넣는 과정에서 삭제한다.
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
log.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
6. SecurityConfig
회원가입과 로그인의 경우 시큐리티에서 걸리지 않아야 하기 떄문에
.requestMatchers("/api/v1/auth/login").permitAll() 을 통해 통과 시켜준다.
@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
// Session 비활성화
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 요청 권한 설정
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
// api 요청 허가
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/sign-up").permitAll()
// HTML 페이지 접근을 인증 없이 허용
// .requestMatchers("/").permitAll()
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
return http.build();
}
}
'Spring Boot > Sprng Boot Sample Code' 카테고리의 다른 글
Spring Boot 에서 네이버 클라우드 Object Storage 에 파일 업로드 (1) | 2025.03.03 |
---|---|
[SpringBoot 3.x] 4. @PreAuthorize 어노테이션을 이용한 API 인가 설정 (0) | 2025.03.03 |
[Spring Boot 3.x] 3. JWT 토큰 인증(filter) & API 테스트 (0) | 2025.03.01 |
[Spring Boot 3.x] 1. 회원가입 구현 (1) | 2025.02.02 |
build.gradle 이해하기 (1) | 2025.01.30 |