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#11 #12

Closed
wants to merge 12 commits into from
Closed
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
3 changes: 3 additions & 0 deletions classmate/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.auth0:java-jwt:4.2.1'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableJpaAuditing
@SpringBootApplication
Expand All @@ -11,5 +14,4 @@ public class ClassmateApplication {
public static void main(String[] args) {
SpringApplication.run(ClassmateApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package devteamOne.classmate.global.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import devteamOne.classmate.global.jwt.JwtService;
import devteamOne.classmate.global.jwt.filter.JwtAuthenticationProcessingFilter;
import devteamOne.classmate.global.login.filter.CustomJsonAuthenticationFilter;
import devteamOne.classmate.global.login.handler.LoginFailureHandler;
import devteamOne.classmate.global.login.handler.LoginSuccessHandler;
import devteamOne.classmate.global.login.service.LoginService;
import devteamOne.classmate.global.oauth.handler.OAuth2LoginFailureHandler;
import devteamOne.classmate.global.oauth.handler.OAuth2LoginSuccessHandler;
import devteamOne.classmate.global.oauth.service.CustomOAuth2UserService;
import devteamOne.classmate.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final ObjectMapper objectMapper;
private final UserRepository userRepository;
private final LoginService loginService;
private final JwtService jwtService;

private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
private final CustomOAuth2UserService customOAuth2UserService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin().disable()
.httpBasic().disable()
.csrf().disable()
.headers().frameOptions().disable()
.and()

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()

.authorizeRequests()
.requestMatchers("/sign-up").permitAll()
.anyRequest().authenticated()

.and()
.oauth2Login()
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint().userService(customOAuth2UserService);

http.addFilterAfter(customJsonAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationProcessingFilter(), CustomJsonAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(loginService);
return new ProviderManager(provider);
}

@Bean
public LoginSuccessHandler loginSuccessHandler() {
return new LoginSuccessHandler(jwtService, userRepository);
}

@Bean
public LoginFailureHandler loginFailureHandler() {
return new LoginFailureHandler();
}


@Bean
public CustomJsonAuthenticationFilter customJsonAuthenticationFilter() {
CustomJsonAuthenticationFilter customJsonAuthenticationFilter =
new CustomJsonAuthenticationFilter(objectMapper);

customJsonAuthenticationFilter.setAuthenticationManager(authenticationManager());
customJsonAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
customJsonAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler());

return customJsonAuthenticationFilter;
}

@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
return new JwtAuthenticationProcessingFilter(jwtService, userRepository);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package devteamOne.classmate.global.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import devteamOne.classmate.user.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.transaction.Transactional;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Getter
@Slf4j
public class JwtService {

@Value("${jwt.secretKey}")
private String secretKey;

@Value("${jwt.access.expiration}")
private Long accessTokenExpirationPeriod;

@Value("${jwt.refresh.expiration}")
private Long refreshTokenExpirationPeriod;

@Value("${jwt.access.header}")
private String accessHeader;

@Value("${jwt.refresh.header}")
private String refreshHeader;

private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String EMAIL_CLAIM = "email";
private static final String BEARER = "Bearer ";

private final UserRepository userRepository;

/**
* AccessToken 생성 λ©”μ„œλ“œ
*/
public String createAccessToken(String email) {
Date now = new Date();

return JWT.create()
.withSubject(ACCESS_TOKEN_SUBJECT)
.withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod))

.withClaim(EMAIL_CLAIM, email)
.sign(Algorithm.HMAC512(secretKey));
}

/**
* RefreshToken 생성 λ©”μ„œλ“œ
*/
public String createRefreshToken() {
Date now = new Date();

return JWT.create()
.withSubject(REFRESH_TOKEN_SUBJECT)
.withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod))
.sign(Algorithm.HMAC512(secretKey));
}

/**
* 응닡 λ©”μ‹œμ§€μ— AccessToken 헀더에 μ‹€μ–΄μ„œ 보내기
*/
public void sendAccessToken(HttpServletResponse response, String accessToken) {

response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(accessHeader, accessToken);
log.info("μž¬λ°œκΈ‰λœ Access Token : {}", accessToken);
}

/**
* 응닡 λ©”μ‹œμ§€μ— AccessToken + RefreshToken 헀더에 μ‹€μ–΄μ„œ 보내기
*/
public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) {
response.setStatus(HttpServletResponse.SC_OK);

response.setHeader(accessHeader, accessToken);
response.setHeader(refreshHeader, refreshToken);
log.info("Access Token, Refresh Token 헀더 μ„€μ • μ™„λ£Œ");
}

/**
* ν—€λ”μ—μ„œ AccessToken μΆ”μΆœ
*/
public Optional<String> extractAccessToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(accessHeader))
.filter(refreshToken -> refreshToken.startsWith(BEARER))
.map(refreshToken -> refreshToken.replace(BEARER, ""));
}

/**
* ν—€λ”μ—μ„œ RefreshToken μΆ”μΆœ
*/
public Optional<String> extractRefreshToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(refreshHeader))
.filter(refreshToken -> refreshToken.startsWith(BEARER))
.map(refreshToken -> refreshToken.replace(BEARER, ""));
}

/**
* AccessToken μ—μ„œ Email μΆ”μΆœ
*/
public Optional<String> extractEmail(String accessToken) {
try {
return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
.build()
.verify(accessToken)
.getClaim(EMAIL_CLAIM)
.asString());
} catch (Exception e) {
log.error("μ•‘μ„ΈμŠ€ 토큰이 μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
return Optional.empty();
}
}

/**
* RefreshToken DB μ—…λ°μ΄νŠΈ
*/
@Transactional
public void updateRefreshToken(String email, String refreshToken) {
userRepository.findByEmail(email)
.ifPresentOrElse(
user -> user.updateRefreshToken(refreshToken),
() -> new Exception("μΌμΉ˜ν•˜λŠ” νšŒμ›μ΄ μ—†μŠ΅λ‹ˆλ‹€.")
);
}

public boolean isTokenValid(String token) {
try {
JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
return true;
} catch (Exception e) {
log.error("μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€. {}", e.getMessage());
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package devteamOne.classmate.global.jwt.filter;

import devteamOne.classmate.global.jwt.JwtService;
import devteamOne.classmate.global.jwt.util.PasswordUtil;
import devteamOne.classmate.user.domain.User;
import devteamOne.classmate.user.repository.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {

private static final String NO_CHECK_URL = "/login";

private final JwtService jwtService;
private final UserRepository userRepository;

private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response);
return;
}

String refreshToken = jwtService.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null);

if (refreshToken != null) {
checkRefreshTokenAndReIssueAccessToken(response, refreshToken);
return;
}

if (refreshToken == null) {
checkAccessTokenAndAuthentication(request, response, filterChain);
}

}

private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

log.info("call checkAccessTokenAndAuthentication()");

jwtService.extractAccessToken(request)
.filter(jwtService::isTokenValid)
.ifPresent(accessToken -> jwtService.extractEmail(accessToken)
.ifPresent(email -> userRepository.findByEmail(email)
.ifPresent(this::saveAuthentication)));

filterChain.doFilter(request, response);
}

private void saveAuthentication(User user) {
String password = user.getPassword();

if (password == null) {
password = PasswordUtil.generateRandomPassword();
}

UserDetails userDetails = org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(password)
.roles(user.getRole().name())
.build();

Authentication authentication =
new UsernamePasswordAuthenticationToken(userDetails, null
, authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));

SecurityContextHolder.getContext().setAuthentication(authentication);
}

public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
userRepository.findByRefreshToken(refreshToken)
.ifPresent(user -> {
String reIssuedRefreshToken = reIssueRefreshToken(user);
jwtService.sendAccessAndRefreshToken(response, jwtService.createAccessToken(user.getEmail()),
reIssuedRefreshToken);
});
}

private String reIssueRefreshToken(User user) {
String reIssuedRefreshToken = jwtService.createRefreshToken();
user.updateRefreshToken(reIssuedRefreshToken);
userRepository.saveAndFlush(user);
return reIssuedRefreshToken;
}
}
Loading