Skip to content

feat: OAuth2와 Jwt를 이용한 인증/인가 기능(#52) #60

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 26 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f0c73af
refactor: UserRepository에 JpaRepository 적용 (#52)
Sangwook02 Sep 20, 2023
45f2195
docs: security 의존성 추가 (#52)
Sangwook02 Sep 20, 2023
04ce574
feat: implement UserDetails (#52)
Sangwook02 Sep 20, 2023
7bd8ca6
refactor: JpaRepository 적용에 따른 수정 (#52)
Sangwook02 Sep 20, 2023
b5ff776
feat: security config 추가 (#52)
Sangwook02 Sep 22, 2023
4f5e654
feat: security를 위한 UserDetailService 추가 (#52)
Sangwook02 Sep 22, 2023
dc55719
Merge branch 'develop' of https://github.com/NewFit/NewFit-Backend in…
Sangwook02 Sep 22, 2023
8e5071c
chore: resolve merge conflict (#52)
Sangwook02 Sep 22, 2023
ccf14ce
feat: nickname으로 회원 조회 메서드 생성 (#52)
Sangwook02 Sep 22, 2023
1efeacc
docs: security를 위한 의존성 추가 (#52)
Sangwook02 Sep 24, 2023
08a9146
docs: security 비밀키 분리 (#52)
Sangwook02 Sep 24, 2023
245eba1
remove: 불필요한 UserDetailService 제거 (#52)
Sangwook02 Sep 24, 2023
a65a0c1
feat: OAuth2와 Jwt 적용을 위한 SecurityConfig 구현 (#52)
Sangwook02 Sep 24, 2023
9474a5b
refactor: UserDetailService 제거에 따른 수정 (#52)
Sangwook02 Sep 24, 2023
389f973
feat: CustomOAuth2User 구현 (#52)
Sangwook02 Sep 24, 2023
c1deb7a
feat: Role에 description 추가 (#52)
Sangwook02 Sep 24, 2023
c417fe4
feat: Provider에 description 추가 (#52)
Sangwook02 Sep 24, 2023
c655c91
feat: RefreshToken 구현 (#52)
Sangwook02 Sep 24, 2023
6005c66
feat: RefreshTokenRepository 구현 (#52)
Sangwook02 Sep 24, 2023
c0e0c97
feat: OAuthHistory domain, repository 구현 (#52)
Sangwook02 Sep 24, 2023
1640ae8
feat: Jwt token 발행 Property class 구현 (#52)
Sangwook02 Sep 24, 2023
8025058
feat: OAuth2 Handler 구현 (#52)
Sangwook02 Sep 24, 2023
1996c49
feat: Jwt token 생성 및 검증 (#52)
Sangwook02 Sep 24, 2023
e03a5c9
feat: Jwt token 검증 필터 (#52)
Sangwook02 Sep 24, 2023
313aec2
feat: Custom OAuth2UserService 구현 (#52)
Sangwook02 Sep 24, 2023
d5dc887
feat: 인증 요청 상태 저장을 위한 쿠키 및 repository (#52)
Sangwook02 Sep 24, 2023
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/
src/main/resources/data.sql
src/main/resources/application-security.properties

### STS ###
.apt_generated
Expand Down
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ 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-web'

// security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.newfit.reservation.common.config;

import com.newfit.reservation.common.jwt.TokenProvider;
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.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final static String AUTHENTICATION = "Authorization";
private final static String BEARER = "Bearer ";
private final TokenProvider tokenProvider;

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

if(tokenProvider.validToken(accessToken, request)) {
Authentication authentication = tokenProvider.getAuthentication(accessToken, request);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}

private String getAccessToken(HttpServletRequest request) {
String authorizationHeader = request.getHeader(AUTHENTICATION);
if (authorizationHeader != null && authorizationHeader.startsWith(BEARER)) {
return authorizationHeader.substring(BEARER.length());
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.newfit.reservation.common.config;

import com.newfit.reservation.common.jwt.TokenProvider;
import com.newfit.reservation.common.oauth.OAuth2AuthorizationRequestCookieRepository;
import com.newfit.reservation.common.oauth.handler.OAuth2FailureHandler;
import com.newfit.reservation.common.oauth.handler.OAuth2SuccessHandler;
import com.newfit.reservation.common.oauth.OAuth2UserCustomService;
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.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;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.stream.Stream;

import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final TokenProvider tokenProvider;
private final OAuth2UserCustomService oAuth2UserCustomService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final OAuth2FailureHandler oAuth2FailureHandler;

private static final String[] PERMIT_ALL_PATTERNS = new String[] {
"/login/**"
};

@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers(AntPathRequestMatcher.antMatcher("/static/**"));
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
.requestMatchers(Stream.of(PERMIT_ALL_PATTERNS).map(AntPathRequestMatcher::antMatcher).toArray(AntPathRequestMatcher[]::new)).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/api/**")).authenticated()
.anyRequest().permitAll())
.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(oAuth2UserCustomService))
.successHandler(oAuth2SuccessHandler)
.failureHandler(oAuth2FailureHandler)
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
.authorizationRequestRepository(oAuth2AuthorizationRequestCookieRepository())))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login"))
.build();
}

@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(tokenProvider);
}

@Bean
public OAuth2AuthorizationRequestCookieRepository oAuth2AuthorizationRequestCookieRepository() {
return new OAuth2AuthorizationRequestCookieRepository();
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/newfit/reservation/common/jwt/JwtProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.newfit.reservation.common.jwt;

import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
public class JwtProperties {
@Value("${jwt.issuer}")
private String issuer;

@Value("${jwt.secret_key}")
private byte[] secretKey;
}
99 changes: 99 additions & 0 deletions src/main/java/com/newfit/reservation/common/jwt/TokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.newfit.reservation.common.jwt;

import com.newfit.reservation.domain.Authority;
import com.newfit.reservation.domain.User;
import com.newfit.reservation.repository.AuthorityRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class TokenProvider {
private final JwtProperties jwtProperties;
private final AuthorityRepository authorityRepository;

public String generateToken(User user, Duration duration) {
Date now = new Date();
Date expiryAt = new Date(now.getTime() + duration.toMillis());
if (user == null) {
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiryAt)
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}

List<Long> authorityIdList = user.getAuthorityList().stream().map(Authority::getId).collect(Collectors.toList());

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiryAt)
.setSubject(user.getNickname())
.claim("authorityIdList", authorityIdList)
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}

public boolean validToken(String token, HttpServletRequest request) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token);
checkAuthorityIdList(token, request);
return true;
} catch (Exception exception) {
return false;
}
}

private void checkAuthorityIdList(String token, HttpServletRequest request) {
List<Long> authorityIdList = getAuthorityIdList(token);
Long authorityId = authorityRepository.findOne(Long.parseLong(request.getHeader("authorityId"))).orElseThrow(IllegalArgumentException::new).getId();
authorityIdList
.stream()
.filter(authority -> authority.equals(authorityId))
.findAny()
.orElseThrow(IllegalArgumentException::new);
}

public Authentication getAuthentication(String token, HttpServletRequest request) {
Claims claims = getClaims(token);
Authority authority = authorityRepository.findOne(Long.parseLong(request.getHeader("authority")))
.orElseThrow(IllegalArgumentException::new);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority(authority.getRole().getDescription()));

return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject(), "", authorities), token, authorities);
}

public List<Long> getAuthorityIdList(String token) {
Claims claims = getClaims(token);
return claims.get("authorityIdList", List.class);
}

public Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.newfit.reservation.common.oauth;

import com.newfit.reservation.domain.auth.OAuthHistory;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;

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

@Getter
public class CustomOAuth2User extends DefaultOAuth2User {
private OAuthHistory oAuthHistory;

public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey, OAuthHistory oAuthHistory) {
super(authorities, attributes, nameAttributeKey);
this.oAuthHistory = oAuthHistory;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.newfit.reservation.common.oauth;

import com.newfit.reservation.common.util.CookieUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.web.util.WebUtils;

import java.io.IOException;

public class OAuth2AuthorizationRequestCookieRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
private final static String OAUTH2_AUTHORIZATION_REQUEST = "oauth2_authorization_request";
private final static int COOKIE_EXPIRY_SECONDS= 18000;

@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
try {
Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST);
return CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class);
} catch (IOException exception) {
return null;
}
}

@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
removeAuthorizationRequestCookies(request, response);
return;
}
CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRY_SECONDS);
}

@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
return this.loadAuthorizationRequest(request);
}

public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.newfit.reservation.common.oauth;

import com.newfit.reservation.domain.auth.OAuthHistory;
import com.newfit.reservation.domain.Provider;
import com.newfit.reservation.repository.auth.OAuthHistoryRepository;
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;

import java.util.Map;

@Service
@RequiredArgsConstructor
public class OAuth2UserCustomService extends DefaultOAuth2UserService {
private final OAuthHistoryRepository oAuthHistoryRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Provider provider = Provider.getProvider(userRequest.getClientRegistration().getRegistrationId());
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String nameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
OAuthHistory oAuthHistory = findOAuthHistory(provider, (String) attributes.get(nameAttributeName));
return new CustomOAuth2User(null, attributes, nameAttributeName, oAuthHistory);
}

private OAuthHistory findOAuthHistory(Provider provider, String attributeName) {
return oAuthHistoryRepository.findByProviderAndAttributeName(provider, attributeName)
.stream().findAny()
.orElse(createdOAuthHistory(provider, attributeName));
}

private OAuthHistory createdOAuthHistory(Provider provider, String attributeName) {
return oAuthHistoryRepository.save(OAuthHistory.createOAuthHistory(provider, attributeName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.newfit.reservation.common.oauth.handler;

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

import java.io.IOException;

@Component
public class OAuth2FailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.sendRedirect("/login");
}
}
Loading