728x90
반응형
JWT Token
인증에 필요한 정보들을 Token에 담아서 암호화시켜 사용하는 인증 방식 입니다. 아래와 같이 Header, Payload, Signature로 구성되어 있습니다.
- Header에는 보통 토큰의 타입이나, 서명 생성에 사용되는 알고리즘에 대한 내용을 저장합니다.
- Payload에는 보통 Claim이라는 사용자, 토큰에 대한 property를 key-value의 형태로 저장합니다.
- Signature는 Header와 Payload에 대한 값을 디코딩하여 서버가 개인키를 가지고 암호화 한 상태를 보여줍니다.
JWT Token을 이용한 인증 시스템 구현
전체적인 흐름
- 사용자가 로그인 했을 때 토큰 발급
- 사용자는 토큰을 저장해놓고 API 요청 할 때 토큰을 Authorization 헤더에 넣어서 요청합니다.
- 서버는 Authorization 헤더에서 토큰을 꺼내서 검증을 진행합니다.
- 검증이 완료되면 사용자가 원하는 정보를 응답합니다.
구체적인 흐름
- HTTP 요청이 들어오면 요청 Header에 Authorization 값에서 토큰을 가져와서 검증을 진행합니다.
- 검증이 완료되었다면 AuthenticationProvider를 통해 Authentication객체를 생성해야합니다.
- AuthenticationProvider에서 MemberDetailService에 loadUserByUsername()을 호출해서 DB에 userId와 일치하는 사용자를 가져와서 userDetails 객체를 만들어 반환합니다.
- userDetails 객체를 통해서 UsernamePasswordAuthenticationToken()을 호출하여 Authentication 인증객체를 받아옵니다.
- 인증 객체 Authentication을 인증된 사용자 정보를 저장해놓는 SecurityContextHolder의 Context에 저장합니다.
- 마지막으로 chain.doFilter를 통해 계속 진행시키고 만약 인증이 실패했다면 403 Forbidden을 반환하고, 인증에 성공했다면 사용자가 요구하는 응답을 하게됩니다.
참고
저는 AuthenticationManager를 생략하고 진행했습니다. 이점 참고해주시면 감사하겠습니다.
코드
[WebSecurityConfig.class]
package hyjung.shop_management.config;
import hyjung.shop_management.jwt.JwtAuthenticationFilter;
import hyjung.shop_management.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
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.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
.authorizeRequests().mvcMatchers("/login", "/api/user").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
//JWTAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 등록을 해줍니다.
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
.authorizeRequests().mvcMatchers("/login", "/api/user").permitAll()
.anyRequest().authenticated()
위의 코드를 사용하여 로그인과, 회원가입을 할 때는 인증을 진행하지 않고, 그 외의 경우에만 인증을 진행하도록 하였습니다.
[JwtAuthenticationFilter.class]
package hyjung.shop_management.jwt;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// HTTP request에서 Authorization헤더에서 토큰을 가져오는 메서드입니다.
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
// 토큰 검증을 진행합니다. 토큰이 있는지와 토큰이 만료되지 않았는지 확인합니다.
if(token!=null && jwtTokenProvider.validateTokenExpiration(token)){
// 토큰을 통해 Authentication 객체를 얻어오는 메서드입니다.
Authentication auth = jwtTokenProvider.getAuthentication(token);
// Authentication 객체를 인증된 사용자 정보를 저장하는 곳에 저장
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
[application.propertiest]
spring.jwt.secretKey=#ShOp#MaNaGeMeNt#SeCrEt#KeY#
[JwtTokenProvider.class]
package hyjung.shop_management.jwt;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
// 위에 application.properties에서 설정한 값 가져왔습니다.
@Value("${spring.jwt.secretKey}")
private String secretKey;
// 토큰 만료 시간 설정합니다.
private Long ExpiredTime = 1000L * 60 * 15;
// Refresh 토큰 만료 시간 설정합니다.
private Long RefreshTime = 1000L * 60 * 60 * 24 * 7;
// 서명 암호화 알고리즘 설정합니다.
private SignatureAlgorithm algorithm = SignatureAlgorithm.HS256;
//사용자의 정보를 받아오는 서비스 클래스
private final UserDetailsService memberDetailService;
// 토큰 생성합니다.
public String createToken(String userId){
Claims claims = Jwts.claims().setSubject(userId);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime()+ExpiredTime))
.signWith(algorithm, secretKey)
.compact();
}
// 리프레시 토큰 생성합니다.
public String createRefreshToken(){
Date now = new Date();
return Jwts.builder()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime()+RefreshTime))
.signWith(algorithm, secretKey)
.compact();
}
// Authentication 인증 객체 생성하여 반환합니다.
public Authentication getAuthentication(String token){
// 사용자의 정보를 반환합니다.
UserDetails userDetails = memberDetailService.loadUserByUsername(getUserId(token));
// 사용자 정보인 userDetails를 통해 Authentication 객체를 반환합니다.
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰을 통해서 사용자의 아이디를 받습니다.
public String getUserId(String token){
try{
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
} catch (ExpiredJwtException e){
return e.getClaims().getSubject();
}
}
// 토큰이 만료되었는지 확인합니다.
public boolean validateTokenExpiration(String token){
try{
// 토큰의 Claims를 받아오는 코드를 실행하고 만약 토큰이 만료되었거나, 잘못된 토큰이라면 에러가 반환될 것이기 때문에
// 에러가 반환되지 않았다면 만료되지 않은 토큰으로 판단합니다.
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (Exception e){
return false;
}
}
public String resolveToken(HttpServletRequest request){
// Http Header에 Authorization 값을 가져옵니다.
// 원래 Bearer을 앞에 붙여주어야합니다.
// 이 코드에서만 Bearer을 제외시키고 코드를 구성하였습니다.
return request.getHeader("Authorization");
}
}
[MemeberDetail.class]
package hyjung.shop_management.jwt;
import lombok.Builder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class MemberDetail implements UserDetails {
private String userId;
private String userPw;
private List<GrantedAuthority> authorities;
@Builder
public MemberDetail(String userId, String userPw, List<GrantedAuthority> authorities) {
this.userId = userId;
this.userPw = userPw;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return userPw;
}
@Override
public String getUsername() {
return userId;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
[MemberDetailService.class]
package hyjung.shop_management.jwt;
import hyjung.shop_management.domain.Member;
import hyjung.shop_management.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class MemberDetailService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
// 데이터베이스에서 사용자를 찾아서 엔티티 반환합니다.
Member member = memberRepository.findByUserId(userId);
// 역할을 저장하기 위해서 리스트를 만들어줍니다.
List<GrantedAuthority> roles = new ArrayList<>();
// 역할에 member의 역할에 대한 내용을 넣어줍니다.
roles.add(new SimpleGrantedAuthority(member.getRole().toString()));
// MemberDetail을 생성하여 반환합니다.
return MemberDetail.builder()
.userId(member.getUserId())
.userPw(member.getUserPw())
.authorities(roles)
.build();
}
}
결론
그동안 개념만 이해하고 실제로 코드에 적용해보지 못했는데 이번 기회에
실제 코드에 적용해보면서 사용해봐서 실제 코드에서 적용하는 방법을 알게되었습니다.
실제 실무에서 사용할 수 있도록 세부적인 내용을 더 공부해서
JWT를 더 잘 사용할 수 있도록 실력을 키우겠습니다.
포스팅 봐주셔서 감사합니다.
전체 Code Git Link
728x90
반응형
'Spring' 카테고리의 다른 글
[Spring] CGLIB: Code Generator Library (0) | 2022.08.24 |
---|---|
[Spring] Server-Client 간 암, 복호화 구현 (0) | 2022.08.18 |
[Spring] Netty 서버 데이터 끊기는 문제 해결 (0) | 2022.08.16 |
[Spring] Netty 서버 구현과 문제 발생 (0) | 2022.08.16 |
[Spring] Netty 개념 (0) | 2022.08.11 |