Skip to content
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

Feat(#1): social login 구현하기 #2

Merged
merged 28 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3e42330
Initial commit
tmddus2 Jul 8, 2024
9a960d4
🎉 feat: init project
tmddus2 Jul 8, 2024
e424a9b
🚀 feat: GitHub Actions CI/CD 추가
tmddus2 Jul 9, 2024
109d15c
🚀 fix: CI/CD working-directory 수정
tmddus2 Jul 9, 2024
3a0d1ab
🚀 fix: CI 에러 수정
tmddus2 Jul 9, 2024
d060778
🚀 fix: CD 에러 수정
tmddus2 Jul 9, 2024
c31fa2a
✨ feat: Spring Security config 추가
tmddus2 Jul 14, 2024
592a095
✨ feat: CustomOAuth2UserService에 successHandler 추가
tmddus2 Jul 14, 2024
4a68615
✨ feat: UserRepository 추가
tmddus2 Jul 14, 2024
5e3b223
✨ feat: JWTUtil 추가
tmddus2 Jul 14, 2024
6182140
✨ feat: username 값 담은 JWT 토큰 반환
tmddus2 Jul 14, 2024
a8881c0
✨ feat: jwt filter 추가
tmddus2 Jul 14, 2024
f558851
✨ feat: OAuth failureHandler 추가
tmddus2 Jul 14, 2024
30535ef
✨ feat: exceptionHandling 추가
tmddus2 Jul 14, 2024
1db961b
♻️refactor: 로그인 성공 시 token response body로 전달
tmddus2 Jul 14, 2024
81721b0
🚚rename: 인증 관련 내용 auth로 분리
tmddus2 Jul 14, 2024
71f5c96
Initial commit
tmddus2 Jul 8, 2024
e2c0932
✨ feat: Spring Security config 추가
tmddus2 Jul 14, 2024
bfaf2f5
✨ feat: CustomOAuth2UserService에 successHandler 추가
tmddus2 Jul 14, 2024
ea8b7ae
✨ feat: UserRepository 추가
tmddus2 Jul 14, 2024
c218606
✨ feat: JWTUtil 추가
tmddus2 Jul 14, 2024
eaf1bf7
✨ feat: username 값 담은 JWT 토큰 반환
tmddus2 Jul 14, 2024
0f57f54
✨ feat: jwt filter 추가
tmddus2 Jul 14, 2024
cafc493
✨ feat: OAuth failureHandler 추가
tmddus2 Jul 14, 2024
26812cd
✨ feat: exceptionHandling 추가
tmddus2 Jul 14, 2024
93dc864
♻️refactor: 로그인 성공 시 token response body로 전달
tmddus2 Jul 14, 2024
c48c781
🚚rename: 인증 관련 내용 auth로 분리
tmddus2 Jul 14, 2024
7e18599
Merge branch '1-social-login-구현하기' of https://github.com/Central-Make…
tmddus2 Jul 14, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Purithm_Server
# PURITHM-Server
CMC 15기 PURITHM 서버 레포지토리입니다.
7 changes: 7 additions & 0 deletions purithm/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
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 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.purithm.auth.config;

import com.example.purithm.auth.exception.JWTAuthenticationEntryPoint;
import com.example.purithm.auth.filter.JWTFilter;
import com.example.purithm.auth.handler.OAuth2AuthenticationFailureHandler;
import com.example.purithm.auth.handler.OAuth2AuthenticationSuccessHandler;
import com.example.purithm.auth.jwt.JWTUtil;
import com.example.purithm.auth.service.OAuth2UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final OAuth2UserService oAuth2UserService;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
private final JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JWTUtil jwtUtil;

public SecurityConfig(
OAuth2UserService oAuth2UserService,
OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler,
OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler,
JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JWTUtil jwtUtil) {
this.oAuth2UserService = oAuth2UserService;
this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler;
this.oAuth2AuthenticationFailureHandler = oAuth2AuthenticationFailureHandler;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtUtil = jwtUtil;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.oauth2Login(oauth2Login -> oauth2Login
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
.baseUri("/oauth2/authorization")
)
.redirectionEndpoint(redirectionEndpoint -> redirectionEndpoint
.baseUri("/login/oauth2/code/*")
)
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(oAuth2UserService))
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
)
.exceptionHandling(handler -> handler.authenticationEntryPoint(jwtAuthenticationEntryPoint));

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.purithm.auth.dto.response;

import lombok.Builder;

@Builder
public record LoginSuccessResponseDto(
int code,
String message,
String token
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.purithm.auth.entity;

import java.util.Collection;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

public class CustomOAuth2User implements OAuth2User {

private final String username;

public CustomOAuth2User(String username) {
this.username = username;
}

@Override
public <A> A getAttribute(String name) {
return OAuth2User.super.getAttribute(name);
}

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

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getName() {
return username;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.purithm.auth.exception;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
authException.getLocalizedMessage()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.example.purithm.auth.filter;

import com.example.purithm.auth.entity.CustomOAuth2User;
import com.example.purithm.auth.jwt.JWTUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

public class JWTFilter extends OncePerRequestFilter {

private final JWTUtil jwtUtil;

public JWTFilter(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = request.getHeader("Authorization");

if (token == null || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

token = token.substring(7);
if (jwtUtil.isExpired(token)) {
filterChain.doFilter(request, response);
return;
}

String username = jwtUtil.getUsername(token);

CustomOAuth2User oAuth2User = new CustomOAuth2User(username);
Authentication authToken = new UsernamePasswordAuthenticationToken(oAuth2User, null, null);
SecurityContextHolder.getContext().setAuthentication(authToken);
} catch (Exception e) {
request.setAttribute("exception", e);
}

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.purithm.auth.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Component
public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(response.SC_UNAUTHORIZED);
response.getOutputStream().println("{\"error\": \"Unauthorized\", \"message\": \"" + exception.getMessage() + "\"}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.purithm.auth.handler;

import com.example.purithm.auth.dto.response.LoginSuccessResponseDto;
import com.example.purithm.auth.entity.CustomOAuth2User;
import com.example.purithm.auth.jwt.JWTUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

@Component
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

private final JWTUtil jwtUtil;

public OAuth2AuthenticationSuccessHandler(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();

String token = jwtUtil.createJwt(oAuth2User.getName(), 60 * 60 * 60 * 1000L);

response.setContentType("application/json");
response.setStatus(response.SC_OK);
LoginSuccessResponseDto body = LoginSuccessResponseDto.builder()
.code(200).message("login success").token(token).build();

ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(body);

response.getOutputStream().println(json);
}
}
36 changes: 36 additions & 0 deletions purithm/src/main/java/com/example/purithm/auth/jwt/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.purithm.auth.jwt;

import io.jsonwebtoken.Jwts;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JWTUtil {
private SecretKey secretKey;

public JWTUtil(@Value("${spring.jwt.secret}")String secret) {
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}

public String getUsername(String token) {

return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}

public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}

public String createJwt(String nickname, Long expiredMs) {
return Jwts.builder()
.claim("nickname", nickname)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.example.purithm.auth.service;

import com.example.purithm.auth.entity.CustomOAuth2User;
import com.example.purithm.user.entity.User;
import com.example.purithm.user.repository.UserRepository;
import java.util.Map;
import lombok.RequiredArgsConstructor;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class OAuth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);

String provider = userRequest.getClientRegistration().getRegistrationId().toUpperCase();
Map<String, Object> properties = oAuth2User.getAttribute("properties");
String id = oAuth2User.getAttributes().get("id").toString();

String username = provider+" "+id;
User existUser = userRepository.findByUsername(username);

if (existUser == null) {
User user = User.builder()
.profile((String) properties.get("thumbnail_image"))
.nickname((String) properties.get("nickname"))
.username(username)
.build();

userRepository.save(user);
}

return new CustomOAuth2User(username);
}

}
32 changes: 32 additions & 0 deletions purithm/src/main/java/com/example/purithm/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.purithm.user.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String nickname;

private String profile;

@Builder
public User(String username, String nickname, String profile) {
this.username = username;
this.nickname = nickname;
this.profile = profile;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.purithm.user.repository;

import com.example.purithm.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
Loading