작은 지식주머니
개인 토이 프로젝트 (3) JPA, JWT를 이용한 간단한 RESTful 백엔드 API만들어보기 본문
따라하지말고 보기만하세요 별로 좋지 않은 코드덩어리입니다.
개인 공부용 내용입니다!
저번 글에서 회원가입 API까지 만들고 그만둔것 같은데 이번에는 로그인 API를 만들려고 한다.
로그인을 위해서 나는 JWT토큰을 사용하기로 하였다 이유는 간단하다.
전자 서명된 토큰을 이용한다면 스케일 문제를 해결할 수 있기 떄문이다. 또한 사용법이 간단하기 때문에
사용하였다.
Json Web Token(JWT)는
header, payload, signature 로 이루어져있다.
header = [typ = type의 약자, alg = 사용된 알고리즘의 종류]
payload = [sub = 토큰의 주인, iss = 토큰을 발행한 주체, iat = 발행된 날짜,시간, exp = 토큰이 만료되는 시간]
signature = 토큰의 유효성 검사에 사용됨
내가 만들 내용을 간단하게 그림으로 표현해봤다.
자세한 내용은 JWT https://jwt.io/ 에서 확인하면 된다.
본론으로 들어가 jwt를 생성해야한다.
일단 의존성 주입부터 해야하는데
JWT와 Spring-boot-security를 주입하였다.
일단 SECURITY는 주석처리를 해 두는것이 좋다.
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2',
// Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
//'org.bouncycastle:bcprov-jdk15on:1.60',
'io.jsonwebtoken:jjwt-jackson:0.11.2' // or 'io.jsonwebtoken:jjwt-gson:0.11.2' for gson
implementation 'org.springframework.boot:spring-boot-starter-security'
그리고 Token을 만들어야 하기 떄문에 Security 패키지를 만들고 그 안에 TokenProvider라는 클래스를 생성했다.
package com.todo.todoP.Security;
import com.todo.todoP.Entity.Member;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Calendar;
import java.util.Date;
@Slf4j
@Service
public class TokenProvider {
private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 토큰 키
public String create(Member member){
Date date = new Date();
Calendar calendar = Calendar.getInstance();
int tokenExpire = 1800000; //30분
calendar.add(Calendar.MILLISECOND, tokenExpire);
Date expireDate = calendar.getTime();
return Jwts.builder()
.signWith(key)
.setSubject(member.getId().toString())
.setIssuer("todo app")
.setIssuedAt(date)
.setExpiration(expireDate)
.compact();
}
public String validateToken(String authToken){
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(authToken)
.getBody();
return claims.getSubject();
}
}
우선 create는 사용할 알고리즘 Key, 유효시간을 설정하고 Jwts.builder로 방금 적혀있던
header,payload를 넣고 compact()을 실행하면 토큰이 만들어진다.
validateToken은 해당 토큰을 검증하고 Subject부분을 가져오기 위해 만들었다.
우선 create가 제대로 작동하는지 확인하기 위해 Signin API를 만들도록 하겠다.
우선 우리는 이름과, 비밀번호로 검증을 해야하는데
비밀번호는 우선 암호화시켜 의미없는 값으로 변환하였다.
또한 검증에서는 matches를 이용해 Salting을이용해 비교하였고 성공했을 경우 entitiy값을 넘겨주게 된다.
public Member getByCredentials(String name, String password, PasswordEncoder encoder){
Member entity = memberRepository.findMemberByName(name);
if (entity != null && encoder.matches(password, entity.getPassword())){
return entity;
}
return null;
}
비밀번호 인코딩 방식은 BCryptPasswordEncoder()를 사용하였다.
굳이 비밀번호를 DTO에 담을 이유가 없다고 생각해서 담지 않았다.
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@PostMapping("/signin")
public ResponseEntity<?> authenticate(@Valid @RequestBody MemberSignInDTO memberDTO){
Member member = memberService.getByCredentials(memberDTO.getName(), memberDTO.getPassword(), passwordEncoder);
if (member != null){
String token = tokenProvider.create(member); //방금 만든 CREATE
MemberDTO responseMemberDTO = MemberDTO.builder()
.email(member.getEmail())
.id(member.getId())
.token(token)
.name(member.getName())
.build();
return ResponseEntity.ok().body(responseMemberDTO);
} else {
ResponseDTO<Object> responseDTO = ResponseDTO.builder()
.etc("Login Failed")
.build();
return ResponseEntity.badRequest().body(responseDTO);
}
}
POSTMAN으로 확인.
이제 토큰을 만들었으니 API가 실행될 떄마다 이 토큰을 가지고 인증을 해야한다.
인증 부분은 Spring Security를 활용하기로 했다.
서블릿 컨테이너에서 서블릿 필터를 구현하고 실행하도록 설정해야한다.
서블릿 필터 안에는 여러가지 필터가 있을수 있지만 나는 OncePerRequestFilter를 사용하기로 하였다.
기존 Filter와 OncePerRequestFilter의 차이는 이 글에서 상세히 적혀있어 도움을 받았다.
https://minkukjo.github.io/framework/2020/12/18/Spring-142/
OncePerRequestFilter와 Filter의 차이
OncePerRequestFilter와 Filter
minkukjo.github.io
이제 구현을 해아하는데 우선 security의존성 주입 주석을 풀고
Security 패키지에 JwtAuthenticationFilter라는 클래스를 작성했다.
package com.todo.todoP.Security;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider provider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String token = parseBearerToken(request); // 밑에있음
log.info("token = " + token);
if (token != null && !token.equalsIgnoreCase("null")){
Long userId = Long.parseLong(provider.validateToken(token));
AbstractAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken
(userId,null, AuthorityUtils.NO_AUTHORITIES);
authentication.setDetails
(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
}
} catch (Exception e){
logger.error("Could not set user authentication in security Context " + e);
}
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;
}
}
1. 코드를 설명하자면 헤더에서 Bearer 토큰을 가져오고
2. TokenProvider를 사용하여 토큰을 인증, UsernamePasswordAuthenticationToken을 작성
3. SecurityContext에 인증된 사용자를 등록.
해당 서블릿 필터를 구현하였으니
이제 스프링 시큐리티에 적용을 해야한다.
Config라는 패키지를 만들어 WebSecurityConfig 라는 클래스를 생성하고 WebSecurityConfigurerAdapter를 상속하였다.
package com.todo.todoP.Config;
import com.todo.todoP.Security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.filter.CorsFilter;
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter filter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf() //csrf사용하고 있지않음.
.disable()
.httpBasic() // token을 사용하므로 basic인증 disable
.disable()
.sessionManagement() // session 안씀
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // "/","/auth/** 경로는 인증 x
.antMatchers("/","/auth/**").permitAll()
.anyRequest() 나머지는 전부 인증
.authenticated();
http.addFilterAfter(
filter,
CorsFilter.class
); //CORS는 나중에 구현함.
}
}
스프링 설정까지 하고나서 POSTMAN으로 실행하니 문제없이 실행 되었다.
로그인 로직까지 완성하였으니 다음엔 조회, 업데이트 내용을 적도록 하겠다.
'spring' 카테고리의 다른 글
Mockito BDD mockito unnecessary stubbings detected 해결 (0) | 2022.07.18 |
---|---|
개인 토이 프로젝트 (2) JPA, JWT를 이용한 간단한 RESTful 백엔드 API만들어보기 (0) | 2022.02.20 |
개인 토이 프로젝트 (1) JPA, JWT를 이용한 간단한 RESTful 백엔드 API만들어보기 (0) | 2022.02.20 |
SOLID (0) | 2021.11.12 |