Skip to content

Commit 0374595

Browse files
authored
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+
}

0 commit comments

Comments
 (0)