스프링 시큐리티 + Github Oauth + RESTAPI + JWT 활용(With Gradle)
- -
개요
이전에 개발 동아리 홈페이지 제작 프로젝트 중에 깃허브를 활용하여 동아리 회원 인증을 하자는 의견으로 Github Oauth + RestAPI + JWT 구현 했던 내용을 정리한다.
굳이 Github OAuth를 사용해야 했던 이유
동아리의 모든 인원이 github가입이 필수
홈페이지에 github api와 연동해야 했음(깃허브 커밋시 활동 포인트 증가 기능)
소개
JWT의 동작의 장점은 따로 세션 서버를 구성하지 않고도 확장성이 뛰어나다는 점이다. 또한 토큰을 서버에서 관리하지 않고 클라이언트 쪽에서 관리하기 때문에 서버 쪽 부담이 적다.
기본적으로 스프링 시큐리티는 세션 기반으로 동작한다. 이를 JWT 기반 동작으로 변경해야 하는데, 시큐리티는 변화에 유연하기 때문에 기존 동작들을 사용자가 원하는 방식으로 적절하게 구현할 수 있다.
전체 인증 플로우
Spring Security와 OAuth를 활용한 인증/인가 과정의 전체적인 플로우는 위와 같은 형식으로 진행이 된다.
주요 클래스 소개
SecurityFilterChain내 filterChain메서드를 통해 전반적인 설정을 진행할 수 있는데, 이때 JWT를 위한 설정들을 커스텀하여 설정할 수 있다.
1. CookieAuthorizationRequestRepository
OAUTH 인증요청 관련 인터페이스인 AuthorizationRequestRepository을 세션이 아닌 쿠키 방식으로 구현할 클래스이다. 해당 클래스는 provider에 인증 요청을 보내고 인증 성공 후 검증을 위해 확인하는 state필드를 확인하기 위해 사용되는 클래스이다.
쿠키에 저장했던 state(요청객체)값을 응답때 검증하는 용도로 사용한다.
또한 입력받은 redirect_uri값을 저장하는 용도로도 사용된다. 해당 redirect_uri값은 인증 / 인가 과정이 끝나고 최종적으로 리다이렉트 할 주소를 저장한다.(서버에 저장된 redirect_uri값과 일치해야 한다.)
2. CustomOauthUserService
해당 클래스는 provider(github)로부터 access token으로 사용자 정보를 받아와 처리하는 클래스이다. loadUser메서드를 통해 사용자 정보로 인증 객체를 만들고 회원가입을 하는 등의 처리를 한다.
3. OAuth2AuthenticationSuccessHandler, OAuth2AuthenticationFailureHandler
인증과정이 성공적으로 이루어지거나 실패했을 때 불려지는 핸들러이다. 보통 성공했을 경우, 쿠키해 저장했던 redirect_uri(프론트 주소)로 access 토큰과 refresh토큰을 제공하며 redirect시킨다.
실패했을 때는 redirect_uri로 에러를 반환한다.
환경 설정
작업환경
Gradle
Springboot 2.7.13
Springsecurity
SpringOAuth
MySQL 8.0.0
Objectmapper
Java 11
Github 환경설정 값
1️⃣ 깃허브 접속 후 본인 프로필 사진을 누르고 다음과 같은 창에서 setting을 클릭한다.
2️⃣ 좌측 메뉴 리스트에서 맨 아래에 있는 Developer setting을 클릭한다.
3️⃣ 좌측 메뉴 리스트에서 OAuth Apps 클릭하고 New OAuth App을 클릭한다.
4️⃣ Application name 과 Hompage URL을 입력하고 Authorization callback URL을 입력한다. Authorization callback URL은 spring.security.oauth2.client.registration.github.redirect-uri 설정 값을 입력하면 된다.
5️⃣ 이후에 Client ID를 저장하고 Client secrets를 생성하여 저장하고 해당 값을 설정 값에 입력한다.
기본 컨트롤러 및 서비스
단순하게 Oauth를 통한 인증 및 인가 기능만 구현 할 것이다. API는 본인 인증 API와 Refreshtoken 갱신 API 두 개만 구현했다.
User.java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Builder
public User(String email, String name, String githubId, String refreshToken) {
this.email = email;
this.name = name;
this.githubId = githubId;
this.refreshToken = refreshToken;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String githubId;
@Column
private String refreshToken;
}
UserController.java
@Log4j2
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserRepository userRepository;
@GetMapping("/users/me")
@PreAuthorize("hasRole('USER')")
public User getCurrentUser(@AuthenticationPrincipal CustomUserDetails user) {
return userRepository.findById(user.getId())
.orElseThrow(() -> new IllegalStateException("등록된 유저가 아닙니다."));
}
}
UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByGithubId(String githubId);
@Modifying
@Query(value = "UPDATE User u SET u.refreshToken=:token WHERE u.id=:id")
@Transactional
void updateRefreshToken(@Param("id") Long id, @Param("token") String token);
@Query("Select u.refreshToken FROM User u WHERE u.id=:id")
String getRefreshTokenById(@Param("id") Long id);
}
AuthController.java
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/refresh")
public ResponseEntity refreshToken(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String, String> accessToken) {
return ResponseEntity.ok()
.body(
authService.refreshToken(request, response, accessToken.get("accessToken"))
);
}
}
AuthService.java
@Log4j2
@Service
@RequiredArgsConstructor
public class AuthService {
@Value("${app.auth.token.refresh-cookie-key}")
private String cookieKey;
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
public String refreshToken(HttpServletRequest request, HttpServletResponse response, String oldAccessToken) {
String oldRefreshToken = CookieUtil.getCookie(request, cookieKey)
.map(Cookie::getValue).orElseThrow(() -> new RuntimeException("refreshToken이 없습니다."));
if(!jwtTokenProvider.validateToken(oldRefreshToken)) {
throw new RuntimeException("Not Validated Refresh Token");
}
Authentication authentication = jwtTokenProvider.getAuthentication(oldAccessToken);
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
Long id = Long.valueOf(user.getName());
String savedToken = userRepository.getRefreshTokenById(id);
if (!savedToken.equals(oldRefreshToken)) {
throw new RuntimeException("refreshToken이 일치하지 않습니다.");
}
String accessToken = jwtTokenProvider.createAccessToken(authentication);
jwtTokenProvider.createRefreshToken(authentication, response);
return accessToken;
}
}
Refreshtoken을 재발급 하기 위한 서비스 클래스이다.
시큐리티 구성
시큐리티 필터를 JWT 인증 방식에 맞추어 변경해야 한다. 세션 방식을 JWT로 변경해야 할 부분들을 커스텀 필터 / 핸들러로 교체한다.
WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("<http://localhost:3000>")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Cors 설정을 위한 클래스이다. 스프링 부트 프로젝트이기 때문에 @Configuration으로 빈으로 등록해주는 것만으로 동작한다.
WebSecurityConfigure.java
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfigure {
private final CustomOAuth2UserService customOAuth2UserService;
private final CookieAuthorizationRequestRepository cookieAuthorizationRequestRepository;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.cors()
.and()
.httpBasic().disable()
.csrf().disable()
.formLogin().disable()
.rememberMe().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//oauth2Login
http.oauth2Login()
.authorizationEndpoint().baseUri("/api/login/oauth2/code") // 소셜 로그인 url
.authorizationRequestRepository(cookieAuthorizationRequestRepository) // 인증 요청을 cookie 에 저장
.and()
.redirectionEndpoint().baseUri("/api/login/oauth/redirect/github") // 소셜 인증 후 redirect url
.and()
// userService()는 OAuth2 인증 과정에서 Authentication 생성에 필요한 OAuth2User 를 반환하는 클래스를 지정한다.
.userInfoEndpoint().userService(customOAuth2UserService) // 회원 정보 처리
.and()
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler);
http.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler);
//jwt filter 설정
http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
- oauth2Login()
- authorizationEndpoint : 프론트엔드에서 백엔드로 소셜로그인 요청을 보내는 URI이다.
- 기본 URI는 /oauth2/authorization/{provider} 이다. ex) /oauth2/authorization/google
- URI를 변경하고 싶으면 baseUri(uri)를 사용하여 설정한다..
- 위에 같이 설정하면 /oauth2/authorize/{provider}가 된다. ex) /oauth2/authorize/google
- authorizationRequestRepository : Authorization 과정에서 기본으로 Session을 사용하지만 Cookie로 변경하기 위해 설정한다
- redirectionEndpoint : Authorization 과정이 끝나면 Authorization Code와 함께 리다이렉트할 URI이다
- 기본 URI는 /login/oauth2/code/{provider} 이다. ex) /login/oauth2/code/google
- URI를 변경하고 싶으면 마찬가지로 baseUri(uri)를 사용하여 설정한다..
- 위에 같이 설정하면 /oauth2/callback/{provider}가 된다. ex) /oauth2/callback/google
- userInfoEndPoint : Provider로부터 획득한 유저정보를 다룰 service class를 지정한다
- successHandler : OAuth2 로그인 성공시 호출할 handler 이다.
- failureHandler : OAuth2 로그인 실패시 호출할 handler 이다.
- authorizationEndpoint : 프론트엔드에서 백엔드로 소셜로그인 요청을 보내는 URI이다.
- exceptionHandling() : JWT를 다룰 때 생길 excepion을 처리할 class를 지정한다
- authenticationEntryPoint : 인증 과정에서 생길 exception을 처리 한다.
- accessDeniedHandler : 인가 과정에서 생길 exception을 처리 한다.
- addFilterBefore() : 모든 request에서 JWT를 검사할 filter를 추가한다.
- UsernamePasswordAuthenticationFilter에서 클라이언트가 요청한 리소스의 접근권한이 없을 때 막는 역할을 하기 때문에 이 필터 전에 jwtAuthenticationFilter를 실행 한다.
인증 / 인가시 검증 관련
처음 요청이 들어올 때 기존 HttpSessionOAuth2AuthorizationRequestRepository 클래스를 통해 요청을 검증하기 위해 저장했던 부분을 쿠키로 저장하기 위한 클래스이다. 요청에 대한 정보를 저장하고 provider와의 요청을 검증하기 위해 사용되거나, redirect_url(프론트)정보를 저장할 때 사용된다.
CookieAuthorizationRequestRepository.java
- oauth2_auth_request: 요청정보를 저장하여 깃허브와의 통신을 검증하는 데 사용된다.
- redirect_uri: 요청 받을 때, 해당 요청으로 담겨져 온 redirect_uri정보를 담는다. 인증이 끝나고 검증 후 해당 uri로 리다이렉트 한다.
- 인증 요청시 생성된 2개의 쿠키는 인증이 종료될 때 실행되는 successHandler와 failureHandler에서 제거된다
@Component
public class CookieAuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri";
private static final int COOKIE_EXPIRE_SECONDS = 180;
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return CookieUtil.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
.map(cookie -> CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class))
.orElse(null);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
return;
}
CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME);
if (StringUtils.isNotBlank(redirectUriAfterLogin)) {
CookieUtil.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, COOKIE_EXPIRE_SECONDS);
}
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) {
return this.loadAuthorizationRequest(request);
}
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME);
}
}
인증 객체 관련
provider와의 통신을 통해 해당 유저의 정보를 받아오고 인증객체에 저장하기 위해 사용된다.
CustomOauthUserService.java
DefaultOAuth2UserService의 loadUser() 메서드를 통해 provider로부터 유저 정보를 받아온 후 인증객체를 저장하고, 회원가입이 진행되지 않은 경우 회원가입을 진행한다.
@Service
@RequiredArgsConstructor
public class CustomOAuthUserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
GithubOAuth2UserInfo userInfo = new GithubOAuth2UserInfo(oAuth2User.getAttributes());
//회원이 아닐 경우 회원 가입 진행
User user = userRepository.findByGithubId(userInfo.getId())
.orElseGet(() -> createUser(userInfo));
return CustomUserDetails.create(user, oAuth2User.getAttributes());
}
private User createUser(GithubOAuth2UserInfo userInfo) {
User user = User.builder()
.githubId(userInfo.getId())
.name(userInfo.getName())
.email(userInfo.getEmail())
.build();
return userRepository.save(user);
}
}
CustomUserDetails.java
Security세션에 저장될 유저 정보이다. 인증객체 내에 저장된다. 사용자를 검증하는데 사용된다.
@Getter
public class CustomUserDetails implements UserDetails, OAuth2User {
private Long id;
private String email;
private Collection<? extends GrantedAuthority> authorities;
private Map<String, Object> attributes;
public CustomUserDetails(Long id, String email, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.email = email;
this.authorities = authorities;
}
public static CustomUserDetails create(User user) {
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
return new CustomUserDetails(
user.getId(),
user.getEmail(),
authorities
);
}
public static CustomUserDetails create(User user, Map<String, Object> attributes) {
CustomUserDetails userDetails = CustomUserDetails.create(user);
userDetails.setAttributes(attributes);
return userDetails;
}
@Override
public String getName() {
return String.valueOf(id);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, Object> attributes) {
this.attributes = attributes;
}
}
GithubOAuth2UserInfo.java
Github로부터 정보를 필요한 정보를 저장하는 클래스이다.
public class GithubOAuth2UserInfo {
private Map<String, Object> attributes;
public GithubOAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getId() {
return (String) attributes.get("login");
}
public String getName() {
return (String) attributes.get("name");
}
public String getEmail() {
return (String) attributes.get("email");
}
}
인증 성공 / 실패 핸들러
OAuth2AuthenticationFailureHandler.java
OAuth 인증 실패 시 호출되는 핸들러이다. 초기에 쿠키에 저장되어 있는 프론트 url로 redirect를 통해 오류를 반환한다.
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final CookieAuthorizationRequestRepository authorizationRequestRepository;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String targetUrl = CookieUtil.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
.map(Cookie::getValue)
.orElse("/");
targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("error", exception.getLocalizedMessage())
.build().toUriString();
authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
OAuth2AuthenticationSuccessHandler.java
인증에 성공했을 경우 호출되는 핸들러이다. 초기 저장된 redirect_url쿠키를 검증(application.prperties파일의 설정 값과 비교)하고, 일치할 경우 accesstoken과 refreshtoken을 쿼리스트링과, 쿠키를 통해 redirect_url에 리다이렉트로 반환한다.
import static com.oauth.githuboauth.security.CookieAuthorizationRequestRepository.REDIRECT_URI_PARAM_COOKIE_NAME;
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Value("${app.oauth2.authorizedRedirectUri}")
private String redirectUri;
private final JwtTokenProvider tokenProvider;
private final CookieAuthorizationRequestRepository authorizationRequestRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String targetUrl = deterMineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
return;
}
clearAuthenticationAttributes(request, response);
if(response.getStatus() != HttpServletResponse.SC_BAD_REQUEST) {
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
protected String deterMineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
Optional<String> redirect = CookieUtil.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
.map(Cookie::getValue);
ObjectMapper objectMapper = new ObjectMapper();
if (redirect.isPresent() && !isAuthorizedRedirectUri(redirect.get())) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getWriter(), new ErrorResponse(HttpServletResponse.SC_BAD_REQUEST, "redirect_uri가 일치하지 않습니다."));
}
String targetUrl = redirect.orElse(getDefaultTargetUrl());
//JWT 생성
String accessToken = tokenProvider.createAccessToken(authentication);
tokenProvider.createRefreshToken(authentication, response);
return UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("accessToken", accessToken)
.build().toUriString();
}
protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
private boolean isAuthorizedRedirectUri(String uri) {
URI clientRedirectUri = URI.create(uri);
URI authorizedUri = URI.create(redirectUri);
return authorizedUri.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
&& authorizedUri.getPort() == clientRedirectUri.getPort();
}
}
JWT 처리 및 인증/인가 관련
JwtAuthenticationEntryPoint.java
인증되지 않은 사용자가 인증이 필요한 요청 엔드포인트로 접근하려 할 때, 예외를 핸들링 할 수 있도록 도와준다.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getLocalizedMessage());
}
}
JwtAuthenticationFilter.java
accessToken을 통해 사용자를 인증하고, 인증객체를 생성하여 시큐리티 인증 세션에 저장하는 필터이다.
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = parseBearerToken(request);
//Access Token 검증
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
private String parseBearerToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
JwtTokenProvider.java
토큰 검증, 생성, 저장 등 토큰과 관련된 기능을 담당하는 클래스이다.
@Component
@Slf4j
public class JwtTokenProvider {
private final Key SECRET_KEY;
private final String COOKIE_REFRESH_TOKEN_KEY;
private final long ACCESS_TOKEN_EXPIRE_LENGTH = 1000L * 60 * 60;
private final long REFRESH_TOKEN_EXPIRE_LENGTH = 1000L * 60 * 60 * 24 * 7;
private final String AUTHORITIES_KEY = "role";
private final String EMAIL_KEY = "email";
private final UserRepository userRepository;
public JwtTokenProvider(@Value("${app.auth.token.secret-key}")String secretKey, @Value("${app.auth.token.refresh-cookie-key}")String cookieKey, UserRepository userRepository) {
this.SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
this.COOKIE_REFRESH_TOKEN_KEY = cookieKey;
this.userRepository = userRepository;
}
public String createAccessToken(Authentication authentication) {
Date now = new Date();
Date validity = new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_LENGTH);
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
String userId = user.getName();
String email = user.getUsername();
String role = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(userId)
.claim(AUTHORITIES_KEY, role)
.claim(EMAIL_KEY, email)
.setIssuer("bok")
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.compact();
}
public void createRefreshToken(Authentication authentication, HttpServletResponse response) {
Date now = new Date();
Date validity = new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_LENGTH);
String refreshToken = Jwts.builder()
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.setIssuer("bok")
.setIssuedAt(now)
.setExpiration(validity)
.compact();
saveRefreshToken(authentication, refreshToken);
ResponseCookie cookie = ResponseCookie.from(COOKIE_REFRESH_TOKEN_KEY, refreshToken)
.httpOnly(false)
.secure(true)
.sameSite("None")
.maxAge(REFRESH_TOKEN_EXPIRE_LENGTH / 1000)
.path("/")
.build();
response.addHeader("Set-Cookie", cookie.toString());
}
private void saveRefreshToken(Authentication authentication, String refreshToken) {
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
long id = Long.parseLong(user.getName());
userRepository.updateRefreshToken(id, refreshToken);
}
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
CustomUserDetails principal = new CustomUserDetails(Long.valueOf(claims.getSubject()), claims.get(EMAIL_KEY, String.class), authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public Boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token).getBody();
return true;
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (JwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalStateException e) {
log.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
// Access Token 만료시 갱신 때 사용할 정보를 얻기 위해 Claim 리턴
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
JwtAccessDeniedHandler.java
인증은 완료되었으나 요청에 대한 권한을 가지고 있지 않은 사용자가 엔드포인트에 접근하려고 할 때 발생한 예외를 잡아서 응답하도록 한다.
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
유틸리티 클래스
CookieUtil.java
쿠키와 관련된 기능을 하는 클래스이다.
public class CookieUtil {
public static Optional<Cookie> getCookie(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for(Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return Optional.of(cookie);
}
}
}
return Optional.empty();
}
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie: cookies) {
if (cookie.getName().equals(name)) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
}
}
public static String serialize(Object object) {
return Base64.getUrlEncoder()
.encodeToString(SerializationUtils.serialize(object));
}
public static <T> T deserialize(Cookie cookie, Class<T> cls) {
return cls.cast(SerializationUtils.deserialize(
Base64.getUrlDecoder().decode(cookie.getValue())
));
}
}
ErrorResponse.java
에러를 반환하기 위한 클래스이다.
@Getter
public class ErrorResponse {
private final int code;
private final String message;
public ErrorResponse(int code, String message) {
this.code = code;
this.message = message;
}
}
'Spring' 카테고리의 다른 글
[Spring Security] 스프링 시큐리티 권한처리 하기 (0) | 2024.06.04 |
---|---|
[스프링 부트 + 시큐리티 + REST] STOMP를 통한 채팅 구현(JWT를 통한 사용자 인증) (0) | 2024.05.29 |
[Spring] 롬복을 사용한 Builder 패턴 Null체크 하며 사용하기 (0) | 2023.11.10 |
우아한 객체지향 [우아한테크세미나] 정리 5 (0) | 2023.11.08 |
우아한 객체지향 [우아한테크세미나] 정리 4 (1) | 2023.11.08 |
소중한 공감 감사합니다