Skip to content

Commit 3b7f868

Browse files
authored
[feat] Security Config설정, CustomAuthenticationFilter 설정#1 (#18)
* chore : 코드래빗 한국어 설정 * feat : OAuth&시큐리티 의존성 추가 및 환경변수 등록 * feat : Security Config설정, CustomAuthenticationFilter 설정1
1 parent 51bb0c7 commit 3b7f868

File tree

12 files changed

+566
-3
lines changed

12 files changed

+566
-3
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ out/
3838

3939
### Custom ###
4040
db_dev.mv.db
41-
db_dev.trace.db
41+
db_dev.trace.db
42+
43+
### Environment Variables ###
44+
.env

build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,24 @@ dependencies {
2828
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
2929
implementation("org.springframework.boot:spring-boot-starter-validation")
3030
implementation("org.springframework.boot:spring-boot-starter-web")
31+
32+
implementation("org.springframework.boot:spring-boot-starter-security")
33+
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
3134
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
35+
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
36+
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
37+
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
38+
implementation("me.paulschwarz:spring-dotenv:4.0.0")
3239
compileOnly("org.projectlombok:lombok")
3340
developmentOnly("org.springframework.boot:spring-boot-devtools")
3441
runtimeOnly("com.h2database:h2")
3542
annotationProcessor("org.projectlombok:lombok")
43+
44+
45+
//test
3646
testImplementation("org.springframework.boot:spring-boot-starter-test")
3747
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
48+
testImplementation("org.springframework.security:spring-security-test")
3849
}
3950

4051
tasks.withType<Test> {

db_dev.mv.db

-20 KB
Binary file not shown.

src/main/java/com/back/domain/user/dto/UserDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public static UserDto from(User user) {
2828
.id(user.getId())
2929
.email(user.getEmail())
3030
.nickname(user.getNickname())
31-
.profileImgUrl(user.getProfileImgUrl())
31+
// .profileImgUrl(user.getProfileImgUrl())
3232
.abvDegree(user.getAbvDegree())
3333
.createdAt(user.getCreatedAt())
3434
.updatedAt(user.getUpdatedAt())

src/main/java/com/back/domain/user/entity/User.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
import jakarta.persistence.*;
44
import lombok.*;
5+
import org.springframework.security.core.GrantedAuthority;
6+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
57

68
import java.time.LocalDateTime;
9+
import java.util.ArrayList;
10+
import java.util.Collection;
11+
import java.util.List;
712

813
@Entity
914
@Table(name = "users") // 예약어 충돌 방지를 위해 "users" 권장
@@ -37,4 +42,26 @@ public class User {
3742
private String role = "USER";
3843

3944
private String profileImgUrl;
45+
46+
public boolean isAdmin() {
47+
return "ADMIN".equalsIgnoreCase(role);
48+
}
49+
50+
private List<String> getAuthoritiesAsStringList() {
51+
List<String> authorities = new ArrayList<>();
52+
if (isAdmin()) {
53+
authorities.add("ADMIN");
54+
} else {
55+
authorities.add("USER");
56+
}
57+
return authorities;
58+
}
59+
60+
// Member의 role을 Security가 사용하는 ROLE_ADMIN, ROLE_USER 형태로 변환
61+
public Collection<? extends GrantedAuthority> getAuthorities() {
62+
return getAuthoritiesAsStringList()
63+
.stream()
64+
.map(auth -> new SimpleGrantedAuthority("ROLE_" + auth))
65+
.toList();
66+
}
4067
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.back.global.jwt;
2+
3+
import io.jsonwebtoken.*;
4+
import io.jsonwebtoken.security.Keys;
5+
import jakarta.servlet.http.Cookie;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.stereotype.Component;
11+
12+
import javax.crypto.SecretKey;
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.Date;
15+
16+
@Slf4j
17+
@Component
18+
public class JwtUtil {
19+
20+
private final SecretKey secretKey;
21+
private final long accessTokenExpiration;
22+
private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";
23+
24+
public JwtUtil(@Value("${custom.jwt.secretKey}") String secretKey,
25+
@Value("${custom.accessToken.expirationSeconds}") long accessTokenExpiration) {
26+
this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
27+
this.accessTokenExpiration = accessTokenExpiration * 1000;
28+
}
29+
30+
public String generateAccessToken(Long userId, String email) {
31+
Date now = new Date();
32+
Date expiration = new Date(now.getTime() + accessTokenExpiration);
33+
34+
return Jwts.builder()
35+
.subject(String.valueOf(userId))
36+
.claim("email", email)
37+
.issuedAt(now)
38+
.expiration(expiration)
39+
.signWith(secretKey)
40+
.compact();
41+
}
42+
43+
public void addAccessTokenToCookie(HttpServletResponse response, String accessToken) {
44+
Cookie cookie = new Cookie(ACCESS_TOKEN_COOKIE_NAME, accessToken);
45+
cookie.setHttpOnly(true);
46+
cookie.setSecure(false); // 개발환경에서는 false, 프로덕션에서는 true
47+
cookie.setPath("/");
48+
cookie.setMaxAge((int) (accessTokenExpiration / 1000));
49+
response.addCookie(cookie);
50+
}
51+
52+
public String getAccessTokenFromCookie(HttpServletRequest request) {
53+
Cookie[] cookies = request.getCookies();
54+
if (cookies != null) {
55+
for (Cookie cookie : cookies) {
56+
if (ACCESS_TOKEN_COOKIE_NAME.equals(cookie.getName())) {
57+
return cookie.getValue();
58+
}
59+
}
60+
}
61+
return null;
62+
}
63+
64+
public void removeAccessTokenCookie(HttpServletResponse response) {
65+
Cookie cookie = new Cookie(ACCESS_TOKEN_COOKIE_NAME, null);
66+
cookie.setHttpOnly(true);
67+
cookie.setSecure(false);
68+
cookie.setPath("/");
69+
cookie.setMaxAge(0);
70+
response.addCookie(cookie);
71+
}
72+
73+
public boolean validateToken(String token) {
74+
try {
75+
Jwts.parser()
76+
.verifyWith(secretKey)
77+
.build()
78+
.parseSignedClaims(token);
79+
return true;
80+
} catch (SecurityException | MalformedJwtException e) {
81+
log.error("Invalid JWT signature: {}", e.getMessage());
82+
} catch (ExpiredJwtException e) {
83+
log.error("Expired JWT token: {}", e.getMessage());
84+
} catch (UnsupportedJwtException e) {
85+
log.error("Unsupported JWT token: {}", e.getMessage());
86+
} catch (IllegalArgumentException e) {
87+
log.error("JWT claims string is empty: {}", e.getMessage());
88+
}
89+
return false;
90+
}
91+
92+
public Long getUserIdFromToken(String token) {
93+
return Long.valueOf(parseToken(token).getSubject());
94+
}
95+
96+
public String getEmailFromToken(String token) {
97+
return parseToken(token).get("email").toString();
98+
}
99+
100+
public String getNicknameFromToken(String token) {
101+
return parseToken(token).get("nickname", String.class);
102+
}
103+
104+
private Claims parseToken(String token) {
105+
return Jwts.parser()
106+
.verifyWith(secretKey)
107+
.build()
108+
.parseSignedClaims(token)
109+
.getPayload();
110+
}
111+
112+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.back.global.rq;
2+
3+
import com.back.domain.user.entity.User;
4+
import com.back.domain.user.service.UserService;
5+
import com.back.global.security.SecurityUser;
6+
import jakarta.servlet.http.Cookie;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.SneakyThrows;
11+
import org.springframework.http.ResponseCookie;
12+
import org.springframework.security.core.Authentication;
13+
import org.springframework.security.core.GrantedAuthority;
14+
import org.springframework.security.core.context.SecurityContextHolder;
15+
import org.springframework.stereotype.Component;
16+
17+
import java.util.Arrays;
18+
import java.util.Optional;
19+
20+
@Component
21+
@RequiredArgsConstructor
22+
public class Rq {
23+
private final HttpServletRequest req;
24+
private final HttpServletResponse resp;
25+
private final UserService userService;
26+
27+
public User getActor() {
28+
return Optional.ofNullable(
29+
SecurityContextHolder
30+
.getContext()
31+
.getAuthentication()
32+
)
33+
.map(Authentication::getPrincipal)
34+
.filter(principal -> principal instanceof SecurityUser)
35+
.map(principal -> {
36+
SecurityUser securityUser = (SecurityUser) principal;
37+
// 권한에서 ROLE_ 접두사를 제거하여 ADMIN/USER로 변환
38+
String role = securityUser.getAuthorities().stream()
39+
.map(GrantedAuthority::getAuthority)
40+
.filter(auth -> auth.startsWith("ROLE_"))
41+
.map(auth -> auth.substring(5)) // "ROLE_" 제거
42+
.findFirst()
43+
.orElse("USER");
44+
return User.builder()
45+
.id(securityUser.getId())
46+
.email(securityUser.getEmail())
47+
.nickname(securityUser.getName())
48+
.role(role)
49+
.build();
50+
})
51+
.orElse(null);
52+
}
53+
54+
public String getHeader(String name, String defaultValue) {
55+
return Optional
56+
.ofNullable(req.getHeader(name))
57+
.filter(headerValue -> !headerValue.isBlank())
58+
.orElse(defaultValue);
59+
}
60+
61+
public void setHeader(String name, String value) {
62+
63+
if (value.isBlank()) {
64+
req.removeAttribute(name);
65+
} else {
66+
resp.setHeader(name, value);
67+
}
68+
}
69+
70+
public String getCookieValue(String name, String defaultValue) {
71+
return Optional
72+
.ofNullable(req.getCookies())
73+
.flatMap(
74+
cookies ->
75+
Arrays.stream(cookies)
76+
.filter(cookie -> cookie.getName().equals(name))
77+
.map(Cookie::getValue)
78+
.filter(value -> !value.isBlank())
79+
.findFirst()
80+
)
81+
.orElse(defaultValue);
82+
}
83+
84+
public void setCrossDomainCookie(String name, String value, int maxAge) {
85+
ResponseCookie cookie = ResponseCookie.from(name, value)
86+
.path("/")
87+
.maxAge(maxAge)
88+
.secure(true)
89+
.sameSite("None")
90+
.httpOnly(true)
91+
.build();
92+
resp.addHeader("Set-Cookie", cookie.toString());
93+
}
94+
95+
public void deleteCrossDomainCookie(String name) {
96+
setCrossDomainCookie(name, "", 0);
97+
}
98+
99+
@SneakyThrows
100+
public void sendRedirect(String url) {
101+
resp.sendRedirect(url);
102+
}
103+
104+
public User getActorFromDb() {
105+
User actor = getActor();
106+
if(actor == null) return null;
107+
return userService.findById(actor.getId());
108+
}
109+
}

0 commit comments

Comments
 (0)