Skip to content

Commit

Permalink
Merge pull request #34 from kakao-tech-campus-2nd-step3/feature/ISSUE-22
Browse files Browse the repository at this point in the history
username / jwt 인증 filter 구현
  • Loading branch information
amm0124 authored Oct 2, 2024
2 parents 60b20f4 + b9721d6 commit 83c875e
Show file tree
Hide file tree
Showing 10 changed files with 388 additions and 9 deletions.
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'


// H2 Database
// H2 Database
runtimeOnly 'com.h2database:h2'

// JSON
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package poomasi.domain.auth.config;

import jdk.jfr.Description;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import poomasi.domain.auth.util.JwtUtil;

@Configuration
public class SecurityBeanGenerator {

@Bean
@Description("AuthenticationProvider를 위한 Spring bean")
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Description("open endpoint를 위한 spring bean")
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector);
}

@Bean
@Description("jwt 토큰 발급을 위한 spring bean")
JwtUtil jwtProvider() {
return new JwtUtil();
}
}

80 changes: 75 additions & 5 deletions src/main/java/poomasi/domain/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,85 @@
package poomasi.domain.auth.config;

import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import poomasi.domain.auth.security.filter.CustomLogoutFilter;
import poomasi.domain.auth.security.filter.CustomUsernamePasswordAuthenticationFilter;
import poomasi.domain.auth.security.filter.JwtAuthenticationFilter;
import poomasi.domain.auth.util.JwtUtil;

@AllArgsConstructor
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) // 인가 처리에 대한 annotation
public class SecurityConfig {

private final AuthenticationConfiguration authenticationConfiguration;
private final JwtUtil jwtUtil;
private final MvcRequestMatcher.Builder mvc;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

// TODO : 나중에 허용될 endpoint가 많아지면 whiteList로 관리 예정
// 임시로 GET : [api/farms, api/products, api/login, api/signup, /]은 열어둠
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(HttpMethod.GET, "/api/farms/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/login", "/", "/api/signup").permitAll()
.anyRequest().
authenticated()
);

//csrf 해제
http.csrf(AbstractHttpConfigurer::disable);

//cors 해제
http.cors(AbstractHttpConfigurer::disable);

//session 해제 -> jwt token 로그인
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);

//Oauth2.0 소셜 로그인 필터 구현


//jwt 인증 필터 구현
http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), CustomUsernamePasswordAuthenticationFilter.class);

//로그인 filter 구현
http.addFilterAt(new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

//form login disable
http.formLogin(AbstractHttpConfigurer::disable);

//basic login disable
http.httpBasic(AbstractHttpConfigurer::disable);

//log out filter 추가
http.addFilterBefore(new CustomLogoutFilter(), CustomLogoutFilter.class);
return http.build();

}
}
}




Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package poomasi.domain.auth.security.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class CustomLogoutFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// access token 블랙리스트 저장해야 함
// refresh token 삭제해야 함
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package poomasi.domain.auth.security.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jdk.jfr.Description;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import poomasi.domain.auth.security.userdetail.UserDetailsImpl;
import poomasi.domain.auth.util.JwtUtil;

@RequiredArgsConstructor
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;

@Description("인증 시도 메서드")
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
return authenticationManager.authenticate(authToken);
}

@Override
@Description("로그인 성공 시, accessToken과 refreshToken 발급")
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
UserDetailsImpl customUserDetails = (UserDetailsImpl) authentication.getPrincipal();
String username = customUserDetails.getUsername();
String role = customUserDetails.getAuthority();

String accessToken = jwtUtil.generateAccessToken(username, role);
String refreshToken = jwtUtil.generateRefreshToken(username, role);

response.setHeader("access", accessToken);
response.addCookie(createCookie("refresh", refreshToken));
response.setStatus(HttpStatus.OK.value());
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
response.setStatus(401);
}

private Cookie createCookie(String key, String value) {

Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24*60*60);
cookie.setHttpOnly(true);
return cookie;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package poomasi.domain.auth.security.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jdk.jfr.Description;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import poomasi.domain.auth.util.JwtUtil;
import poomasi.domain.member.entity.Member;
import poomasi.domain.member.entity.Role;

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

@Description("access token을 검증하는 필터")
@AllArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

String accessToken = request.getHeader("access");

// refresh 재발급이나 다른 요청에 대해서 넘어감
// access <~token~>
if (accessToken == null) {
log.info("access token이 존재하지 않아서 다음 filter로 넘어갑니다.");
filterChain.doFilter(request, response);
return;
}

// 만료 검사
if(jwtUtil.isTokenExpired(accessToken)){
log.warn("[인증 실패] - 토큰이 만료되었습니다.");
PrintWriter writer = response.getWriter();
writer.print("만료된 토큰입니다.");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

// 유효성 검사
if(jwtUtil.validateToken(accessToken)) {
log.warn("[인증 실패] - 위조된 토큰입니다.");
PrintWriter writer = response.getWriter();
writer.print("위조된 토큰입니다.");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

// access token 추출하기
String tokenType = jwtUtil.getTokenTypeFromToken(accessToken);

if(!tokenType.equals("access")){
log.info("[인증 실패] - 위조된 토큰입니다.");
PrintWriter writer = response.getWriter();
writer.print("위조된 토큰입니다.");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

String username = jwtUtil.getEmailFromToken(accessToken);
String role = jwtUtil.getRoleFromToken(accessToken);

//TODO : Object, Object, Collection 형태 ..처리 해야 함
//TODO : userDetailsImpl(), null(password)
//TODO : security context에 저장해야 함.
Member member = new Member(username, Role.valueOf(role));






}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package poomasi.domain.auth.security.userdetail;

import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import poomasi.domain.member.entity.Member;

import java.util.ArrayList;
import java.util.Collection;



@Getter
public class UserDetailsImpl implements UserDetails {

//private static final long serialVersionUID = 1L;
private final Member member;

public UserDetailsImpl(Member member) {
this.member = member;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return member.getRole()
.name();
}
});

return collection;
}

public String getAuthority() {
return member.getRole().name();
}

@Override
public String getPassword() {
return this.member
.getPassword();
}

@Override
public String getUsername() {
return this.member
.getEmail();
}

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

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

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

@Override
public boolean isEnabled() {
return true;
}

}
Loading

0 comments on commit 83c875e

Please sign in to comment.