Skip to content

Commit

Permalink
Merge pull request #69 from Nexters/feature/auth
Browse files Browse the repository at this point in the history
jwt 추가
  • Loading branch information
emost22 authored Jul 13, 2024
2 parents 9e94f16 + ec59b41 commit 9c82d62
Show file tree
Hide file tree
Showing 41 changed files with 761 additions and 447 deletions.
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ dependencies {
testImplementation 'com.h2database:h2:2.1.214'
compileOnly('com.h2database:h2:2.1.214')

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

implementation group: 'com.auth0', name: 'java-jwt', version: '4.2.1'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'org.apache.commons:commons-lang3:3.4'

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

Expand Down
61 changes: 61 additions & 0 deletions src/main/java/com/sirius/spurt/common/auth/PrincipalDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.sirius.spurt.common.auth;

import com.sirius.spurt.store.repository.database.entity.UserEntity;
import java.util.Collection;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

@Getter
@Builder
public class PrincipalDetails implements UserDetails, OAuth2User {
private final UserEntity userEntity;

@Override
public String getName() {
return null;
}

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

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

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return null;
}

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

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

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

@Override
public boolean isEnabled() {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.sirius.spurt.common.auth;

import com.sirius.spurt.common.validator.UserValidator;
import com.sirius.spurt.store.repository.database.entity.UserEntity;
import com.sirius.spurt.store.repository.database.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByUserId(userId);
UserValidator.validator(userEntity);
return PrincipalDetails.builder().userEntity(userEntity).build();
}
}
60 changes: 60 additions & 0 deletions src/main/java/com/sirius/spurt/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.sirius.spurt.common.config;

import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sirius.spurt.common.filter.ExceptionHandlerFilter;
import com.sirius.spurt.common.jwt.AuthorizationFilter;
import com.sirius.spurt.common.jwt.JwtUtils;
import com.sirius.spurt.store.repository.database.repository.UserRepository;
import lombok.RequiredArgsConstructor;
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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserRepository userRepository;
private final JwtUtils jwtUtils;
private final ObjectMapper objectMapper;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(
(sessionManagement) -> sessionManagement.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(
(requests) ->
requests
.requestMatchers("/v1/question/random")
.permitAll()
.requestMatchers("/error")
.permitAll()
.anyRequest()
.authenticated())
.addFilterBefore(authorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(exceptionHandlerFilter(), LogoutFilter.class);

return http.build();
}

@Bean
public AuthorizationFilter authorizationFilter() {
return new AuthorizationFilter(userRepository, jwtUtils);
}

@Bean
public ExceptionHandlerFilter exceptionHandlerFilter() {
return new ExceptionHandlerFilter(objectMapper);
}
}
12 changes: 3 additions & 9 deletions src/main/java/com/sirius/spurt/common/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.sirius.spurt.common.config;

import com.sirius.spurt.common.resolver.LoginUserResolver;
import com.sirius.spurt.common.jwt.JwtUtils;
import com.sirius.spurt.common.resolver.NonLoginUserResolver;
import com.sirius.spurt.store.provider.auth.AuthProvider;
import com.sirius.spurt.store.provider.jobgroup.JobGroupProvider;
import com.sirius.spurt.store.provider.user.UserProvider;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
Expand All @@ -14,13 +11,10 @@
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final AuthProvider authProvider;
private final UserProvider userProvider;
private final JobGroupProvider jobGroupProvider;
private final JwtUtils jwtUtils;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginUserResolver(authProvider, userProvider, jobGroupProvider));
resolvers.add(new NonLoginUserResolver(authProvider));
resolvers.add(new NonLoginUserResolver(jwtUtils));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.sirius.spurt.common.filter;

import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sirius.spurt.common.exception.GlobalException;
import com.sirius.spurt.common.meta.ResultCode;
import com.sirius.spurt.service.controller.RestResponse;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
@RequiredArgsConstructor
public class ExceptionHandlerFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper;

@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (GlobalException e) {
setErrorResponse(response, e.getResultCode());
}
}

private void setErrorResponse(HttpServletResponse response, ResultCode resultCode)
throws IOException {
response.setCharacterEncoding(UTF_8.name());
response.setContentType(APPLICATION_JSON_VALUE);
response.setStatus(SC_OK);

try {
response.getWriter().write(objectMapper.writeValueAsString(RestResponse.error(resultCode)));
} catch (Exception e) {
log.error(e.getMessage());
} finally {
response.getWriter().close();
}
}
}
107 changes: 107 additions & 0 deletions src/main/java/com/sirius/spurt/common/jwt/AuthorizationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.sirius.spurt.common.jwt;

import static com.sirius.spurt.common.jwt.JwtUtils.ACCESS_TOKEN_NAME;
import static com.sirius.spurt.common.jwt.JwtUtils.REFRESH_TOKEN_NAME;
import static com.sirius.spurt.common.jwt.JwtUtils.TOKEN_TYPE;
import static org.springframework.http.HttpMethod.GET;

import com.sirius.spurt.common.auth.PrincipalDetails;
import com.sirius.spurt.common.validator.TokenValidator;
import com.sirius.spurt.common.validator.UserValidator;
import com.sirius.spurt.store.repository.database.entity.UserEntity;
import com.sirius.spurt.store.repository.database.repository.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
@RequiredArgsConstructor
public class AuthorizationFilter extends OncePerRequestFilter {
private final UserRepository userRepository;
private final JwtUtils jwtUtils;

private static final List<RequestMatcher> whiteList =
List.of(new AntPathRequestMatcher("/v1/question/random", GET.name()));

@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (isTestHeader(request)) {
setAuthentication("111705357761926793028");
filterChain.doFilter(request, response);
return;
}

String accessCookie = getCookie(request, ACCESS_TOKEN_NAME);
String refreshCookie = getCookie(request, REFRESH_TOKEN_NAME);

log.info("accessCookie: " + accessCookie);
log.info("refreshCookie: " + refreshCookie);
TokenValidator.validateCookie(accessCookie);

String accessToken = accessCookie.replace(TOKEN_TYPE, "");
String userId = jwtUtils.getUserId(accessToken);
if (userId == null) {
TokenValidator.validateCookie(refreshCookie);
String refreshToken = refreshCookie.replace(TOKEN_TYPE, "");
userId = jwtUtils.getUserId(refreshToken);
TokenValidator.validateUserId(userId);
jwtUtils.updateTokens(response, userId);
}

setAuthentication(userId);
filterChain.doFilter(request, response);
}

private boolean isTestHeader(HttpServletRequest request) {
return StringUtils.hasText(request.getHeader("test"));
}

private String getCookie(HttpServletRequest request, String key) {
if (ArrayUtils.isEmpty(request.getCookies())) {
return null;
}

Cookie cookie =
Arrays.stream(request.getCookies())
.filter(c -> key.equals(c.getName()))
.findFirst()
.orElse(null);
if (cookie == null) {
return null;
}

return cookie.getValue();
}

private void setAuthentication(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
UserValidator.validator(userEntity);
PrincipalDetails principalDetails = PrincipalDetails.builder().userEntity(userEntity).build();
Authentication authentication =
new UsernamePasswordAuthenticationToken(
principalDetails, null, principalDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return whiteList.stream().anyMatch(url -> url.matches(request));
}
}
Loading

0 comments on commit 9c82d62

Please sign in to comment.