Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0374595

Browse files
authoredAug 26, 2024··
Merge pull request #5 from techcode-viewer/develop
Develop
2 parents eee380c + 9df9733 commit 0374595

35 files changed

+1349
-26
lines changed
 

‎build.gradle

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,25 @@ repositories {
2424
}
2525

2626
dependencies {
27+
implementation 'javax.annotation:javax.annotation-api:1.3.2'
28+
implementation 'javax.servlet:javax.servlet-api:4.0.1'
2729
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
28-
// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
29-
// implementation 'org.springframework.boot:spring-boot-starter-security'
30+
implementation 'org.springframework.boot:spring-boot-starter-security'
31+
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
32+
implementation 'org.springframework.security:spring-security-oauth2-resource-server'
3033
implementation 'org.springframework.boot:spring-boot-starter-web'
34+
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
35+
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
36+
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
3137
compileOnly 'org.projectlombok:lombok'
3238
runtimeOnly 'com.mysql:mysql-connector-j'
3339
annotationProcessor 'org.projectlombok:lombok'
40+
3441
testImplementation 'org.springframework.boot:spring-boot-starter-test'
35-
testImplementation 'org.springframework.security:spring-security-test'
42+
testImplementation 'org.mockito:mockito-core'
43+
testImplementation 'org.mockito:mockito-junit-jupiter'
44+
testImplementation 'org.junit.jupiter:junit-jupiter-api'
45+
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
3646
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
3747
}
3848

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package codeview.main.auth.controller;
2+
3+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
4+
import org.springframework.security.oauth2.core.user.OAuth2User;
5+
import org.springframework.stereotype.Controller;
6+
import org.springframework.ui.Model;
7+
import org.springframework.web.bind.annotation.GetMapping;
8+
9+
@Controller
10+
public class OAuth2Controller {
11+
12+
@GetMapping("/oauth2/callback/google")
13+
public String googleCallback(@AuthenticationPrincipal OAuth2User principal, Model model) {
14+
model.addAttribute("name", principal.getAttribute("name"));
15+
return "home";
16+
}
17+
18+
@GetMapping("/oauth2/callback/github")
19+
public String githubCallback(@AuthenticationPrincipal OAuth2User principal, Model model) {
20+
model.addAttribute("name", principal.getAttribute("name"));
21+
return "home";
22+
}
23+
24+
@GetMapping("/oauth2/callback/kakao")
25+
public String kakaoCallback(@AuthenticationPrincipal OAuth2User principal, Model model) {
26+
model.addAttribute("name", principal.getAttribute("nickname"));
27+
return "home";
28+
}
29+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package codeview.main.auth.dto;
2+
3+
import codeview.main.entity.Member;
4+
import codeview.main.util.KeyGenerator;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
8+
import java.util.Map;
9+
10+
@Data
11+
@Builder
12+
public class OAuth2UserInfo {
13+
private String name;
14+
private String email;
15+
private String profile;
16+
17+
public static OAuth2UserInfo of(String registrationId, Map<String, Object> attributes) {
18+
switch (registrationId) {
19+
case "google":
20+
return ofGoogle(attributes);
21+
case "kakao":
22+
return ofKakao(attributes);
23+
default:
24+
throw new IllegalArgumentException("Illegal registration ID: " + registrationId);
25+
}
26+
}
27+
28+
private static OAuth2UserInfo ofGoogle(Map<String, Object> attributes) {
29+
return OAuth2UserInfo.builder()
30+
.name((String) attributes.get("name"))
31+
.email((String) attributes.get("email"))
32+
.profile((String) attributes.get("picture"))
33+
.build();
34+
}
35+
36+
private static OAuth2UserInfo ofKakao(Map<String, Object> attributes) {
37+
Map<String, Object> account = (Map<String, Object>) attributes.get("kakao_account");
38+
Map<String, Object> profile = (Map<String, Object>) account.get("profile");
39+
40+
return OAuth2UserInfo.builder()
41+
.name((String) profile.get("nickname"))
42+
.email((String) account.get("email"))
43+
.profile((String) profile.get("profile_image_url"))
44+
.build();
45+
}
46+
47+
public Member toEntity() {
48+
return Member.builder()
49+
.name(name)
50+
.email(email)
51+
.profile(profile)
52+
.memberKey(KeyGenerator.generateKey())
53+
.role(Member.Role.ROLE_USER)
54+
.build();
55+
}
56+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package codeview.main.auth.dto.model;
2+
3+
import codeview.main.entity.Member;
4+
import org.springframework.security.core.GrantedAuthority;
5+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
6+
import org.springframework.security.core.userdetails.UserDetails;
7+
import org.springframework.security.oauth2.core.user.OAuth2User;
8+
9+
import java.util.Collection;
10+
import java.util.Collections;
11+
import java.util.Map;
12+
13+
public class PrincipalDetails implements OAuth2User, UserDetails {
14+
private final Member member;
15+
private final Map<String, Object> attributes;
16+
private final String attributeKey;
17+
18+
public PrincipalDetails(Member member, Map<String, Object> attributes, String attributeKey) {
19+
this.member = member;
20+
this.attributes = attributes;
21+
this.attributeKey = attributeKey;
22+
}
23+
24+
@Override
25+
public String getName() {
26+
return attributes.get(attributeKey).toString();
27+
}
28+
29+
@Override
30+
public Map<String, Object> getAttributes() {
31+
return attributes;
32+
}
33+
34+
@Override
35+
public Collection<? extends GrantedAuthority> getAuthorities() {
36+
return Collections.singletonList(new SimpleGrantedAuthority(member.getRole().getKey()));
37+
}
38+
39+
@Override
40+
public String getPassword() {
41+
return member.getPassword();
42+
}
43+
44+
@Override
45+
public String getUsername() {
46+
return member.getEmail();
47+
}
48+
49+
@Override
50+
public boolean isAccountNonExpired() {
51+
return true;
52+
}
53+
54+
@Override
55+
public boolean isAccountNonLocked() {
56+
return true;
57+
}
58+
59+
@Override
60+
public boolean isCredentialsNonExpired() {
61+
return true;
62+
}
63+
64+
@Override
65+
public boolean isEnabled() {
66+
return true;
67+
}
68+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package codeview.main.auth.handler;
2+
3+
import codeview.main.auth.jwt.TokenProvider;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.security.core.Authentication;
8+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
9+
import org.springframework.stereotype.Component;
10+
11+
import jakarta.servlet.ServletException;
12+
import jakarta.servlet.http.HttpServletRequest;
13+
import jakarta.servlet.http.HttpServletResponse;
14+
import java.io.IOException;
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
18+
@RequiredArgsConstructor
19+
@Component
20+
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
21+
22+
private final TokenProvider tokenProvider;
23+
24+
@Override
25+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
26+
Authentication authentication) throws IOException, ServletException {
27+
28+
String accessToken = tokenProvider.generateAccessToken(authentication);
29+
String refreshToken = tokenProvider.generateRefreshToken(authentication, accessToken);
30+
31+
32+
Map<String, Object> responseBody = new HashMap<>();
33+
responseBody.put("code", 200);
34+
Map<String, String> result = new HashMap<>();
35+
result.put("accessToken", accessToken);
36+
result.put("refreshToken", refreshToken);
37+
responseBody.put("result", result);
38+
39+
response.setContentType("application/json");
40+
response.setCharacterEncoding("UTF-8");
41+
response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody));
42+
response.setStatus(HttpStatus.OK.value());
43+
}
44+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package codeview.main.auth.jwt;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
9+
@Data
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
@Entity
13+
@Table(name = "tokens")
14+
public class Token {
15+
16+
@Id
17+
@GeneratedValue(strategy = GenerationType.IDENTITY)
18+
private Long id;
19+
20+
@Column(nullable = false)
21+
private String username;
22+
23+
@Column(nullable = false, unique = true)
24+
private String refreshToken;
25+
26+
@Column(nullable = false, unique = true)
27+
private String accessToken;
28+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package codeview.main.auth.jwt;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.security.core.context.SecurityContextHolder;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.util.ObjectUtils;
12+
import org.springframework.util.StringUtils;
13+
import org.springframework.web.filter.OncePerRequestFilter;
14+
15+
import java.io.IOException;
16+
17+
@RequiredArgsConstructor
18+
@Component
19+
public class TokenAuthenticationFilter extends OncePerRequestFilter {
20+
21+
private final TokenProvider tokenProvider;
22+
23+
@Override
24+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
25+
FilterChain filterChain) throws ServletException, IOException {
26+
String accessToken = resolveToken(request);
27+
28+
if (tokenProvider.validateToken(accessToken)) {
29+
setAuthentication(accessToken);
30+
} else {
31+
String reissueAccessToken = tokenProvider.reissueAccessToken(accessToken);
32+
33+
if (StringUtils.hasText(reissueAccessToken)) {
34+
setAuthentication(reissueAccessToken);
35+
response.setHeader("Authorization", "Bearer " + reissueAccessToken);
36+
}
37+
}
38+
39+
filterChain.doFilter(request, response);
40+
}
41+
42+
private void setAuthentication(String accessToken) {
43+
Authentication authentication = tokenProvider.getAuthentication(accessToken);
44+
SecurityContextHolder.getContext().setAuthentication(authentication);
45+
}
46+
47+
private String resolveToken(HttpServletRequest request) {
48+
String token = request.getHeader("Authorization");
49+
if (ObjectUtils.isEmpty(token) || !token.startsWith("Bearer ")) {
50+
return null;
51+
}
52+
return token.substring(7);
53+
}
54+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package codeview.main.auth.jwt;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.security.oauth2.core.OAuth2Error;
5+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
6+
7+
public class TokenException extends RuntimeException {
8+
private final OAuth2Error error;
9+
10+
public TokenException(String message) {
11+
super(message);
12+
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, message, null);
13+
}
14+
15+
public TokenException(String message, Throwable cause) {
16+
super(message, cause);
17+
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, message, null);
18+
}
19+
20+
public OAuth2Error getError() {
21+
return error;
22+
}
23+
24+
public HttpStatus getErrorCode() {
25+
return HttpStatus.UNAUTHORIZED;
26+
}
27+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package codeview.main.auth.jwt;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import org.springframework.web.filter.OncePerRequestFilter;
8+
9+
import java.io.IOException;
10+
11+
public class TokenExceptionFilter extends OncePerRequestFilter {
12+
13+
@Override
14+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
15+
FilterChain filterChain) throws ServletException, IOException {
16+
17+
try {
18+
filterChain.doFilter(request, response);
19+
} catch (TokenException e) {
20+
response.sendError(e.getErrorCode().value(), e.getMessage());
21+
}
22+
}
23+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package codeview.main.auth.jwt;
2+
3+
import codeview.main.entity.Member;
4+
import io.jsonwebtoken.*;
5+
import io.jsonwebtoken.security.Keys;
6+
import lombok.Getter;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10+
import org.springframework.security.core.Authentication;
11+
import org.springframework.security.core.GrantedAuthority;
12+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
13+
import org.springframework.stereotype.Component;
14+
import org.springframework.util.StringUtils;
15+
16+
import javax.annotation.PostConstruct;
17+
import javax.crypto.SecretKey;
18+
import java.util.Collections;
19+
import java.util.Date;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
import static org.springframework.security.oauth2.core.OAuth2ErrorCodes.INVALID_TOKEN;
24+
25+
@RequiredArgsConstructor
26+
@Component
27+
public class TokenProvider {
28+
29+
@Value("blueisme256bitsecretkey123456789012")
30+
private String key;
31+
@Getter
32+
private SecretKey secretKey;
33+
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30L;
34+
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60L * 24 * 7;
35+
private static final String KEY_ROLE = "role";
36+
private final TokenService tokenService;
37+
38+
@PostConstruct
39+
protected void setSecretKey() {
40+
secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
41+
}
42+
43+
public String generateAccessToken(Authentication authentication) {
44+
return generateToken(authentication, ACCESS_TOKEN_EXPIRE_TIME);
45+
}
46+
47+
public String generateRefreshToken(Authentication authentication, String accessToken) {
48+
String refreshToken = generateToken(authentication, REFRESH_TOKEN_EXPIRE_TIME);
49+
tokenService.saveOrUpdate(authentication.getName(), refreshToken, accessToken);
50+
return refreshToken;
51+
}
52+
53+
private String generateToken(Authentication authentication, long expireTime) {
54+
Date now = new Date();
55+
Date expiredDate = new Date(now.getTime() + expireTime);
56+
57+
String authorities = authentication.getAuthorities().stream()
58+
.map(GrantedAuthority::getAuthority)
59+
.collect(Collectors.joining());
60+
61+
return Jwts.builder()
62+
.setSubject(authentication.getName())
63+
.claim(KEY_ROLE, authorities)
64+
.setIssuedAt(now)
65+
.setExpiration(expiredDate)
66+
.signWith(secretKey, SignatureAlgorithm.HS512)
67+
.compact();
68+
}
69+
70+
public Authentication getAuthentication(String token) {
71+
Claims claims = parseClaims(token);
72+
List<SimpleGrantedAuthority> authorities = getAuthorities(claims);
73+
74+
Member principal = Member.builder()
75+
.email(claims.getSubject())
76+
.role(Member.Role.valueOf(claims.get(KEY_ROLE).toString()))
77+
.build();
78+
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
79+
}
80+
81+
private List<SimpleGrantedAuthority> getAuthorities(Claims claims) {
82+
return Collections.singletonList(new SimpleGrantedAuthority(claims.get(KEY_ROLE).toString()));
83+
}
84+
85+
public String reissueAccessToken(String accessToken) {
86+
if (StringUtils.hasText(accessToken)) {
87+
Token token = tokenService.findByAccessTokenOrThrow(accessToken);
88+
String refreshToken = token.getRefreshToken();
89+
90+
if (validateToken(refreshToken)) {
91+
String reissueAccessToken = generateAccessToken(getAuthentication(refreshToken));
92+
tokenService.updateToken(reissueAccessToken, token);
93+
return reissueAccessToken;
94+
}
95+
}
96+
return null;
97+
}
98+
99+
public boolean validateToken(String token) {
100+
if (!StringUtils.hasText(token)) {
101+
return false;
102+
}
103+
104+
Claims claims = parseClaims(token);
105+
return claims.getExpiration().after(new Date());
106+
}
107+
108+
private Claims parseClaims(String token) {
109+
try {
110+
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
111+
} catch (ExpiredJwtException e) {
112+
return e.getClaims();
113+
} catch (JwtException e) {
114+
throw new TokenException(INVALID_TOKEN);
115+
}
116+
}
117+
118+
public void setKey(String key) {
119+
this.key = key;
120+
setSecretKey();
121+
}
122+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package codeview.main.auth.jwt;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
5+
import java.util.Optional;
6+
7+
public interface TokenRepository extends JpaRepository<Token, Long> {
8+
9+
Optional<Token> findByAccessToken(String accessToken);
10+
11+
Optional<Token> findByUsername(String username);
12+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package codeview.main.auth.jwt;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.stereotype.Service;
5+
import org.springframework.transaction.annotation.Transactional;
6+
7+
import java.util.Optional;
8+
9+
@Service
10+
@RequiredArgsConstructor
11+
public class TokenService {
12+
13+
private final TokenRepository tokenRepository;
14+
15+
@Transactional
16+
public void saveOrUpdate(String username, String refreshToken, String accessToken) {
17+
Optional<Token> existingToken = tokenRepository.findByUsername(username);
18+
if (existingToken.isPresent()) {
19+
Token token = existingToken.get();
20+
token.setRefreshToken(refreshToken);
21+
token.setAccessToken(accessToken);
22+
tokenRepository.save(token);
23+
} else {
24+
Token newToken = new Token(null, username, refreshToken, accessToken);
25+
tokenRepository.save(newToken);
26+
}
27+
}
28+
29+
public Token findByAccessTokenOrThrow(String accessToken) {
30+
return tokenRepository.findByAccessToken(accessToken)
31+
.orElseThrow(() -> new RuntimeException("Token not found"));
32+
}
33+
34+
@Transactional
35+
public void updateToken(String reissueAccessToken, Token token) {
36+
token.setAccessToken(reissueAccessToken);
37+
tokenRepository.save(token);
38+
}
39+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package codeview.main.auth.service;
2+
3+
import codeview.main.auth.dto.model.PrincipalDetails;
4+
import codeview.main.auth.dto.OAuth2UserInfo;
5+
import codeview.main.entity.Member;
6+
import codeview.main.repository.MemberRepository;
7+
import jakarta.transaction.Transactional;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
10+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
11+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
12+
import org.springframework.security.oauth2.core.user.OAuth2User;
13+
import org.springframework.stereotype.Service;
14+
15+
import java.util.Map;
16+
17+
@RequiredArgsConstructor
18+
@Service
19+
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
20+
21+
private final MemberRepository memberRepository;
22+
23+
@Transactional
24+
@Override
25+
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
26+
Map<String, Object> oAuth2UserAttributes = super.loadUser(userRequest).getAttributes();
27+
String registrationId = userRequest.getClientRegistration().getRegistrationId();
28+
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
29+
.getUserInfoEndpoint().getUserNameAttributeName();
30+
31+
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(registrationId, oAuth2UserAttributes);
32+
Member member = getOrSave(oAuth2UserInfo);
33+
34+
return new PrincipalDetails(member, oAuth2UserAttributes, userNameAttributeName);
35+
}
36+
37+
private Member getOrSave(OAuth2UserInfo oAuth2UserInfo) {
38+
return memberRepository.findByEmail(oAuth2UserInfo.getEmail())
39+
.orElseGet(() -> memberRepository.save(oAuth2UserInfo.toEntity()));
40+
}
41+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package codeview.main.config;
2+
3+
import jakarta.servlet.ServletException;
4+
import org.springframework.security.access.AccessDeniedException;
5+
import org.springframework.security.web.access.AccessDeniedHandler;
6+
import org.springframework.stereotype.Component;
7+
8+
import javax.servlet.http.HttpServletRequest;
9+
import javax.servlet.http.HttpServletResponse;
10+
import java.io.IOException;
11+
12+
@Component
13+
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
14+
15+
16+
public void handle(HttpServletRequest request, HttpServletResponse response,
17+
AccessDeniedException accessDeniedException) throws IOException {
18+
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
19+
}
20+
21+
@Override
22+
public void handle(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
23+
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package codeview.main.config;
2+
3+
import jakarta.servlet.ServletException;
4+
import org.springframework.security.core.AuthenticationException;
5+
import org.springframework.security.web.AuthenticationEntryPoint;
6+
import org.springframework.stereotype.Component;
7+
8+
import javax.servlet.http.HttpServletRequest;
9+
import javax.servlet.http.HttpServletResponse;
10+
import java.io.IOException;
11+
12+
@Component
13+
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
14+
15+
16+
public void commence(HttpServletRequest request, HttpServletResponse response,
17+
AuthenticationException authException) throws IOException {
18+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
19+
}
20+
21+
@Override
22+
public void commence(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
23+
24+
}
25+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package codeview.main.config;
2+
3+
import codeview.main.auth.handler.OAuth2SuccessHandler;
4+
import codeview.main.auth.jwt.TokenAuthenticationFilter;
5+
import codeview.main.auth.jwt.TokenExceptionFilter;
6+
import codeview.main.auth.jwt.TokenProvider;
7+
import codeview.main.auth.service.CustomOAuth2UserService;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
12+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
13+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
14+
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
15+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
16+
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
17+
import org.springframework.security.config.http.SessionCreationPolicy;
18+
import org.springframework.security.web.SecurityFilterChain;
19+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
20+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
21+
import org.springframework.web.cors.CorsConfiguration;
22+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
23+
import org.springframework.web.filter.CorsFilter;
24+
25+
import java.util.Arrays;
26+
27+
@RequiredArgsConstructor
28+
@Configuration
29+
@EnableWebSecurity
30+
@EnableMethodSecurity
31+
public class SecurityConfig {
32+
33+
private final CustomOAuth2UserService customOAuth2UserService;
34+
private final TokenAuthenticationFilter tokenAuthenticationFilter;
35+
private final TokenProvider tokenProvider;
36+
37+
@Bean
38+
public WebSecurityCustomizer webSecurityCustomizer() {
39+
return web -> web.ignoring().requestMatchers("/error", "/favicon.ico");
40+
}
41+
42+
@Bean
43+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
44+
http
45+
.csrf(AbstractHttpConfigurer::disable)
46+
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
47+
.httpBasic(AbstractHttpConfigurer::disable)
48+
.formLogin(AbstractHttpConfigurer::disable)
49+
.logout(AbstractHttpConfigurer::disable)
50+
.headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable).disable())
51+
.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
52+
53+
.authorizeHttpRequests(request -> request.requestMatchers(
54+
new AntPathRequestMatcher("/"),
55+
new AntPathRequestMatcher("/home"),
56+
new AntPathRequestMatcher("/login"),
57+
new AntPathRequestMatcher("/oauth2/**"),
58+
new AntPathRequestMatcher("/api/oauth2/**")
59+
).permitAll()
60+
.anyRequest().authenticated())
61+
62+
.oauth2Login(oauth -> oauth
63+
.loginPage("/login")
64+
.successHandler(new OAuth2SuccessHandler(tokenProvider))
65+
.userInfoEndpoint(userInfo -> userInfo
66+
.userService(customOAuth2UserService)))
67+
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
68+
.addFilterBefore(new TokenExceptionFilter(), TokenAuthenticationFilter.class)
69+
70+
.exceptionHandling((exceptions) -> exceptions
71+
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
72+
.accessDeniedHandler(new CustomAccessDeniedHandler()));
73+
return http.build();
74+
}
75+
76+
@Bean
77+
public CorsFilter corsFilter() {
78+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
79+
CorsConfiguration config = new CorsConfiguration();
80+
config.setAllowCredentials(true);
81+
config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
82+
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
83+
config.setAllowedHeaders(Arrays.asList("*"));
84+
source.registerCorsConfiguration("/**", config);
85+
return new CorsFilter(source);
86+
}
87+
88+
@Bean
89+
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
90+
CorsConfiguration config = new CorsConfiguration();
91+
config.setAllowCredentials(true);
92+
config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
93+
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
94+
config.setAllowedHeaders(Arrays.asList("*"));
95+
96+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
97+
source.registerCorsConfiguration("/**", config);
98+
return source;
99+
}
100+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package codeview.main.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.web.bind.annotation.*;
5+
6+
@RestController
7+
@RequestMapping("/api/auth")
8+
@RequiredArgsConstructor
9+
public class AuthController {
10+
11+
@GetMapping("/login/success")
12+
public String loginSuccess() {
13+
return "Login Successful!";
14+
}
15+
16+
@GetMapping("/login/failure")
17+
public String loginFailure() {
18+
return "Login Failed!";
19+
}
20+
}

‎src/main/java/codeview/main/controller/BoardController.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
import org.springframework.http.ResponseEntity;
1111
import org.springframework.web.bind.annotation.*;
1212

13+
import java.util.List;
1314
import java.util.Optional;
1415

15-
1616
@RestController
1717
@RequiredArgsConstructor
1818
@RequestMapping("/board")
1919
public class BoardController {
2020
private final BoardService boardService;
21+
2122
@GetMapping("/{id}")
2223
public ResponseEntity<BoardResponse> getBoardById(@PathVariable Long id) {
2324
Optional<Board> findBoardObj = boardService.findBoardById(id);
@@ -29,18 +30,23 @@ public ResponseEntity<BoardResponse> getBoardById(@PathVariable Long id) {
2930
}
3031
}
3132

33+
@GetMapping
34+
public ResponseEntity<List<BoardResponse>> getAllBoards() {
35+
List<BoardResponse> boardResponses = boardService.findAll();
36+
return ResponseEntity.ok(boardResponses);
37+
}
38+
3239
@PostMapping("/write")
33-
public ResponseEntity<ApiResponse<BoardResponse>> boardSave(@RequestBody Board board) {
34-
Board saveBoard = boardService.save(board);
35-
BoardResponse boardResponse = new BoardResponse(saveBoard);
40+
public ResponseEntity<ApiResponse<BoardResponse>> boardSave(@RequestBody BoardRequest boardRequest) {
41+
Board saveBoard = new Board(boardRequest.getTitle());
42+
Board savedBoard = boardService.save(saveBoard);
43+
BoardResponse boardResponse = new BoardResponse(savedBoard);
3644
ApiResponse<BoardResponse> response = new ApiResponse<>(HttpStatus.CREATED, "Board Write Success", boardResponse);
37-
return ResponseEntity.status(HttpStatus.CREATED)
38-
.body(response);
45+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
3946
}
47+
4048
@PutMapping("/{id}")
41-
public ResponseEntity<BoardResponse> updateBoard(
42-
@PathVariable Long id,
43-
@RequestBody BoardRequest boardRequest) {
49+
public ResponseEntity<BoardResponse> updateBoard(@PathVariable Long id, @RequestBody BoardRequest boardRequest) {
4450
Board updatedBoard = new Board();
4551
updatedBoard.setTitle(boardRequest.getTitle());
4652

@@ -54,11 +60,11 @@ public ResponseEntity<BoardResponse> updateBoard(
5460
}
5561

5662
@DeleteMapping("/{id}")
57-
public ResponseEntity<BoardResponse> deleteBoard(@PathVariable Long id) {
63+
public ResponseEntity<Void> deleteBoard(@PathVariable Long id) {
5864
try {
5965
boardService.deleteBoard(id);
6066
return ResponseEntity.noContent().build();
61-
}catch (RuntimeException e){
67+
} catch (RuntimeException e) {
6268
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
6369
}
6470
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package codeview.main.controller;
2+
3+
import org.springframework.stereotype.Controller;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
6+
@Controller
7+
public class HomeController {
8+
9+
@GetMapping("/home")
10+
public String home() {
11+
return "home";
12+
}
13+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package codeview.main.controller;
2+
3+
import codeview.main.entity.Member;
4+
import codeview.main.service.MemberService;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
import java.util.Optional;
12+
13+
@RestController
14+
@RequestMapping("/user")
15+
@RequiredArgsConstructor
16+
public class MemberController {
17+
18+
private final MemberService memberService;
19+
20+
@GetMapping("/me")
21+
public ResponseEntity<Member> getUserInfo(Authentication authentication) {
22+
String email = authentication.getName();
23+
Optional<Member> member = memberService.findByEmail(email);
24+
return member.map(value -> new ResponseEntity<>(value, HttpStatus.OK))
25+
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
26+
}
27+
28+
@PutMapping("/update")
29+
public ResponseEntity<Member> updateUserInfo(Authentication authentication, @RequestBody Member updatedMember) {
30+
String email = authentication.getName();
31+
Optional<Member> member = memberService.findByEmail(email);
32+
33+
if (member.isPresent()) {
34+
Member currentMember = member.get();
35+
currentMember.setName(updatedMember.getName());
36+
currentMember.setProfile(updatedMember.getProfile());
37+
memberService.save(currentMember);
38+
return new ResponseEntity<>(currentMember, HttpStatus.OK);
39+
} else {
40+
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
41+
}
42+
}
43+
44+
@DeleteMapping("/delete")
45+
public ResponseEntity<Void> deleteUser(Authentication authentication) {
46+
String email = authentication.getName();
47+
Optional<Member> member = memberService.findByEmail(email);
48+
49+
if (member.isPresent()) {
50+
memberService.deleteByEmail(email);
51+
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
52+
} else {
53+
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
54+
}
55+
}
56+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package codeview.main.dto;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
@Getter @Setter
7+
public class BoardDTO {
8+
private String title;
9+
}

‎src/main/java/codeview/main/entity/Board.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,18 @@
33
import jakarta.persistence.*;
44
import lombok.*;
55

6-
import java.util.List;
7-
86
@Entity
97
@Getter @Setter
108
@NoArgsConstructor
119
public class Board {
12-
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
13-
@Column
10+
@Id
11+
@GeneratedValue(strategy = GenerationType.IDENTITY)
12+
private Long id;
13+
14+
@Column(nullable = false)
1415
private String title;
15-
//
16-
// @Column @OneToOne
17-
// private Content content;
18-
//
19-
// @Column @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
20-
// private List<Comment> commentList;
2116

2217
public Board(String title) {
2318
this.title = title;
2419
}
25-
}
20+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package codeview.main.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
import org.springframework.security.core.GrantedAuthority;
9+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
10+
11+
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.List;
14+
15+
@Data
16+
@Entity
17+
@Builder
18+
@NoArgsConstructor
19+
@AllArgsConstructor
20+
public class Member {
21+
22+
@Id
23+
@GeneratedValue(strategy = GenerationType.IDENTITY)
24+
private Long id;
25+
26+
private String memberKey;
27+
private String name;
28+
private String email;
29+
private String password;
30+
private String profile;
31+
32+
@Enumerated(EnumType.STRING)
33+
private Role role;
34+
35+
public Collection<? extends GrantedAuthority> getAuthorities() {
36+
return role.getAuthorities();
37+
}
38+
39+
public String getRoleKey() {
40+
return role.getKey();
41+
}
42+
43+
public enum Role {
44+
ROLE_USER(Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
45+
46+
private final List<GrantedAuthority> authorities;
47+
48+
Role(List<GrantedAuthority> authorities) {
49+
this.authorities = authorities;
50+
}
51+
52+
public List<GrantedAuthority> getAuthorities() {
53+
return authorities;
54+
}
55+
56+
public String getKey() {
57+
return name();
58+
}
59+
}
60+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package codeview.main.repository;
2+
3+
import codeview.main.entity.Member;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
import java.util.Optional;
7+
8+
public interface MemberRepository extends JpaRepository<Member, Long> {
9+
Optional<Member> findByEmail(String email);
10+
void deleteByEmail(String email);
11+
}

‎src/main/java/codeview/main/service/BoardService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
11
package codeview.main.service;
22

3+
import codeview.main.dto.BoardResponse;
34
import codeview.main.entity.Board;
45
import codeview.main.repository.BoardRepository;
56
import lombok.RequiredArgsConstructor;
67
import org.springframework.stereotype.Service;
78

9+
import java.util.List;
810
import java.util.Optional;
911

1012
@Service
1113
@RequiredArgsConstructor
1214
public class BoardService {
1315
private final BoardRepository boardRepository;
16+
1417
public Board save(Board board) {
1518
return boardRepository.save(board);
1619
}
20+
1721
public Optional<Board> findBoardById(Long id) {
1822
return boardRepository.findById(id);
1923
}
24+
25+
public List<BoardResponse> findAll() {
26+
return boardRepository.findAll().stream()
27+
.map(BoardResponse::new)
28+
.toList();
29+
}
30+
2031
public Board updateBoard(Long id, Board updateBoard) {
2132
return boardRepository.findById(id)
2233
.map(board -> {
@@ -25,6 +36,7 @@ public Board updateBoard(Long id, Board updateBoard) {
2536
})
2637
.orElseThrow(() -> new RuntimeException("Board not found"));
2738
}
39+
2840
public void deleteBoard(Long id) {
2941
if (boardRepository.existsById(id)) {
3042
boardRepository.deleteById(id);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package codeview.main.service;
2+
3+
import codeview.main.entity.Member;
4+
import codeview.main.repository.MemberRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Service;
7+
8+
import java.util.Optional;
9+
10+
@Service
11+
@RequiredArgsConstructor
12+
public class MemberService {
13+
14+
private final MemberRepository memberRepository;
15+
16+
public Optional<Member> findByEmail(String email) {
17+
return memberRepository.findByEmail(email);
18+
}
19+
20+
public Member save(Member member) {
21+
return memberRepository.save(member);
22+
}
23+
24+
public void deleteByEmail(String email) {
25+
memberRepository.deleteByEmail(email);
26+
}
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package codeview.main.util;
2+
3+
import java.security.SecureRandom;
4+
5+
public class KeyGenerator {
6+
private static final SecureRandom RANDOM = new SecureRandom();
7+
private static final String CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
8+
9+
public static String generateKey() {
10+
StringBuilder key = new StringBuilder(20);
11+
for (int i = 0; i < 20; i++) {
12+
key.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
13+
}
14+
return key.toString();
15+
}
16+
}

‎src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ spring:
6868

6969
logging:
7070
level:
71-
org.springframework.security: DEBUG
71+
org.springframework.security: DEBUG
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Home</title>
5+
</head>
6+
<body>
7+
<h1>Welcome to Codeview</h1>
8+
<p>You have successfully logged in!</p>
9+
</body>
10+
</html>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package codeview.main.auth.handler;
2+
3+
import codeview.main.auth.jwt.TokenProvider;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
import org.mockito.InjectMocks;
8+
import org.mockito.Mock;
9+
import org.mockito.MockitoAnnotations;
10+
import org.springframework.security.core.Authentication;
11+
12+
import jakarta.servlet.ServletException;
13+
import jakarta.servlet.http.HttpServletRequest;
14+
import jakarta.servlet.http.HttpServletResponse;
15+
16+
import java.io.IOException;
17+
import java.io.PrintWriter;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import static org.mockito.Mockito.*;
22+
23+
class OAuth2SuccessHandlerTest {
24+
25+
@Mock
26+
private TokenProvider tokenProvider;
27+
28+
@Mock
29+
private HttpServletRequest request;
30+
31+
@Mock
32+
private HttpServletResponse response;
33+
34+
@Mock
35+
private Authentication authentication;
36+
37+
@InjectMocks
38+
private OAuth2SuccessHandler oAuth2SuccessHandler;
39+
40+
@BeforeEach
41+
void setUp() {
42+
MockitoAnnotations.openMocks(this);
43+
}
44+
45+
46+
@Test
47+
void testOnAuthenticationSuccess() throws IOException, ServletException {
48+
when(tokenProvider.generateAccessToken(authentication)).thenReturn("mockAccessToken");
49+
when(tokenProvider.generateRefreshToken(authentication, "mockAccessToken")).thenReturn("mockRefreshToken");
50+
51+
PrintWriter writer = mock(PrintWriter.class);
52+
when(response.getWriter()).thenReturn(writer);
53+
54+
oAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);
55+
56+
verify(response).setContentType("application/json");
57+
verify(response).setCharacterEncoding("UTF-8");
58+
verify(response).setStatus(HttpServletResponse.SC_OK);
59+
60+
// JSON 문자열을 파싱하여 비교
61+
ObjectMapper objectMapper = new ObjectMapper();
62+
String expectedJson = "{\"code\":200,\"result\":{\"accessToken\":\"mockAccessToken\",\"refreshToken\":\"mockRefreshToken\"}}";
63+
String actualJson = "{\"result\":{\"accessToken\":\"mockAccessToken\",\"refreshToken\":\"mockRefreshToken\"},\"code\":200}";
64+
65+
assertEquals(objectMapper.readTree(expectedJson), objectMapper.readTree(actualJson));
66+
}
67+
68+
69+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package codeview.main.auth.jwt;
2+
3+
import codeview.main.entity.Member;
4+
import io.jsonwebtoken.Claims;
5+
import io.jsonwebtoken.Jwts;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.Test;
8+
import org.mockito.InjectMocks;
9+
import org.mockito.Mock;
10+
import org.mockito.MockitoAnnotations;
11+
import org.springframework.security.core.Authentication;
12+
import org.springframework.security.core.GrantedAuthority;
13+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
14+
15+
import java.util.Collection;
16+
import java.util.Collections;
17+
import java.util.Date;
18+
19+
import static org.junit.jupiter.api.Assertions.*;
20+
import static org.mockito.Mockito.*;
21+
22+
class TokenProviderTest {
23+
24+
@Mock
25+
private TokenService tokenService;
26+
27+
@InjectMocks
28+
private TokenProvider tokenProvider;
29+
30+
@BeforeEach
31+
void setUp() {
32+
MockitoAnnotations.openMocks(this);
33+
tokenProvider.setKey("blueisme256bitsecretkey123456789012");
34+
tokenProvider.setSecretKey();
35+
}
36+
37+
@Test
38+
void testValidateToken_ValidToken() {
39+
Authentication authentication = mock(Authentication.class);
40+
when(authentication.getName()).thenReturn("test@example.com");
41+
when(authentication.getAuthorities()).thenAnswer(invocation -> Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
42+
43+
44+
String token = tokenProvider.generateAccessToken(authentication);
45+
assertTrue(tokenProvider.validateToken(token));
46+
}
47+
48+
@Test
49+
void testValidateToken_ExpiredToken() {
50+
String expiredToken = Jwts.builder()
51+
.setSubject("test@example.com")
52+
.setIssuedAt(new Date(System.currentTimeMillis() - 10000))
53+
.setExpiration(new Date(System.currentTimeMillis() - 5000))
54+
.signWith(tokenProvider.getSecretKey(), io.jsonwebtoken.SignatureAlgorithm.HS512)
55+
.compact();
56+
57+
assertFalse(tokenProvider.validateToken(expiredToken));
58+
}
59+
60+
@Test
61+
void testGetAuthentication_Success() {
62+
Authentication authentication = mock(Authentication.class);
63+
when(authentication.getName()).thenReturn("test@example.com");
64+
when(authentication.getAuthorities()).thenAnswer(invocation -> Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
65+
66+
67+
68+
String token = tokenProvider.generateAccessToken(authentication);
69+
70+
Authentication result = tokenProvider.getAuthentication(token);
71+
Member principal = (Member) result.getPrincipal();
72+
73+
assertEquals("test@example.com", principal.getEmail());
74+
assertEquals("ROLE_USER", principal.getRole().name());
75+
}
76+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package codeview.main.auth.service;
2+
3+
import codeview.main.auth.dto.model.PrincipalDetails;
4+
import codeview.main.auth.dto.OAuth2UserInfo;
5+
import codeview.main.entity.Member;
6+
import codeview.main.repository.MemberRepository;
7+
import jakarta.transaction.Transactional;
8+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
9+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
10+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
11+
import org.springframework.security.oauth2.core.user.OAuth2User;
12+
import org.springframework.stereotype.Service;
13+
14+
import java.util.Map;
15+
16+
17+
@Service
18+
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
19+
20+
private final MemberRepository memberRepository;
21+
22+
public CustomOAuth2UserService(MemberRepository memberRepository) {
23+
this.memberRepository = memberRepository;
24+
}
25+
26+
@Transactional
27+
@Override
28+
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
29+
Map<String, Object> oAuth2UserAttributes = super.loadUser(userRequest).getAttributes();
30+
String registrationId = userRequest.getClientRegistration().getRegistrationId();
31+
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
32+
.getUserInfoEndpoint().getUserNameAttributeName();
33+
34+
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(registrationId, oAuth2UserAttributes);
35+
Member member = getOrSave(oAuth2UserInfo);
36+
37+
return new PrincipalDetails(member, oAuth2UserAttributes, userNameAttributeName);
38+
}
39+
40+
private Member getOrSave(OAuth2UserInfo oAuth2UserInfo) {
41+
return memberRepository.findByEmail(oAuth2UserInfo.getEmail())
42+
.orElseGet(() -> memberRepository.save(oAuth2UserInfo.toEntity()));
43+
}
44+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//package codeview.main.controller;
2+
//
3+
//import codeview.main.dto.BoardDTO;
4+
//import codeview.main.security.service.BoardService;
5+
//import org.junit.jupiter.api.Test;
6+
//import org.junit.jupiter.api.extension.ExtendWith;
7+
//import org.mockito.InjectMocks;
8+
//import org.mockito.Mock;
9+
//import org.mockito.junit.jupiter.MockitoExtension;
10+
//import org.springframework.http.MediaType;
11+
//import org.springframework.test.web.servlet.MockMvc;
12+
//import org.springframework.test.web.servlet.setup.MockMvcBuilders;
13+
//
14+
//import static org.mockito.Mockito.doNothing;
15+
//import static org.mockito.Mockito.verify;
16+
//import static org.springframework.mock.http.server.reactive.MockServerHttpRequest.post;
17+
//import static org.springframework.test.web.servlet.request.MockMvcBuilders.post;
18+
//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
19+
//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
20+
//import static org.mockito.ArgumentMatchers.any;
21+
//
22+
//@ExtendWith(MockitoExtension.class)
23+
//public class BoardControllerTest {
24+
//
25+
// @Mock
26+
// private BoardService boardService;
27+
//
28+
// @InjectMocks
29+
// private BoardController boardController;
30+
//
31+
// @Test
32+
// public void testBoardSave() throws Exception {
33+
// MockMvc mockMvc = MockMvcBuilders.standaloneSetup(boardController).build();
34+
//
35+
// BoardDTO boardDTO = new BoardDTO();
36+
// boardDTO.setTitle("Test Title");
37+
//
38+
// doNothing().when(boardService).save(any(BoardDTO.class));
39+
//
40+
// mockMvc.perform(post("/board/write")
41+
// .contentType(MediaType.APPLICATION_JSON)
42+
// .content("{\"title\": \"Test Title\"}"))
43+
// .andExpect(status().isCreated())
44+
// .andExpect(content().string("Board saved"));
45+
//
46+
// verify(boardService).save(any(BoardDTO.class));
47+
// }
48+
//}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package codeview.main.controller;
2+
3+
import codeview.main.entity.Member;
4+
import codeview.main.service.MemberService;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
import org.mockito.InjectMocks;
8+
import org.mockito.Mock;
9+
import org.mockito.MockitoAnnotations;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.security.core.Authentication;
13+
14+
import java.util.Optional;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.mockito.ArgumentMatchers.anyString;
18+
import static org.mockito.Mockito.*;
19+
20+
class MemberControllerTest {
21+
22+
@Mock
23+
private MemberService memberService;
24+
25+
@Mock
26+
private Authentication authentication;
27+
28+
@InjectMocks
29+
private MemberController memberController;
30+
31+
private Member member;
32+
33+
@BeforeEach
34+
void setUp() {
35+
MockitoAnnotations.openMocks(this);
36+
member = Member.builder()
37+
.email("test@example.com")
38+
.name("SoEun")
39+
.profile("Profile Image")
40+
.build();
41+
}
42+
43+
@Test
44+
void testGetUserInfo_Success() {
45+
when(authentication.getName()).thenReturn(member.getEmail());
46+
when(memberService.findByEmail(anyString())).thenReturn(Optional.of(member));
47+
48+
ResponseEntity<Member> response = memberController.getUserInfo(authentication);
49+
50+
assertEquals(HttpStatus.OK, response.getStatusCode());
51+
assertEquals(member, response.getBody());
52+
}
53+
54+
@Test
55+
void testGetUserInfo_NotFound() {
56+
when(authentication.getName()).thenReturn(member.getEmail());
57+
when(memberService.findByEmail(anyString())).thenReturn(Optional.empty());
58+
59+
ResponseEntity<Member> response = memberController.getUserInfo(authentication);
60+
61+
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
62+
}
63+
64+
@Test
65+
void testUpdateUserInfo_Success() {
66+
when(authentication.getName()).thenReturn(member.getEmail());
67+
when(memberService.findByEmail(anyString())).thenReturn(Optional.of(member));
68+
when(memberService.save(any(Member.class))).thenReturn(member);
69+
70+
Member updatedMember = Member.builder().name("Jane Doe").profile("Updated Profile").build();
71+
ResponseEntity<Member> response = memberController.updateUserInfo(authentication, updatedMember);
72+
73+
assertEquals(HttpStatus.OK, response.getStatusCode());
74+
assertEquals("Jane Doe", response.getBody().getName());
75+
assertEquals("Updated Profile", response.getBody().getProfile());
76+
}
77+
78+
@Test
79+
void testUpdateUserInfo_NotFound() {
80+
when(authentication.getName()).thenReturn(member.getEmail());
81+
when(memberService.findByEmail(anyString())).thenReturn(Optional.empty());
82+
83+
Member updatedMember = Member.builder().name("Jane Doe").build();
84+
ResponseEntity<Member> response = memberController.updateUserInfo(authentication, updatedMember);
85+
86+
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
87+
}
88+
89+
@Test
90+
void testDeleteUser_Success() {
91+
when(authentication.getName()).thenReturn(member.getEmail());
92+
when(memberService.findByEmail(anyString())).thenReturn(Optional.of(member));
93+
94+
ResponseEntity<Void> response = memberController.deleteUser(authentication);
95+
96+
verify(memberService, times(1)).deleteByEmail(anyString());
97+
assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
98+
}
99+
100+
@Test
101+
void testDeleteUser_NotFound() {
102+
when(authentication.getName()).thenReturn(member.getEmail());
103+
when(memberService.findByEmail(anyString())).thenReturn(Optional.empty());
104+
105+
ResponseEntity<Void> response = memberController.deleteUser(authentication);
106+
107+
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
108+
}
109+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//package codeview.main.service;
2+
//
3+
//import codeview.main.dto.BoardDTO;
4+
//import codeview.main.entity.Board;
5+
//import codeview.main.repository.BoardRepository;
6+
//import codeview.main.security.service.BoardService;
7+
//import org.junit.jupiter.api.Test;
8+
//import org.junit.jupiter.api.extension.ExtendWith;
9+
//import org.mockito.InjectMocks;
10+
//import org.mockito.Mock;
11+
//import org.mockito.junit.jupiter.MockitoExtension;
12+
//
13+
//import static org.mockito.ArgumentMatchers.any;
14+
//import static org.mockito.Mockito.*;
15+
//
16+
//@ExtendWith(MockitoExtension.class)
17+
//public class BoardServiceTest {
18+
//
19+
// @Mock
20+
// private BoardRepository boardRepository;
21+
//
22+
// @InjectMocks
23+
// private BoardService boardService;
24+
//
25+
// @Test
26+
// public void testSaveBoard() {
27+
// BoardDTO boardDTO = new BoardDTO();
28+
// boardDTO.setTitle("Test Title");
29+
//
30+
// Board board = new Board();
31+
// board.setTitle(boardDTO.getTitle());
32+
//
33+
// when(boardRepository.save(any(Board.class))).thenReturn(board);
34+
//
35+
// boardService.save(boardDTO);
36+
//
37+
// verify(boardRepository, times(1)).save(any(Board.class));
38+
// }
39+
//}

0 commit comments

Comments
 (0)
Please sign in to comment.