1. 의존성 가져오기
2. Config 설정
3. Cors 재설정
4. PasswordEncoder 설정
5. 엔티티 셋팅
6. MemberDTO 구현
7. UserDetailsService 구현
8. JWT 생성
9. RefreshToken 생성
[1. 의존성 가져오기]
build.gradle 에서
아래 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
[2. Config설정]
CustomSecurityConfig.java 파일 생성 후 설정하기
package com.zelkova.zelkova.config;
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.web.SecurityFilterChain;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Configuration
@RequiredArgsConstructor
@Log4j2
public class CustomSecurity {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("-------------security config ----------");
return http.build();
}
}
security 관련 로그 출력을 위해
"application.properties" 에 설정하기
loggin.level.org.springframework.security.web=trace
CORS설정, CSRF 공격 방어용 설정
package com.zelkova.zelkova.config;
import java.util.Arrays;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Configuration
@RequiredArgsConstructor
@Log4j2
public class CustomSecurity {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("-------------security config ----------");
http.cors(httpSecurityCorsConfigurer -> {
httpSecurityCorsConfigurer.configurationSource(configurationSource());
});
// 요청이 들어올 때 마다 세션 생성을 막음
http.sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// GET 이외 요청시 "CSRF 토큰"과 함께 요청이 와야함. 지금은 해제
http.csrf(config -> config.disable());
return http.build();
}
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*")); // CORS 설정할 경로
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE")); // CORS 가능한 Method
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type")); // CORS 가능한 Header
configuration.setAllowCredentials(true); // 자격증명에 대한 위임 여부 설정
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
http.csrf(config -> config.disable());
위 코드는
security가 컨테이너에 추가되면
"GET" 이외 요청시 "CSRF 토큰"이 있어야 증명확인이 되어
end point 까지 갈 수 있다.
지금은 해제하여 사용하도록한다.
[4. PasswordEncoder 설정]
package com.zelkova.zelkova.config;
import java.util.Arrays;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Configuration
@RequiredArgsConstructor
@Log4j2
public class CustomSecurity {
... 생략
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
[5. 엔티티 셋팅]
Member.java
package com.zelkova.zelkova.domain;
import java.util.List;
import java.util.ArrayList;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "memberRoleList")
public class Member {
@Id
private String email;
private String pw;
private String nickname;
private boolean social;
// 값 타입 객체. 테이블로 따로 관리. LazyLoading 형식(접근 필요없으면 쿼리x)
@ElementCollection(fetch = FetchType.LAZY)
@Builder.Default
private List<MemberRole> memberRoleList = new ArrayList<>();
public void addRole(MemberRole memberRole) {
memberRoleList.add(memberRole);
}
public void clearRole() {
memberRoleList.clear();
}
public void changeNickname(String nickname) {
this.nickname = nickname;
}
public void changePw(String pw) {
this.pw = pw;
}
public void changeSocial(boolean social) {
this.social = social;
}
}
MemberRole.java
package com.zelkova.zelkova.domain;
public enum MemberRole {
USER, MANAGER, ADMIN;
}
MemberRepository.java
package com.zelkova.zelkova.repository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.zelkova.zelkova.domain.Member;
public interface MemberRepository {
@EntityGraph(attributePaths = { "memberRoleList" })
@Query("select m from Member m where m.email = :email")
Member getWithRoles(@Param("email") String email);
}
[6. MemberDTO 구현]
package com.zelkova.zelkova.dto;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class MemberDTO extends User {
private String email;
private String pw;
private String nickname;
private boolean social;
private List<String> roleNames = new ArrayList<>();
public MemberDTO(String email, String pw, String nickname, boolean social, List<String> roleNames) {
super(email, pw,
roleNames.stream().map(str -> new SimpleGrantedAuthority("ROLE_" + str)).collect(Collectors.toList()));
this.email = email;
this.pw = pw;
this.nickname = nickname;
this.social = social;
this.roleNames = roleNames;
}
public Map<String, Object> getClaims() {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("email", email);
dataMap.put("pw", pw);
dataMap.put("nickname", nickname);
dataMap.put("social", social);
dataMap.put("roleNames", roleNames);
return dataMap;
}
}
Security에서 회원을 다루는 객체인 'User'를 상속했기 때문에
부모 클래스 프로퍼티를 삽입해주는 super 메소드를 이용한다.
'User'객체는
username, password, authority 등이 프로퍼티로 존재한다.
authority는
GrantedAuthority 타입인데
해당 타입은 SimpleGrantedAuthority 클래스로
처리가 가능하다.
SimpleGrantedAuthority는
String을 인자로 받는 생성자가 있다.
role을 넣어주면 객체 생성할 수 있다.
SimpleGrantedAuthority는
User 객체가 받는
GrantedAuthority 인터페이스를 구현한 클래스이다.
getClaims는
JWT생성할 때 필요한 내용이다.
나중에 알게됨
[7. UserDetailsService 구현]
우선 Security Context가 로그인 처리시 확인하는 인터페이스인
UserDetailsService 를 보면
loadUserByUsername의 메소드에
username 문자열만 넣으면 로그인 처리를 해준다.
그런데 현재 로그인 처리를 위한
Security Context에서의 경로설정을 하지 않았기 때문에
먼저 경로설정을 해줘야한다.
[Security Context에서의 로그인 경로 설정]
public class CustomSecurity {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("-------------security config ----------");
http.cors(httpSecurityCorsConfigurer -> {
httpSecurityCorsConfigurer.configurationSource(configurationSource());
});
// 요청이 들어올 때 마다 세션 생성을 막음
http.sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// GET 이외 요청시 "CSRF 토큰"과 함께 요청이 와야함. 지금은 해제
http.csrf(config -> config.disable());
http.formLogin(config -> config.loginPage("/api/member/login"));
return http.build();
}
...생략
}
아래 코드만 추가하면 된다.
http.formLogin(config -> config.loginPage("/api/member/login"))
HttpSecurity 클래스 내부에 formLogin 메소드를 살펴보려니
겹겹이 쌓여있어서 분석 실패했다.
시간 있으면 살펴보면 좋을 듯 싶다.
Security Context에서의 로그인 경로설정이 완료되었기 때문에
Security Context에서 로그인을 처리하는 부분을
구현하여 커스텀 소스를 추가해보자
[UserDetailsService.java - Security Context에서의 로그인 처리기능 구현]
package com.zelkova.zelkova.security;
import java.util.stream.Collectors;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.zelkova.zelkova.domain.Member;
import com.zelkova.zelkova.dto.MemberDTO;
import com.zelkova.zelkova.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Service
@RequiredArgsConstructor
@Log4j2
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
// Security Config에서 설정한 .formLogin에서의 경로에 Post로 던지면 이곳에 도착한다. (흐름은 Security
// Context 구조특징임)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("---------------loadUserByUserName");
Member member = memberRepository.getWithRoles(username);
if (member == null) {
throw new UsernameNotFoundException("Not Found");
}
MemberDTO memberDTO = new MemberDTO(
member.getEmail(),
member.getPw(),
member.getNickname(),
member.isSocial(),
member.getMemberRoleList().stream().map(memberRole -> memberRole.name()).collect(Collectors.toList()));
log.info("memberDTO ::: " + memberDTO);
return memberDTO;
}
}
Security Context의
로그인 처리기능을 구현했다.
이제는 성공시, 실패시 로직을 구현해야한다
로그인(UserDetailsService.java)
-> 성공시) AuthenticationSuccessHandler.java
-> 실패시) AuthenticationFailureHandler.java
아래 두가지를 구현해보자
AuthenticationSuccessHandler.java
AuthenticationFailureHandler.java
[ AuthenticationSuccessHandler.java - 로그인 성공시 처리]
그 전에 먼저 JSON 문자열 생성 라이브러리 추가하자
implementation 'com.google.code.gson:gson:2.10.1'
[APILoginSuccessHandler.java]
package com.zelkova.zelkova.security.handler;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import com.google.gson.Gson;
import com.zelkova.zelkova.dto.MemberDTO;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class APILoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
log.info("---------------------------success handler start");
log.info(authentication);
log.info("---------------------------success handler finish");
MemberDTO memberDTO = (MemberDTO) authentication.getPrincipal();
Map<String, Object> claims = memberDTO.getClaims();
claims.put("accessToken", "");
claims.put("refreshToken", "");
Gson gson = new Gson();
String jsonStr = gson.toJson(claims);
response.setContentType("application/json; charset=UTF-8");
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonStr);
printWriter.close();
}
}
package com.zelkova.zelkova.security.handler;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import com.google.gson.Gson;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class APILoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
Gson gson = new Gson();
String jsonStr = gson.toJson(Map.of("error", "ERROR_LOGIN"));
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonStr);
printWriter.close();
}
}
Security Context의 설정값이 있는 SecurityConfig에서
로그인 후 해당 클래스로 이동시키기 위해
흐름을 잡아주는 설정을 해줘야한다.
http.formLogin(config -> {
config.loginPage("/api/member/login");
config.successHandler(new APILoginSuccessHandler());
config.failureHandler(new APILoginFailureHandler());
});
[ 8. JWT 생성]
JWT = 헤더 + 페이로드 + 서명
accessToken(10분), refreshToken(1시간) 두개를 발급
accessToken이 만료되면
갈아 끼우고
기존의 refreshToken은 1시간짜리로 재발급한다.
JWT 라이브러리 추가
https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
build.gradle에 추가
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
JWT 처리 기능 추가
예외처리를 위한 클래스
package com.zelkova.zelkova.util;
public class CustomJWTException extends RuntimeException {
public CustomJWTException(String msg) {
super(msg);
}
}
JWT생성 및 검증 클래스
package com.zelkova.zelkova.util;
import java.io.UnsupportedEncodingException;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Map;
import javax.crypto.SecretKey;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.InvalidClaimException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.WeakKeyException;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class JWTUtil {
private static String key = "1234567890123456789012345678901234567890";
public static String generateToken(Map<String, Object> valueMap, int min) {
SecretKey key = null;
try {
key = Keys.hmacShaKeyFor(JWTUtil.key.getBytes("UTF-8"));
} catch (WeakKeyException | UnsupportedEncodingException e) {
e.printStackTrace();
}
String jwtStr = Jwts.builder()
.setHeader(Map.of("typ", "JWT"))
.setClaims(valueMap)
.setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
.setExpiration(Date.from(ZonedDateTime.now().plusMinutes(min).toInstant()))
.signWith(key)
.compact();
return jwtStr;
}
public static Map<String, Object> validateToken(String token) {
Map<String, Object> claim = null;
SecretKey key;
try {
key = Keys.hmacShaKeyFor(JWTUtil.key.getBytes("UTF-8"));
claim = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (MalformedJwtException malformedJwtException) {
throw new CustomJWTException("malformed");
} catch (ExpiredJwtException expiredJwtException) {
throw new CustomJWTException("expired");
} catch (InvalidClaimException invalidClaimException) {
throw new CustomJWTException("invalidClaimException");
} catch (JwtException jwtException) {
throw new CustomJWTException("jwtException");
} catch (Exception e) {
throw new CustomJWTException("Error");
}
return claim;
}
}
APILoginSuccessHandler.java에서
token 추가해주자
package com.zelkova.zelkova.security.handler;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import com.google.gson.Gson;
import com.zelkova.zelkova.dto.MemberDTO;
import com.zelkova.zelkova.util.JWTUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class APILoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
log.info("---------------------------success handler start");
log.info(authentication);
log.info("---------------------------success handler finish");
MemberDTO memberDTO = (MemberDTO) authentication.getPrincipal();
Map<String, Object> claims = memberDTO.getClaims();
// 여기 두 줄 추가됨
String accessToken = JWTUtil.generateToken(claims, 10);
String refreshToken = JWTUtil.generateToken(claims, 60 * 24);
claims.put("accessToken", accessToken);
claims.put("refreshToken", refreshToken);
Gson gson = new Gson();
String jsonStr = gson.toJson(claims);
response.setContentType("application/json; charset=UTF-8");
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonStr);
printWriter.close();
}
}
AccessToken 체크 포인트를 필터로 처리하기
SecurityConfig에서 필터 추가하기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("-------------security config ----------");
http.cors(httpSecurityCorsConfigurer -> {
httpSecurityCorsConfigurer.configurationSource(configurationSource());
});
// 요청이 들어올 때 마다 세션 생성을 막음
http.sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// GET 이외 요청시 "CSRF 토큰"과 함께 요청이 와야함. 지금은 해제
http.csrf(config -> config.disable());
http.formLogin(config -> {
config.loginPage("/api/member/login");
config.successHandler(new APILoginSuccessHandler());
config.failureHandler(new APILoginFailureHandler());
});
// 여기추가됨
http.addFilterBefore(new JWTCheckFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
JWTCheckFilter.java
package com.zelkova.zelkova.security.filter;
import java.io.IOException;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class JWTCheckFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
// 필터지정하지 않을 녀석들 선언
String path = request.getRequestURI();
log.info("check uri ............ " + path);
return false;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
filterChain.doFilter(request, response);
}
}
로그인, preflight, 로그인처리경로, 이미지 리소스 등은
token 체크하는 필터 적용 예외처리를 두자
JWTCheckFilter.java
package com.zelkova.zelkova.security.filter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import org.springframework.web.filter.OncePerRequestFilter;
import com.google.gson.Gson;
import com.zelkova.zelkova.util.JWTUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class JWTCheckFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
// 필터지정하지 않을 녀석들 선언
String path = request.getRequestURI();
if (request.getMethod().equals("OPTIONS")) {
return true;
}
log.info("check uri ............ " + path);
if (path.startsWith("/api/member/")) {
return true;
}
if (path.startsWith("/api/products/view/")) {
return true;
}
return false;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeaderStr = request.getHeader("Authorization");
try {
String accessToken = authHeaderStr.substring(7);
Map<String, Object> claims = JWTUtil.validateToken(accessToken);
log.info("JWT claims " + claims);
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("JWT ERROR");
Gson gson = new Gson();
String msg = gson.toJson(Map.of("ERROR", "ERROR_ACCESS_TOKEN"));
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
printWriter.println(msg);
printWriter.close();
}
}
}
[@PreAuthorize로 권한 여부에 따라 api 접근설정하기]
1. SecurityContext에게 나 "PreAuthorize" 쓸거야라고 말해주기
코드먼저 넣고 소스 분석하자
package com.zelkova.zelkova.config;
import java.util.Arrays;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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 org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.zelkova.zelkova.security.filter.JWTCheckFilter;
import com.zelkova.zelkova.security.handler.APILoginFailureHandler;
import com.zelkova.zelkova.security.handler.APILoginSuccessHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Configuration
@RequiredArgsConstructor
@Log4j2
@EnableMethodSecurity // @PreAuthorize를 위한 설정.(특정 메소드에 권한 확인 후 인가 부여)
public class CustomSecurity {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("-------------security config ----------");
http.cors(httpSecurityCorsConfigurer -> {
httpSecurityCorsConfigurer.configurationSource(configurationSource());
});
// 요청이 들어올 때 마다 세션 생성을 막음
http.sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// GET 이외 요청시 "CSRF 토큰"과 함께 요청이 와야함. 지금은 해제
http.csrf(config -> config.disable());
// 로그인 경로설정
// 성공,실패 핸들러 커스텀
http.formLogin(config -> {
config.loginPage("/api/member/login");
config.successHandler(new APILoginSuccessHandler());
config.failureHandler(new APILoginFailureHandler());
});
// JWT 체크할 경로 커스텀 설정
http.addFilterBefore(new JWTCheckFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@PreAuthorize를 사용하기 위해서는
SecurityContext에게 "나 PreAuthorize 사용할거야" 라고 알려줘야한다.
그 어노테이션은
@EnableMethodsSecurity 이며
그 속을 보면 아래와 같다.
2. Controller에 테스트로 붙여보기
@PreAuthorize("hasRole('ROLE_ADMIN')") // 권한설정
@GetMapping("/list")
public PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO) {
log.info("test");
return boardSerivce.list(pageRequestDTO);
}
3. SecurityConfig의 JWTFilter에서 인증 설정을 하기
코드먼저..
package com.zelkova.zelkova.security.filter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.List;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import com.google.gson.Gson;
import com.zelkova.zelkova.dto.MemberDTO;
import com.zelkova.zelkova.util.JWTUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class JWTCheckFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
// 필터지정하지 않을 녀석들 선언
String path = request.getRequestURI();
if (request.getMethod().equals("OPTIONS")) {
return true;
}
log.info("check uri ............ " + path);
if (path.startsWith("/api/member/")) {
return true;
}
// if (path.startsWith("/api/board/")) {
// return true;
// }
return false;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authHeaderStr = request.getHeader("Authorization");
try {
String accessToken = authHeaderStr.substring(7);
Map<String, Object> claims = JWTUtil.validateToken(accessToken);
log.info("JWT claims " + claims);
String email = (String) claims.get("email");
String pw = (String) claims.get("pw");
String nickname = (String) claims.get("nickname");
Boolean social = (Boolean) claims.get("social");
List<String> roleNames = (List<String>) claims.get("roleNames");
MemberDTO memberDTO = new MemberDTO(email, pw, nickname, social.booleanValue(), roleNames);
log.info("------------------------");
log.info(memberDTO);
log.info(memberDTO.getAuthorities());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memberDTO,
memberDTO.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
;
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("JWT ERROR");
Gson gson = new Gson();
String msg = gson.toJson(Map.of("ERROR", "ERROR_ACCESS_TOKEN"));
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
printWriter.println(msg);
printWriter.close();
}
}
}
Security Context가 Framework에 들어오면
서버에 접근시 SecurityContext에서 먼저 로그인 인증을 확인한다.
그 중에서
우리가 추가한 필터를 살펴보자.
서버 접근시 거의 최초로 만나는 필터다.
JWT증명에 대한 필터인데 그걸 커스텀으로 구현한
JWTCheckFilter를 보자
JWT 를 체크하는 부분이다.
멤버정보의 인증권한을 체크하여 SecurityContext에 집어넣는 과정을 보자
가. 토큰에서 Member 정보를 추출하여
나. 멤버정보와 인증권한들을 토큰화 시킨다.
다. 인증권한 토큰을 SecurityContext의 Authentication에 집어 넣는다.
포스트맨으로 테스트해보기
@hasRole 로 "ROLE_ADMIN" 만 설정했으니
"ROLE_USER"인 계정으로 테스트 해보자
1st. /api/member/login 으로 accessToken 받기
2nd. "ROLE_USER" 계정인 토큰 넣어서 /api/board/list 조회해보기
접근제한이 걸린 것을 확인할 수 있다.
[9. Refresh 토큰]
package com.zelkova.zelkova.controller;
import java.util.Map;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zelkova.zelkova.util.CustomJWTException;
import com.zelkova.zelkova.util.JWTUtil;
import lombok.extern.log4j.Log4j2;
@RestController
@Log4j2
public class APIRefreshController {
@RequestMapping("/api/member/refresh")
public Map<String, Object> refresh(@RequestHeader("Authorization") String authHeader, String refreshToken) {
if (refreshToken == null) {
throw new CustomJWTException("NULL_REFRESH");
}
if (authHeader == null || authHeader.length() < 7) {
throw new CustomJWTException("INVALID_STRING");
}
String accessToken = authHeader.substring(7);
// ACCESS TOKEN 살아있으면 그대로 리턴
if (checkExpiredToken(accessToken) == false) {
return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
}
// refresh check
Map<String, Object> claims = JWTUtil.validateToken(refreshToken);
String newAccessToken = JWTUtil.generateToken(claims, 10);
// refreshToken 만료가 1시간도 안남았으면 새로발급
String newRefreshToken = checkTime((Integer) claims.get("exp")) == true ? JWTUtil.generateToken(claims, 60 * 24)
: refreshToken;
return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
}
private boolean checkTime(Integer exp) {
java.util.Date expDate = new java.util.Date((long) exp * (1000));
long gap = expDate.getTime() - System.currentTimeMillis();
long leftMin = gap / (1000 * 60);
return leftMin < 60;
}
private boolean checkExpiredToken(String token) {
try {
JWTUtil.validateToken(token);
} catch (CustomJWTException e) {
if (e.getMessage().equals("Expired")) {
return true;
}
}
return false;
}
}
만료시)
accessToken 재발급
-> refreshToken 만료유무 체크 후 조건에 따라 재발급
'서버' 카테고리의 다른 글
synology nas - ubuntu 이미지에서 git, java 17, gradle 설치 후 spring boot 서버 띄우기(1) (5) | 2024.09.28 |
---|---|
배포시 자주 사용하는 Gradle 명령어 (0) | 2024.09.23 |
Spring Boot 3 - 이미지 업로드시 에러 "charset=UTF-8' is not supported" (0) | 2024.09.18 |
SpringBoot 3 - 파일업로드 api (1) | 2024.09.17 |
Java - 반복해서 문구 생성해야 할 때 java로 프로그램 만들기 (0) | 2024.08.28 |