Skip to content

Commit

Permalink
Feat: JWT 발급 및 검증 클래스, 필터 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
yxhwxn committed Jul 29, 2024
1 parent 5957636 commit 6163946
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 1 deletion.
110 changes: 110 additions & 0 deletions src/main/java/com/cmc/suppin/global/security/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.cmc.suppin.global.security;

import com.cmc.suppin.global.security.jwt.JWTFilter;
import com.cmc.suppin.global.security.jwt.JWTUtil;
import com.cmc.suppin.global.security.jwt.LoginFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Collections;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

//AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
private final AuthenticationConfiguration authenticationConfiguration;

//JWTUtil 주입
private final JWTUtil jwtUtil;

public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) {

this.authenticationConfiguration = authenticationConfiguration;
this.jwtUtil = jwtUtil;
}

//AuthenticationManager Bean 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {

return configuration.getAuthenticationManager();
}

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {

return new BCryptPasswordEncoder();
}

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

http
.cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {

@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);

configuration.setExposedHeaders(Collections.singletonList("Authorization"));

return configuration;
}
})));

//csrf disable
http
.csrf((auth) -> auth.disable());

//From 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());

//http basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());

//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());

//JWTFilter 추가
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

//LoginFilter 추가 LoginFilter()는 인자를 받음 (AuthenticationManager() 메소드에 authenticationConfiguration 객체를 넣어야 함) 따라서 등록 필요
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

//세션 설정
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));


return http.build();
}
}

74 changes: 74 additions & 0 deletions src/main/java/com/cmc/suppin/global/security/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.cmc.suppin.global.security.jwt;

import com.cmc.suppin.member.controller.dto.MemberDetails;
import com.cmc.suppin.member.domain.Member;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JWTFilter extends OncePerRequestFilter {

private final JWTUtil jwtUtil;

public JWTFilter(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}

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

//request에서 Authorization 헤더를 찾음
String authorization = request.getHeader("Authorization");

//Authorization 헤더 검증(토큰이 없는 경우 처리해주는 부분)
if (authorization == null || !authorization.startsWith("Bearer ")) {

System.out.println("token null");
filterChain.doFilter(request, response);

//조건이 해당되면 메소드 종료 (필수)
return;
}

//Bearer 부분 제거 후 순수 토큰만 획득
String token = authorization.split(" ")[1];

//토큰 소멸 시간 검증(토큰 만료시 처리해주는 부분)
if (jwtUtil.isExpired(token)) {

System.out.println("token expired");
filterChain.doFilter(request, response);

//조건이 해당되면 메소드 종료 (필수)
return;
}

//토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);

// Member Entity를 생성하여 값 set
Member member = new Member();
member.setName(username);
member.setPassword("tempPassword");
member.setRole(role);

//MemberDetails에 회원 정보 객체 담기
MemberDetails customUserDetails = new MemberDetails(member);

//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);

filterChain.doFilter(request, response);
}
}

48 changes: 48 additions & 0 deletions src/main/java/com/cmc/suppin/global/security/jwt/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.cmc.suppin.global.security.jwt;

import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JWTUtil {

private SecretKey secretKey;

public JWTUtil(@Value("${JWT_TOKEN_SECRET}") String secret) {

this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}

public String getUsername(String token) {

return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}

public String getRole(String token) {

return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}

public Boolean isExpired(String token) {

return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}

public String createJwt(String username, String role, Long expiredMs) {

return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}

77 changes: 77 additions & 0 deletions src/main/java/com/cmc/suppin/global/security/jwt/LoginFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.cmc.suppin.global.security.jwt;

import com.cmc.suppin.member.controller.dto.MemberDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.Collection;
import java.util.Iterator;

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

private final AuthenticationManager authenticationManager;

private final JWTUtil jwtUtil;

public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {

this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

//클라이언트 요청에서 username, password 추출
String username = obtainUsername(request);
String password = obtainPassword(request);

//스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);

//token에 담은 검증을 위한 AuthenticationManager로 전달
return authenticationManager.authenticate(authToken);
}

//로그인 성공시 실행하는 메소드 (여기서 JWT를 발급됨)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {

MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();

String username = memberDetails.getUsername();

Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.isEmpty()) {
throw new IllegalStateException("권한이 없습니다.");
}

Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();

String role = auth.getAuthority();

long expirationTime = 1000 * 60 * 60 * 24 * 7; // 7일

String token = jwtUtil.createJwt(username, role, expirationTime);

response.addHeader("Authorization", "Bearer " + token);
}


//로그인 실패시 실행하는 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {

//로그인 실패시 401 응답코드 반환
response.setStatus(401);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.cmc.suppin.member.controller.dto;

import com.cmc.suppin.member.domain.Member;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class MemberDetails implements UserDetails {

private final Member member;

public MemberDetails(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();
}
});

return collection;
}

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

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

@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 6163946

Please sign in to comment.