Skip to content

Develop #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,25 @@ repositories {
}

dependencies {
implementation 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'javax.servlet:javax.servlet-api:4.0.1'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.security:spring-security-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Expand Down
29 changes: 29 additions & 0 deletions src/main/java/codeview/main/auth/controller/OAuth2Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package codeview.main.auth.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class OAuth2Controller {

@GetMapping("/oauth2/callback/google")
public String googleCallback(@AuthenticationPrincipal OAuth2User principal, Model model) {
model.addAttribute("name", principal.getAttribute("name"));
return "home";
}

@GetMapping("/oauth2/callback/github")
public String githubCallback(@AuthenticationPrincipal OAuth2User principal, Model model) {
model.addAttribute("name", principal.getAttribute("name"));
return "home";
}

@GetMapping("/oauth2/callback/kakao")
public String kakaoCallback(@AuthenticationPrincipal OAuth2User principal, Model model) {
model.addAttribute("name", principal.getAttribute("nickname"));
return "home";
}
}
56 changes: 56 additions & 0 deletions src/main/java/codeview/main/auth/dto/OAuth2UserInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package codeview.main.auth.dto;

import codeview.main.entity.Member;
import codeview.main.util.KeyGenerator;
import lombok.Builder;
import lombok.Data;

import java.util.Map;

@Data
@Builder
public class OAuth2UserInfo {
private String name;
private String email;
private String profile;

public static OAuth2UserInfo of(String registrationId, Map<String, Object> attributes) {
switch (registrationId) {
case "google":
return ofGoogle(attributes);
case "kakao":
return ofKakao(attributes);
default:
throw new IllegalArgumentException("Illegal registration ID: " + registrationId);
}
}

private static OAuth2UserInfo ofGoogle(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.profile((String) attributes.get("picture"))
.build();
}

private static OAuth2UserInfo ofKakao(Map<String, Object> attributes) {
Map<String, Object> account = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) account.get("profile");

return OAuth2UserInfo.builder()
.name((String) profile.get("nickname"))
.email((String) account.get("email"))
.profile((String) profile.get("profile_image_url"))
.build();
}

public Member toEntity() {
return Member.builder()
.name(name)
.email(email)
.profile(profile)
.memberKey(KeyGenerator.generateKey())
.role(Member.Role.ROLE_USER)
.build();
}
}
68 changes: 68 additions & 0 deletions src/main/java/codeview/main/auth/dto/model/PrincipalDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package codeview.main.auth.dto.model;

import codeview.main.entity.Member;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;

public class PrincipalDetails implements OAuth2User, UserDetails {
private final Member member;
private final Map<String, Object> attributes;
private final String attributeKey;

public PrincipalDetails(Member member, Map<String, Object> attributes, String attributeKey) {
this.member = member;
this.attributes = attributes;
this.attributeKey = attributeKey;
}

@Override
public String getName() {
return attributes.get(attributeKey).toString();
}

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority(member.getRole().getKey()));
}

@Override
public String getPassword() {
return member.getPassword();
}

@Override
public String getUsername() {
return member.getEmail();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
44 changes: 44 additions & 0 deletions src/main/java/codeview/main/auth/handler/OAuth2SuccessHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package codeview.main.auth.handler;

import codeview.main.auth.jwt.TokenProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {

private final TokenProvider tokenProvider;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {

String accessToken = tokenProvider.generateAccessToken(authentication);
String refreshToken = tokenProvider.generateRefreshToken(authentication, accessToken);


Map<String, Object> responseBody = new HashMap<>();
responseBody.put("code", 200);
Map<String, String> result = new HashMap<>();
result.put("accessToken", accessToken);
result.put("refreshToken", refreshToken);
responseBody.put("result", result);

response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody));
response.setStatus(HttpStatus.OK.value());
}
}
28 changes: 28 additions & 0 deletions src/main/java/codeview/main/auth/jwt/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package codeview.main.auth.jwt;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "tokens")
public class Token {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String username;

@Column(nullable = false, unique = true)
private String refreshToken;

@Column(nullable = false, unique = true)
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package codeview.main.auth.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.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {

private final TokenProvider tokenProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String accessToken = resolveToken(request);

if (tokenProvider.validateToken(accessToken)) {
setAuthentication(accessToken);
} else {
String reissueAccessToken = tokenProvider.reissueAccessToken(accessToken);

if (StringUtils.hasText(reissueAccessToken)) {
setAuthentication(reissueAccessToken);
response.setHeader("Authorization", "Bearer " + reissueAccessToken);
}
}

filterChain.doFilter(request, response);
}

private void setAuthentication(String accessToken) {
Authentication authentication = tokenProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

private String resolveToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (ObjectUtils.isEmpty(token) || !token.startsWith("Bearer ")) {
return null;
}
return token.substring(7);
}
}
27 changes: 27 additions & 0 deletions src/main/java/codeview/main/auth/jwt/TokenException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package codeview.main.auth.jwt;

import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;

public class TokenException extends RuntimeException {
private final OAuth2Error error;

public TokenException(String message) {
super(message);
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, message, null);
}

public TokenException(String message, Throwable cause) {
super(message, cause);
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, message, null);
}

public OAuth2Error getError() {
return error;
}

public HttpStatus getErrorCode() {
return HttpStatus.UNAUTHORIZED;
}
}
23 changes: 23 additions & 0 deletions src/main/java/codeview/main/auth/jwt/TokenExceptionFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package codeview.main.auth.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class TokenExceptionFilter extends OncePerRequestFilter {

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

try {
filterChain.doFilter(request, response);
} catch (TokenException e) {
response.sendError(e.getErrorCode().value(), e.getMessage());
}
}
}
Loading
Loading