일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- 자동잔디
- 엘라스틱서치로 로그관리
- 자바 패치노트
- 자동커밋
- 프론트 면접족보
- 엘라스틱서치
- 프론트 면접
- java 면접정리
- 자바 패치
- 프론트엔드 면접
- githubaction
- 백엔드 신입
- 잔디심기
- 백엔드 면접
- java 신입면접
- next로 jwt
- 리액트 버전
- 리액트 #무한스크롤
- 자바17
- 키바나
- 도커컴포즈
- 로그스태쉬
- java 신입
- 깃허브액션
- 파일비트
- 프론트엔드 신입
- 리액트 패치노트
- nextjs와 typescript
- NextJs
- 리액트 패치
- Today
- Total
천쓰의 개발동산
Spring의 JsonWebToken(JWT)로 안전하게 회원관리하기!! 본문
종합 프로젝트때 보안처리를 위해 JWT 토큰을 사용하였다 .
jwt 토큰이란 access토큰, refresh토큰 이두개를 이용하여 로그인시 발급받아 회원에 필요한기능을 사용할때
검증을 받아 사용하게됩니다 .
이래에는 GPT가 말해주는 특징 입니다
JWT는 다음과 같은 특징을 가지고 있습니다:
- 자가수용적(Self-contained): 토큰에 필요한 모든 정보가 포함되어 있어 별도의 저장 공간이나 데이터베이스에 의존하지 않습니다.
- JSON 기반: 토큰의 정보 표현은 JSON 객체로 이루어져 있어 가독성이 좋고 다양한 데이터 유형을 지원합니다.
- 디지털 서명 또는 암호화: 필요에 따라 토큰은 디지털 서명 또는 암호화를 통해 보호될 수 있습니다. 이를 통해 토큰이 변조되지 않았음을 검증할 수 있습니다.
이로서 쉽게말해서는 인증 및 정보 교환을 위한 토큰 기반의 권한 부여 메커니즘? 이라보면될꺼같습니다
토큰을 만들기에앞서 필요한 함수가있습니다 .
토큰을 생성,검증,회원정보추출 해주는 TokenProvider
토큰을 인증처리를 해주는 JwtFilter
토큰의 보안 처리를해주는 WebSecurityConfig
권한이없는경우 에러를 처리해주는 Config와 DeniedHandler
그리고 토큰으로 회원정보를 가져오는 SecurityUtil이 있습니다 .
이렇게 준비가되면 Token을 사용할 준비는 끝났습니다 .
TokenProvider
@Slf4j
@Component
public class TokenProvider {
private static final String AUTHORITIES_KEY = "auth"; // 토큰에 저장되는 권한 정보의 key
private static final String BEARER_TYPE = "Bearer"; // 토큰의 타입
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 1; // 30분
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7L; // 1일
private final Key key; // 토큰을 서명하기 위한 Key
// 주의점 : @Value 어노테이션은 springframework의 어노테이션이다.
public TokenProvider(@Value("${jwt.secret}") String secretKey) {
this.key = Keys.secretKeyFor(SignatureAlgorithm.HS512); // HS512 알고리즘을 사용하는 키 생성
}
// 토큰 생성
public TokenDto generateTokenDto(Authentication authentication) {
// 권한 정보 문자열 생성,
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new java.util.Date()).getTime(); // 현재 시간
// 토큰 만료 시간 설정
Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME);
// 토큰 생성
String accessToken = io.jsonwebtoken.Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
// 리프레시 토큰 생성
String refreshToken = io.jsonwebtoken.Jwts.builder()
.setExpiration(refreshTokenExpiresIn)
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
// 토큰 정보를 담은 TokenDto 객체 생성
return TokenDto.builder()
.grantType(BEARER_TYPE)
.accessToken(accessToken)
.accessTokenExpiresIn(accessTokenExpiresIn.getTime())
.refreshToken(refreshToken)
.refreshTokenExpiresIn(refreshTokenExpiresIn.getTime())
.build();
}
// 토큰에서 회원 정보 추출
public Authentication getAuthentication(String accessToken) {
// 토큰 복호화
Claims claims = parseClaims(accessToken);
// 토큰 복호화에 실패하면
if (claims.get(AUTHORITIES_KEY) == null) {
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
}
// 토큰에 담긴 권한 정보들을 가져옴
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 권한 정보들을 이용해 유저 객체를 만들어서 반환, 여기서 User 객체는 UserDetails 인터페이스를 구현한 객체
User principal = new User(claims.getSubject(), "", authorities);
// 유저 객체, 토큰, 권한 정보들을 이용해 인증 객체를 생성해서 반환
return new UsernamePasswordAuthenticationToken(principal, accessToken, authorities);
}
// 토큰의 유효성 검증
public boolean validateToken(String token) {
try {
io.jsonwebtoken.Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | io.jsonwebtoken.MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
// 토큰 복호화
private Claims parseClaims(String accessToken) {
try {
return io.jsonwebtoken.Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
// access 토큰 재발급
public String generateAccessToken(Authentication authentication) {
return generateTokenDto(authentication).getAccessToken();
}
}
저는 access 권한시간을 refesh 토큰을 확인하느랴 1분으로 수정해서 사용했습니다 .
JwtFilter
@Slf4j
@RequiredArgsConstructor // final이 붙은 필드를 인자값으로 하는 생성자를 만들어줌
public class JwtFilter extends OncePerRequestFilter {
public static final String AUTHORIZATION_HEADER = "Authorization"; // 토큰을 요청 헤더의 Authorization 키에 담아서 전달
public static final String BEARER_PREFIX = "Bearer "; // 토큰 앞에 붙는 문자열
private final TokenProvider tokenProvider; // 토큰 생성, 토큰 검증을 수행하는 TokenProvider
private String resolveToken(HttpServletRequest request) { // 토큰을 요청 헤더에서 꺼내오는 메서드
String bearerToken = request.getHeader(AUTHORIZATION_HEADER); // 헤더에서 토큰 꺼내오기
if (bearerToken != null && bearerToken.startsWith(BEARER_PREFIX)) { // 토큰이 존재하고, 토큰 앞에 붙는 문자열이 존재하면
return bearerToken.substring(7); // 토큰 앞에 붙는 문자열을 제거하고 토큰 반환
}
return null;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = resolveToken(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Authentication authentication = tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
보기 쉽게 gpt로 설명 주석 달아놨습니다 ^^
WebSecurityConfig
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@Component
public class WebSecurityConfig implements WebMvcConfigurer {
private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; // 인증 실패 시 처리할 클래스
private final JwtAccessDeniedHandler jwtAccessDeniedHandler; // 인가 실패 시 처리할 클래스
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt 암호화 객체를 Bean으로 등록
}
@Bean // SecurityFilterChain 객체를 Bean으로 등록
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.authorizeRequests()
.antMatchers("/auth/**", "/admin/**","/api/**","/feed/**","/post/**","/","/static/**","/favicon.ico","/manifest.json").permitAll() //포스트맨 권한
.antMatchers("/swagger-ui.html", "/v2/api-docs", "/swagger-resources/**", "/webjars/**").permitAll() //스웨거 권한
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtSecurityConfig(tokenProvider));
return http.build();
}
}
저는 스웨거를 쓰기떄문에 스웨거 부문 추가로 만들어줫고 antMatchers쪽에 작업을할땐 ("/*") 로 Controll쪽에 모두 허용시켜 사용하였습니다 (이렇게하면 추후에 작업해줘야할게생김 편의상 ,,)
그리고 권한이 없는경우 에러처리는 생략하도록하고
SecurityUtil
@Slf4j
@Component
public class SecurityUtil {
private SecurityUtil() {
}
// Security Context의 Authentication 객체를 이용해 회원의 정보를 가져온다.
public static Long getCurrentMemberId() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null) {
throw new RuntimeException("Security Context에 인증 정보가 없습니다.");
}
return Long.parseLong(authentication.getName());
}
}
해서 저장된 회원정보인 Long 타입의 id값을 가져옵니다 .
토큰안에 payload라는게 있는데 토큰을생성할때 그안에 회원정보를 저장하는 방식이라 그것을 가져와 유효한 토큰값만있으면 회원정보를 가져와 사용할수있다 .
이로서 토큰을 사용할 준비는 끝났습니다 .
이제 서비스와 컨트롤을 만들어 회원가입할때 비밀번호 인코딩하게 만들고 로그인할때 Spring Security 쪽에서 인코딩한 비밀번호와 유저가친 비밀번호를 확인하고 acess토큰 refresh토큰 을 출력하게해준다면 jwt의 토큰을 인증받아 사용만 하면됩니다 .!
AuthService
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class AuthService {
private final AuthenticationManagerBuilder managerBuilder; // 인증을 담당하는 클래스
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final TokenProvider tokenProvider;
// 회원가입
public MemberResDto signup(MemberReqDto requestDto) {
if (memberRepository.existsByMemberEmail(requestDto.getMemberEmail())) {
throw new RuntimeException("이미 가입되어 있는 유저입니다");
}
Member member = requestDto.toEntity(passwordEncoder);
return MemberResDto.of(memberRepository.save(member));
}
// 비밀번호변경
public boolean passwordChange(String email, String password) {
try {
Member member = memberRepository.findByMemberEmail(email)
.orElseThrow(() -> new RuntimeException("회원 정보가 없습니다."));
String hashedPassword = passwordEncoder.encode(password);
member.setMemberPassword(hashedPassword);
memberRepository.save(member);
return true; // 변경 성공 시 true 반환
} catch (Exception e) {
// 예외가 발생하면 변경 실패로 간주하고 false 반환
return false;
}
}
public TokenDto login(MemberReqDto requestDto) {
UsernamePasswordAuthenticationToken authenticationToken = requestDto.toAuthentication();
log.info("authenticationToken: {}", authenticationToken);
Authentication authentication = managerBuilder.getObject().authenticate(authenticationToken);
log.info("authentication: {}", authentication);
return tokenProvider.generateTokenDto(authentication);
}
public String findId(String name, String tel) {
Member member = memberRepository.findByMemberNameAndMemberTel(name, tel).orElseThrow(
() -> new RuntimeException("해당 회원이 존재하지 않습니다.")
);
return member.getMemberEmail();
}
// accessToken 재발급
public String createAccessToken(String refreshToken) {
Authentication authentication = tokenProvider.getAuthentication(refreshToken);
return tokenProvider.generateAccessToken(authentication);
}
}
여기서 재발급은 refresh토큰을 받아와 accessToken의 시간 만료일떄 재발급해주는 매서드입니다.
AuthController
@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
private final MemberService memberService;
private final EmailService emailService;
//회원가입
@PostMapping("/signup")
public ResponseEntity<MemberResDto> signup(@RequestBody MemberReqDto requestDto) {
return ResponseEntity.ok(authService.signup(requestDto));
}
//로그인
@PostMapping("/login")
public ResponseEntity<TokenDto> login(@RequestBody MemberReqDto requestDto) {
return ResponseEntity.ok(authService.login(requestDto));
}
// 회원 존재 여부 확인
@GetMapping("/exists/{email}")
public ResponseEntity<Boolean> memberExists(@PathVariable String email) {
log.info("email: {}", email);
boolean isTrue = memberService.isMember(email);
return ResponseEntity.ok(!isTrue);
}
이제 포스트맨으로 확인 해볼까요?
회원가입
로그인
이렇게 회원가입후 , 로그인을해보면 정상적으로 토큰 발급되는게 확인이됬습니다 .
이제 사용하는방법도 알려드리자면
필요한 컨트롤에 저 acessToken 을 가져와 헤더부분에 Bearer ${acessToken} 값으로 진행해주면 정상 인증처리가되면서 작동이되는것을 볼수있었습니다 .
필자는 이제 이렇게 발급된 토큰을 리액트에 사용해보았는데 이것을 LocalStorage에 담아 사용했습니다.
다음에는 jwt를 리엑트에서 axios 로받아와 사용하는 방법을 소개해드릴까합니다 .
'JAVA > SPRINGBOOT' 카테고리의 다른 글
Apache POI 아파치포이로 엑셀 DB업로드, DB 엑셀저장 (1) | 2024.07.23 |
---|---|
SRPING BOOT - 파일 DB 저장 하여 사용하는 방법 (0) | 2024.07.09 |
10月-JAVA(JDBC) 와 REACT를 활용한 미니프로젝트 -(백엔드편) (0) | 2024.01.11 |
9月 - JDBC 을 활용한 DB관리 미니프로젝트 연습 (0) | 2024.01.11 |