Users Microsservice - Login
기능 | URI(API Gateway 사용시) | URI(API Gateway 미사용시) | HTTP Method |
사용자 로그인 | /user-service/login | /login | POST |
요청을 서버에 보내면 -> header에 토큰과 userId 반환
AuthenticationFilter.java - Spring Security를 이용한 로그인 요청 발생시 작업을 처리해주는 Custom Filter 클래스
attemptAuthentication() : 사용자로부터 전달받은 데이터를 추출해서 토큰에 저장하고 인증처리
successfulAuthentication() : 인증이 되었을때 어떻게 처리해줄지
package com.example.userservice.security;
import com.example.userservice.vo.RequestLogin;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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;
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
// 사용자의 요청을 토큰으로 바꿔서 manager에게 전달하면 비교해서 인증
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
new ArrayList<>()
)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 인증 성공시 처리
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// super.successfulAuthentication(request, response, chain, authResult);
}
}
WebSecurity.java - 사용자 요청에 대해 AuthenticationFilter를 거치도록 수정
package com.example.userservice.security;
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 javax.servlet.Filter;
@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
// 여러 configure 중 권한 관련
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// http.authorizeRequests().antMatchers("/users/**").permitAll();
http.authorizeRequests().antMatchers("/**")
.hasIpAddress("") // ip 변경 필요
.and()
.addFilter(getAuthenticationFilter());
// h2에선 데이터가 프레임 별로 나눠져 있는데 그걸 무시해야 h2-console 사용가능
http.headers().frameOptions().disable();
}
private AuthenticationFilter getAuthenticationFilter() throws Exception {
AuthenticationFilter authenticationFilter = new AuthenticationFilter();
authenticationFilter.setAuthenticationManager(authenticationManager());
/**
* AuthenticationManager는 ProvierManager를 구현한 클래스로써,
* 인자로 전달받은 유저에 대한 인증 정보를 담고 있으며, 해당 인증 정보가 유효할 경우
* UserDetailsService에서 적절한 Principal을 가지고 있는
* Authentication 객체를 반환해 주는 역할을 하는 인증 공급자(Provider) 입니다.
*/
return authenticationFilter;
}
}
UserDetailsService 등록 - 기존의 UserService.java를 활용
public interface UserService extends UserDetailsService {
}
package com.example.userservice.service;
import com.example.userservice.dto.UserDto;
import com.example.userservice.jpa.UserEntity;
import com.example.userservice.jpa.UserRepository;
import com.example.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;
@Autowired
public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDto createUser(UserDto userDto) {
userDto.setUserId(UUID.randomUUID().toString());
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); // 속성이 모두 맞아 떨어지지 않으면 안되게 세팅
UserEntity userEntity = mapper.map(userDto, UserEntity.class);
userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd()));
userRepository.save(userEntity);
UserDto returnUserDto = mapper.map(userEntity, UserDto.class);
return returnUserDto;
}
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
if (userEntity == null) {
throw new UsernameNotFoundException("User not found");
}
UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
List<ResponseOrder> orders = new ArrayList<>();
userDto.setOrders(orders);
return null;
}
@Override
public Iterable<UserEntity> getUserByAll() {
return userRepository.findAll();
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByEmail(username);
if (userEntity == null) {
throw new UsernameNotFoundException(username); // 사용자가 검색되지 않았을때 security에서 예외 제공
}
// security 에 포함되어 있는 user
// 이메일, 비밀번호 다 검증이 완료 되면 사용자 정보반환하는 클래스
// 추가 옵션
// 마지막 ArrayList 로그인 되었을때 그 다음하는 작업 중 권한을 추가할 수 있음. 여기선 일단 없음
return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
true, true, true, true,
new ArrayList<>());
}
}
API Gateway Service 수정 - User Service 에 대한 Routes 정보 수정
filters - RewritePath = /user-serivce/(?<segment>.*), /$\{segment} -> 사용자가 전달한 경로를 수정해서 마이크로 서비스에 전달
application.yml 수정
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/login
- Method=POST
filters:
- RemoveRequestHeader=Cookie # POST 로 요청된 데이터는 매번 새로운 요청으로 인식하기 위해 header 초기화
- RewritePath=/user-service/(?<segment>.*), /$\{segment} # 사용자가 전달한 경로를 수정해서 마이크로 서비스에 전달
- 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}
API Gateway에서 변경된 uri로 마이크로서비스에게 전달하기 때문에 RequestMapping 수정 필요
Routes 테스트
user-service를 디버깅으로 실행해서 확인
login은 만들지 않았지만 Spring Security를 사용하면 기본적으로 제공
로그인 처리 과정
사용자 요청이 AuthenticationFilter 로 전달되서 attemptAuthentication메서드가 처리
-> UsernamePasswordAuthenticationToken으로 바꿔서 처리
-> UserDetailService를 구현하고 있는 클래스에서 loadUserByUsername() 메서드 실행 (DB 에서 가져온 사용자 정보를 User 객체로 변환해서 사용)
-> 정상적으로 인증되었으면 successfulAuthentication 에서 JWT 토큰을 발행 (userId로 토큰을 만들기 위해 email을 가지고 userId를 가지고 옴)
-> 토큰을 responseHeader 에 저장해서 사용자에게 반환
로그인 성공 처리
AuthenticationFilter.java의 successfulAuthentication() 수정 - 인증 성공 시 사용자에게 Token 발행
private UserService userService;
private Environment env;
public AuthenticationFilter(AuthenticationManager authenticationManager, UserService userService, Environment env) {
super.setAuthenticationManager(authenticationManager);
this.userService = userService;
this.env = env;
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String userName = ((User) authResult.getPrincipal()).getUsername();
UserDto userDetails = userService.getUserDetailsByEmail(userName);
}
WebSecurity
private AuthenticationFilter getAuthenticationFilter() throws Exception {
AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(), userService, env);
// 생성자를 통해 AuthenticationManager 전달했기 때문에 아래 코드 필요 없음
// authenticationFilter.setAuthenticationManager(authenticationManager());
/**
* AuthenticationManager는 ProvierManager를 구현한 클래스로써,
* 인자로 전달받은 유저에 대한 인증 정보를 담고 있으며, 해당 인증 정보가 유효할 경우
* UserDetailsService에서 적절한 Principal을 가지고 있는
* Authentication 객체를 반환해 주는 역할을 하는 인증 공급자(Provider) 입니다.
*/
return authenticationFilter;
}
WebSecurity.java의 configure 수정 - Userservice 사용할 수 있도록 메소드 수정
Userservice, UserserviceImpl, UesrRepository - 사용자 검색 메서드 추가
UserServiceImpl
@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;
}
JWT(Json Web Token)
JWT 생성
pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
application.yml
token:
expiration_time: 86400000 # 토큰 지속 시간 (하루 24 * 60 * 60 * 1000)
secret: user_token # 임의의 값
AuthenticationFilter.java의 successfulAuthentication() 수정 - 인증 성공 시 사용자에게 Token 발행
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String userName = ((User) authResult.getPrincipal()).getUsername();
UserDto userDetails = userService.getUserDetailsByEmail(userName);
String token = Jwts.builder()
.setSubject(userDetails.getUserId()) // 토큰에 들어갈 내용
.setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) // 토큰 시간
.signWith(SignatureAlgorithm.ES512, env.getProperty("token.secret")) // 암호화
.compact();
response.addHeader("token", token);
response.addHeader("userId", userDetails.getUserId());
}
JWT 처리 과정
전통적인 인증 시스템
username, password 전달해서 인증 요청
-> 서버는 DB 확인해서 session 반환
-> cookie 를 이용해서 서비스 사용
-> 서버는 seesion 정보 확인해서 서비스 결과 반환
문제점
- 세션과 쿠키는 모바일 어플리케이션에서 유효하게 사용할 수 없다. (공유 불가) 예> React에서도 안됨
- 렌더링된 HTML 페이지가 반환되지만, 모바일 어플링케이션에서는 JSON(or XML) 과 같은 포맷 필요
Token 기반 인증 시스템
username, password 전달해서 인증 요청
-> 서버는 session을 발급하는 것이 아닌 Token을 발금
-> 발급받은 Token(Bearer Token)을 전달해서 서비스 사용
-> 서버는 Token 검증해서 서비스 결과 반환
JWT 지원 : NodeJS(Vue, React), PHP, java, Ruby, .NET, Python..
장점
- 클라이언트 독립적인 서비스(stateless)
- CDN
- No Cookie-Session(No CSRF, 사이트간 요청 위조)
- 지속적인 토큰 저장
분산된 서버에서 공유 가능 (DB에 발급된 토큰을 저장해서 여러 서버에서 공유 가능)
API Gateway service - AuthorizationHeaderFilter
Spring security와 JWT token 사용 추가
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- java.lang.NoClassDefFoundError: Could not initialize class javax.xml.bind.DatatypeConverterImpl-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
AuthoriztionHeaderFilter.java 추가
package com.example.apigatewayservice.filter;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.HttpHeaders;
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;
@Autowired
public AuthorizationHeaderFilter(Environment env) {
super(Config.class);
this.env = env;
}
public static class Config {
}
// login -> token -> users (with token) -> header (include token)
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 헤더에 AUTHORIZATION 있는지 확인
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "no authorization header", HttpStatus.UNAUTHORIZED);
}
// 헤더에서 Bearer 토큰 추출
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
String jwt = authorizationHeader.replace("Bearer", "");
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 {
// 토큰에서 넣었던 sub 추출
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
} catch (Exception ex) {
returnValue = false;
}
if (subject == null || subject.isEmpty()) {
returnValue = false;
}
return returnValue;
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
}
application.yml
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*),/$\{segment}
- AuthorizationHeaderFilter
token:
secret: user_token # 임의의 값
Postman Authorization 추가
Bearer Authentication :
API 에 접속하기 위해서는 access token 을 API 서버에 제출해서 인증 처리
OAuth를 위해서 고안된 방법, RFC 6750
'Spring > [인프런] Spring Cloud' 카테고리의 다른 글
Spring Cloud Bus (0) | 2022.06.24 |
---|---|
Configuration Service (0) | 2022.06.24 |
Catalogs, Orders Microservice (0) | 2022.06.19 |
Users Microservice (0) | 2022.06.18 |
E-commerce 어플리케이션 (0) | 2022.06.18 |