Skip to content

Commit

Permalink
Merge pull request #5 from Team-Going/feature/3
Browse files Browse the repository at this point in the history
[feat] Spring Security & JWT 설정
  • Loading branch information
SunwoongH authored Jan 3, 2024
2 parents 19a2ec9 + 3692f7d commit cc43dc3
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 1 deletion.
6 changes: 5 additions & 1 deletion doorip-api/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
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-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation project(path: ':doorip-domain')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.doorip.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.doorip.common.ApiResponse;
import org.doorip.common.Constants;
import org.doorip.exception.UnauthorizedException;
import org.doorip.message.ErrorMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.io.PrintWriter;

@Slf4j
public class ExceptionHandlerFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
try {
filterChain.doFilter(request, response);
} catch (UnauthorizedException e) {
handleUnauthorizedException(response, e);
} catch (Exception ee) {
handleException(response, ee);
}
}

private void handleUnauthorizedException(HttpServletResponse response, Exception e) throws IOException {
UnauthorizedException ue = (UnauthorizedException) e;
ErrorMessage errorMessage = ue.getErrorMessage();
HttpStatus httpStatus = errorMessage.getHttpStatus();
setResponse(response, httpStatus, errorMessage);
}

private void handleException(HttpServletResponse response, Exception e) throws IOException {
log.error(">>> Exception Handler Filter : ", e);
setResponse(response, HttpStatus.INTERNAL_SERVER_ERROR, ErrorMessage.INTERNAL_SERVER_ERROR);
}

private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorMessage errorMessage) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(Constants.CHARACTER_TYPE);
response.setStatus(httpStatus.value());
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(ApiResponse.of(errorMessage)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.doorip.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.doorip.common.ApiResponse;
import org.doorip.common.Constants;
import org.doorip.message.ErrorMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
handleException(response);
}

private void handleException(HttpServletResponse response) throws IOException {
setResponse(response, HttpStatus.UNAUTHORIZED, ErrorMessage.UNAUTHORIZED);
}

private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorMessage errorMessage) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(Constants.CHARACTER_TYPE);
response.setStatus(httpStatus.value());
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(ApiResponse.of(errorMessage)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.doorip.auth;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.doorip.auth.jwt.JwtProvider;
import org.doorip.auth.jwt.JwtValidator;
import org.doorip.common.Constants;
import org.doorip.exception.UnauthorizedException;
import org.doorip.message.ErrorMessage;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

import static org.doorip.auth.UserAuthentication.createUserAuthentication;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtValidator jwtValidator;
private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String accessToken = getAccessToken(request);
jwtValidator.validateAccessToken(accessToken);
doAuthentication(request, jwtProvider.getSubject(accessToken));
filterChain.doFilter(request, response);
}

private String getAccessToken(HttpServletRequest request) {
String accessToken = request.getHeader(Constants.AUTHORIZATION);
if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) {
return accessToken.substring(Constants.BEARER.length());
}
throw new UnauthorizedException(ErrorMessage.INVALID_ACCESS_TOKEN);
}

private void doAuthentication(HttpServletRequest request, Long userId) {
UserAuthentication authentication = createUserAuthentication(userId);
createAndSetWebAuthenticationDetails(request, authentication);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
}

private void createAndSetWebAuthenticationDetails(HttpServletRequest request, UserAuthentication authentication) {
WebAuthenticationDetailsSource webAuthenticationDetailsSource = new WebAuthenticationDetailsSource();
WebAuthenticationDetails webAuthenticationDetails = webAuthenticationDetailsSource.buildDetails(request);
authentication.setDetails(webAuthenticationDetails);
}
}
16 changes: 16 additions & 0 deletions doorip-api/src/main/java/org/doorip/auth/UserAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.doorip.auth;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class UserAuthentication extends UsernamePasswordAuthenticationToken {
private UserAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}

public static UserAuthentication createUserAuthentication(Long userId) {
return new UserAuthentication(userId, null, null);
}
}
11 changes: 11 additions & 0 deletions doorip-api/src/main/java/org/doorip/auth/UserId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.doorip.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.doorip.auth;

import org.springframework.core.MethodParameter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasUserIdAnnotation = parameter.hasParameterAnnotation(UserId.class);
boolean isLongType = Long.class.isAssignableFrom(parameter.getParameterType());
return hasUserIdAnnotation && isLongType;
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.doorip.auth.config;

import lombok.RequiredArgsConstructor;
import org.doorip.auth.ExceptionHandlerFilter;
import org.doorip.auth.JwtAuthenticationEntryPoint;
import org.doorip.auth.JwtAuthenticationFilter;
import org.doorip.auth.jwt.JwtProvider;
import org.doorip.auth.jwt.JwtValidator;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtValidator jwtValidator;
private final JwtProvider jwtProvider;

private static final String[] whiteList = {"/api/users/signin", "/api/users/signup", "/api/users/reissue", "/"};

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagementConfigurer ->
sessionManagementConfigurer
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptionHandlingConfigurer ->
exceptionHandlingConfigurer
.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry
.anyRequest()
.authenticated())
.addFilterBefore(new JwtAuthenticationFilter(jwtValidator, jwtProvider), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new ExceptionHandlerFilter(), JwtAuthenticationFilter.class)
.build();
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(whiteList);
}
}
20 changes: 20 additions & 0 deletions doorip-api/src/main/java/org/doorip/auth/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.doorip.auth.config;

import lombok.RequiredArgsConstructor;
import org.doorip.auth.UserIdArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final UserIdArgumentResolver userIdArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userIdArgumentResolver);
}
}
65 changes: 65 additions & 0 deletions doorip-api/src/main/java/org/doorip/auth/jwt/JwtGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.doorip.auth.jwt;

import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Component
public class JwtGenerator {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.access-token-expire-time}")
private long ACCESS_TOKEN_EXPIRE_TIME;
@Value("${jwt.refresh-token-expire-time}")
private long REFRESH_TOKEN_EXPIRE_TIME;

public String generateToken(Long userId, boolean isAccessToken) {
final Date now = generateNowDate();
final Date expiration = generateExpirationDate(isAccessToken, now);
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setSubject(String.valueOf(userId))
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}

public JwtParser getJwtParser() {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build();
}

private Date generateNowDate() {
return new Date();
}

private Date generateExpirationDate(boolean isAccessToken, Date now) {
return new Date(now.getTime() + calculateExpirationTime(isAccessToken));
}

private Key getSigningKey() {
return Keys.hmacShaKeyFor(encodeSecretKey().getBytes());
}

private long calculateExpirationTime(boolean isAccessToken) {
if (isAccessToken) {
return ACCESS_TOKEN_EXPIRE_TIME;
}
return REFRESH_TOKEN_EXPIRE_TIME;
}

private String encodeSecretKey() {
return Base64.getEncoder()
.encodeToString(secretKey.getBytes());
}
}
23 changes: 23 additions & 0 deletions doorip-api/src/main/java/org/doorip/auth/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.doorip.auth.jwt;

import io.jsonwebtoken.JwtParser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class JwtProvider {
private final JwtGenerator jwtGenerator;

public Token issueToken(Long userId) {
return Token.of(jwtGenerator.generateToken(userId, true),
jwtGenerator.generateToken(userId, false));
}

public Long getSubject(String token) {
JwtParser jwtParser = jwtGenerator.getJwtParser();
return Long.valueOf(jwtParser.parseClaimsJws(token)
.getBody()
.getSubject());
}
}
Loading

0 comments on commit cc43dc3

Please sign in to comment.