이 글은 인프런 강의 "Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)" 기반으로 공부하여 정리한 글입니다. 문제/오류가 있을 시 댓글로 알려주시면 감사드리겠습니다
강의의 모든 부분을 정리하는 것이 아니고 비슷한 부분은 생략하고 정리하며, 강의의 내용 뿐만 아니라 개인적으로 공부한 내용도 포함되어 있습니다
JWT를 이용한 로그인을 구현한다
사용하는 이유
User Service
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
logging:
level:
com.zzarbttoo.userservice: DEBUG
#token 관련 작업
token:
expiration_time: 86400000 #하루
secret: user_token #필요한 임의의 값
... 생략
@Configuration //configuration은 우선순위가 높다
@EnableWebSecurity //webSecurity로 등록한다는 얘기
public class WebSecurity extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
...
//인증이 된 상태에서만 permit
http.authorizeRequests().antMatchers("/**")
.hasIpAddress("192.168.0.20") //특정 ip에 대해서만 permit
.and()
.addFilter(getAuthenticationFilter()); //filter을 통과해야만 permit
...
}
private AuthenticationFilter getAuthenticationFilter() throws Exception{
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(), userService, env);
return authenticationFilter;
}
//인증과 관련된 configure
//select pwd from user where email = ?
// db_pwd(encrypted) == input_pwd(encrypted)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder); //select는 userService
//변환처리는 이전에 등록해놓은 bCryptPasswordEncoder bean 이용
super.configure(auth);
}
}
...생략
org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
...
UserDto getUserDetailsByEmail(String userName);
}
package com.zzarbttoo.userservice.service;
import com.zzarbttoo.userservice.dto.UserDto;
import com.zzarbttoo.userservice.jpa.UserEntity;
import com.zzarbttoo.userservice.jpa.UserRepository;
import com.zzarbttoo.userservice.vo.ResponseOrder;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
public class UserServiceImpl implements UserService{
UserRepository userRepository;
BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//email로 찾는다
UserEntity userEntity = userRepository.findByEmail(userName);
//해당하는 사용자가 없다
if (userEntity == null){
throw new UsernameNotFoundException(userName); //spirng security에서 제공
}
//맨 마지막에는 권한을 넣어주면 된다
return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
true, true, true, true,
new ArrayList<>()); //검색-> password -> 반환
}
... 생략
@Override
public UserDto getUserDetailsByEmail(String email) {
UserEntity userEntity = userRepository.findByEmail(email);
if (userEntity == null) {
throw new UsernameNotFoundException(email);
}
UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
return userDto;
}
}
package com.zzarbttoo.userservice.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zzarbttoo.userservice.dto.UserDto;
import com.zzarbttoo.userservice.service.UserService;
import com.zzarbttoo.userservice.vo.RequestLogin;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
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.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
//생성하는 곳에서 직접 userService를 생성해서 쓰기 때문에 주입하지 않아도 된다
private UserService userService;
private Environment env; //토큰 만료 기간, 토큰 생성 알고리즘 등을 application.yml에 작성할 것이다
public AuthenticationFilter(AuthenticationManager authenticationManager,
UserService userService,
Environment env) {
super.setAuthenticationManager(authenticationManager); // super(authenticationManger);
this.userService = userService;
this.env = env;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
//post형태로 전달되는 것은 requestParameter로 받을 수 없기 때문에 inputstream으로 처리
RequestLogin credential = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
//인증 정보를 매니저에 넘긴다(id, password 비교)
return getAuthenticationManager().authenticate(
//인증정보로 만든다
new UsernamePasswordAuthenticationToken(credential.getEmail(),
credential.getPassword(),
new ArrayList<>()) //authentication 할 것들 목록
);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
//로그인 성공 시 정확하게 어떤 처리를 해줄 것인지(ex Token 생성/만료 시간 등, 사용자 반환값 등)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
//User객체는 login 성공 후 email과 encrypted 된 password, 권한들을 가진 객체
//log.debug(((User) authResult.getPrincipal()).getUsername()); //성공한 email 출력
String userName = ((User) authResult.getPrincipal()).getUsername();
UserDto userDetails = userService.getUserDetailsByEmail(userName);
//JWT token
String token = Jwts.builder()
.setSubject(userDetails.getUserId()) //userId로 토큰 생성
.setExpiration(new Date(System.currentTimeMillis() +
Long.parseLong(env.getProperty("token.expiration_time")))) //현재 시간 + 24시간
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret")) //알고리즘 + key 값
.compact();
response.addHeader("token", token);
response.addHeader("userId", userDetails.getUserId());
}
}
token이 정상적인 token인지 아닌지 gateway에서도 확인할 필요가 있다
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
at io.jsonwebtoken.impl.Base64Codec.decode(Base64Codec.java:26) ~[jjwt-0.9.1.jar:0.9.1]
at io.jsonwebtoken.impl.DefaultJwtParser.setSigningKey(DefaultJwtParser.java:151) ~[jjwt-0.9.1.jar:0.9.1]
... 생략
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/login
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment} #/user-service/~~~ 형태로 들어오면 /~~~만 보내주겠다
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/users
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- AuthorizationHeaderFilter #GET으로 요청할 때는 권한 필요
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/order-service/**
filters:
#user-service에 있는 값과 같아야한다
token:
secret: user_token
package com.example.apigatewayservice.filter;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
Environment env;
public AuthorizationHeaderFilter(Environment env) {
super(Config.class); //없으면 class cast exception 발생
this.env = env;
}
//login -> token -> users(with token) -> header(include token)
//Token의 유효성을 확인한 후 통과시킴
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
//인증정보가 포함되어있지 않다면 통과시키지 않는다
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "no authorization header", HttpStatus.UNAUTHORIZED);
}
//배열이기 때문에 0번째 data를 들고온다 -> authorization 에는 Bearer Token 값이 들어가게 된다
String authorizationHeader = request.getHeaders().get(org.springframework.http.HttpHeaders.AUTHORIZATION).get(0);
// Beraer token 형태로 오기 때문에 Bearer을 ""로 변환하고 순수 token 값을 얻어낸다
String jwt = authorizationHeader.replace("Bearer", "");
//JWT가 정상적인 값일 경우 통과
if (!isJwtValid(jwt)) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
return chain.filter(exchange);
});
}
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
//subject(sub)를 token으로부터 추출한다 -> 정상적인 계정 값인지 판별
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
//.parseClaimsJws(jwt).getBody().getSubject();
} catch (Exception ex){
returnValue = false;
}
if(subject == null || subject.isEmpty()){
returnValue = false;
}
return returnValue;
}
//(반환시켜주는 데이터 타입) Mono(단일), Flux(여러개) -> Spring WebFlux
private Mono<Void> onError(ServerWebExchange exchange, String error, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(error);
return response.setComplete();
}
public static class Config{
}
}