힙 정렬
•
완전 이진 트리의 일종으로 우선순위 큐를 위하여 만들어진 자료구조이다.
•
부모 노드의 키 값이 자식 노드의 키 값보다 항상 큰(작은) 이진 트리를 말한다.
•
배열을 넣었을 때 최대힙 트리 구조로 정렬하는 알고리즘
public static int[] down(int[] arr, int a){
int par = (a-1)/2; //인덱스가 0부터 시작
int chil = a;
if (arr[par] < arr[chil]){
int tmp = arr[chil];
arr[chil] = arr[par];
arr[par] = tmp;
if(a*2+1 < arr.length) arr = down(arr, a*2+1); //재귀를 이용하여 아래쪽도 교환을 해준다.
if(a*2+2 < arr.length) arr = down(arr, a*2+2); //자식 노드가 두개 다
}
return arr;
}
public static int[] heap(int[] arr){
for (int i = arr.length-1; i > 0; i--){ //i가 자식 인덱스가 되며 밑에서부터 교환을 한다.
arr = down(arr, i); //교환을 시작하면 아래로 쭉 들어가면서 교환을 한다.
}
return arr;
}
Java
복사
•
삽입, 삭제연산 필요
인가받은 사람만 리뷰쓰기
과정
•
토큰을 헤더로 보내는 방법
◦
Bearer : 인증타입의 한 종류로 JWT 또는 OAuth 토큰을 사용하는 인증 방식
Authorization : Bearer [token]
//hooon, pwd
//eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6Imhvb29uIiwiaWF0IjoxNjcwMjE1MDY3LCJleHAiOjE2NzAyMTg2Njd9.HhMb7YPJEaRs_g8Nw7XNaoUnWLEL_dlSEl3C2KQz9L8
Java
복사
리뷰 api 만들기
1.
ReviewController
@RestController
@RequestMapping("api/v1/reviews")
public class ReviewController {
private final ReviewService reviewService;
public ReviewController(ReviewService reviewService) {
this.reviewService = reviewService;
}
@PostMapping
public String write(@RequestBody ReviewRequest reviewRequest){
return reviewService.write(reviewRequest);
}
}
Java
복사
2.
ReviewService 만들기
@Service
public class ReviewService {
public String write(ReviewRequest reviewRequest){
return "리뷰등록이 되었습니다.";
}
}
Java
복사
3.
실행해보기
리뷰등록이 된다.
security filter로 막기
1.
필터 configuration 추가
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
// .antMatchers("/api/**").permitAll()
.antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll() // join, login은 언제나 가능
//다른 api 들어오기 위해서는 인증가 필요하다
.antMatchers(HttpMethod.POST, "/api/v1/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt사용하는 경우 씀
.and()
// .addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
.build();
}
Java
복사
2.
실행
토큰을 전에 인증 했다면 통과 - token 필터 추가
1.
Security configure설정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserService userService;
@Value("${jwt.token.secret}")
private String secretKey;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
// .antMatchers("/api/**").permitAll()
.antMatchers("/api/v1/users", "/api/v1/users/login").permitAll() // join, login은 언제나 가능
.antMatchers(HttpMethod.POST, "/api/v1/**").authenticated() //다른 api에 접근하기 위해서는 인증이 필요하다
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt사용하는 경우 씀
.and()
.addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
.build();
}
}
Java
복사
2.
JwtTokenFilter 클래스 만들기
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter { //api 요청을 할 때 한 번만 인증을 거친다??
private final UserService userService;
private final String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain){
}
}
Java
복사
3.
doFilterInternal() 만들기
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//Token에서 Claim 꺼내기
//Valid한지 확인하기
//userName 넣기, 문 열어주기
//token 만들기
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("", null, List.of(new SimpleGrantedAuthority("U ser")));
//디테일 설정하기
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
Java
복사
4.
위와 같은 요청 보내보기
문을 열어줬기 때문에 성공한다.
유효한 토큰이 아니면 막기
헤더에서 토큰 가져오기
1.
header에서 Authorization을 가져온다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//Token에서 Claim 꺼내기
final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("authorizationHeader : {}", authorizationHeader); //로그에 찍히도록한다.
...
Java
복사
2.
요청을 보낸다.
3.
로그를 확인한다.
4.
헤더에 Authorization이 없거나 Bearer로 시작하지 않으면 filter(
)
if(authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")){ //header에 AUTHORIZATION이 없거나, Bearer로 시작하지 않으면 filter
filterChain.doFilter(request, response);
return;
}
Java
복사
4.
받은 header 파싱해서 토큰 분리하기 (요청은 2와 동일)
String token;
try {
token = authorizationHeader.split(" ")[1].trim();
} catch (Exception e) {
log.error("토큰을 분리하는데 실패했습니다. - {}", authorizationHeader);
filterChain.doFilter(request, response);
return;
}
log.info("token : {}", token);
Java
복사
5.
token valid한지 확인하기
JwtTokenUtil에 메서드 추가
JwtTokenFilter에 만료된 토큰이면 filter되도록 메서드 추가
f.
실행해보기
i.
정상적인 경우 - 리뷰 작성 완료될 것이다.
b. Authorization 없는 경우
c. Bearer가 없는 경우
d. token이 유효하지 않은 경우 - 만료된 경우
그냥 에러가 난다
예외 처리는???
token의 userName가져오기
1.
JwtTokenUtil에 userName을 가져오는 메서드 만들기
//userName을 꺼내오는 메서드
public static String getUserName(String token, String secretKey){
return extractClaims(token, secretKey).get("userName", String.class);
}
Java
복사
2.
JwtTokenFilter에 userName을 파싱하여 로그에 찍어주는 과정 추가
//userName 넣기
String userName = JwtTokenUtil.getUserName(token, secretKey);
log.info("userName : {}", userName);
Java
복사
3.
실행하기
header에 token을 같이 보낸다.
userName이 찍힌다.
4.
서비스에 userName으로 User 찾는 메서드 추가
public User getUserByUserName(String userName){
return userRepository.findByUserName(userName).orElseThrow(() -> new HospitalReviewAppException(ErrorCode.USER_NOT_FOUND, String.format("아이디가 없습니다.")));
}
Java
복사
User의 role 만들기
권한 객체 만들기
1.
Entity field 추가
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String userName;
private String password;
private String email;
@Enumerated(EnumType.STRING)
private UserRole role;
}
Java
복사
2.
UserRole 작성
public enum UserRole {
ADMIN, USER;
}
Java
복사
3.
entity 만들 때 (UserJoinRequest → User) role도 추가되도록한다.
public User toEntity(String password){ //비밀번호 암호화해서 매개변수로 넣어서 엔티티 생성
return User.builder()
.userName(this.userName)
.password(password)
.email(this.email)
.role(UserRole.USER) //role 추가
.build();
}
Java
복사
userName을 AuthenticationToken에 넣어준다
1.
JwtTokenFilter에 과정 추가
User user = userService.getUserByUserName(userName);
//AuthenticationToken 만들기
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), null, List.of(new SimpleGrantedAuthority(user.getRole().name())));
Java
복사
2.
controller 수정 : authentication은 뭘 하는 건지??
@ApiOperation(value = "리뷰 쓰기")
@PostMapping
public String write(@RequestBody ReviewRequest reviewRequest, Authentication authentication){
log.info("Controller User : {}", authentication.getName());
return reviewService.write(reviewRequest);
}
Java
복사
3.
실행하기
a.
회원 가입
role이 생겼다.
b. 로그인
//token
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImlpIiwiaWF0IjoxNjcwMjIyODc1LCJleHAiOjE2NzAyMjY0NzV9.p_ZslScDIccwe5Czim8-gcG_dlaE1ZqG2_mVpnhcL7A
Java
복사
c. 헤더로 token 넘겨주면서 리뷰 쓰기 api 요청
httpstatus 200, 로그에 유저이름 확인 됨.
JwtTokenFilter
인증 방식
1) credential 기반 인증 : 사용자명과 비밀번호를 이용한 방식
2) 이중 인증(twofactor 인증) : 사용자가 입력한 개인 정보를 인증 후, 다른 인증 체계(예: 물리적인 카드)를 이용하여 두가지의 조합으로 인증하는 방식
3) 하드웨어 인증 : 자동차 키와 같은 방식
이중 Spring Security는 credential 기반의 인증을 취합니다.
•
principal : 아이디
•
credential : 비밀번호
GenericFilterBean vs OncePerRequestFilter
GenericFilterBean은 기존 필터에서 가져올 수 없는 스프링의 설정 정보를 가져올 수 있게 확장된 추상 클래스이다. 서블릿은 사용자의 요청을 받으면 서블릿을 생성해서 메모리에 저장해두고 동일한 클라이언트의 요청을 받으면 재활용하는 구조여서 GenericFilterBean을 상속받으면 RequestDispatcher에 의해 다른 서블릿으로 디스패치되면서 필터가 두 번 실행되는 현상이 발생할 수 있다.
이 같은 문제를 해결하기 위해 등장한 것이 OncePerRequestFilter이며, 이 클래스도 GenericFilterBean을 상속받고 있지만, 이 클래스를 상속받아 구현한 필터는 매 요청마다 한 번만 실행되게끔 구현된다.
Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal method with HttpServletRequest and HttpServletResponse arguments.
어느 서블릿 컨테이너에서나 요청 당 한 번의 실행을 보장하는 것을 목표로 한다.doFilterInternal메소드와 HttpServletRequest와 HttpServletResponse인자를 제공한다.
중요한 점은 요청 당 한번의 실행을 보장한다는 것이다.
서블릿이 실행되는 동안 다른 서블릿에 요청이 올 수도 있다.
예를들어, 어느 필터에서 헤더를 확인 한 후 특정 url로 포워딩 시킨다고 가정하자.
이때 예외가 발생하지 않았다면, url로 포워딩 시키는 것 자체가 서블릿 실행 중 요청이 온 것이다.
OncePerRequestFilter를 사용하지 않았다면 앞서 거친 필터들을 또 한번 거칠 것이고, 쓸데없는 자원만 낭비하는 셈이다.
결국 동일한 request안에서 한번만 필터링을 할 수 있게 해주는 것이 OncePerRequestFilter의 역할이고 보통 인증 또는 인가와 같이 한번만 거쳐도 되는 로직에서 사용한다.
인증 또는 인가를 거치고나서 특정 url로 포워딩하면, 요청이 들어왔으니 인증 및 인가필터를 다시 실행시켜야 하지만, OncePerRequestFilter를 사용함으로써 인증이나 인가를 한번만 거치고 다음 로직을 진행할 수 있도록 한다.
doFilterInternal()
OncePerRequestFilter에서 구현하는 메서드이다. doFilter()는 다음 filter-chain을 실행하는 것이며, 마지막 filter-chain인 경우 Dispatcher Servlet이 실행된다.
UsernamePasswordAuthenticationToken
1) 사용자가 사용자 이름과 암호를 제출하면 UsernamePasswordAuthenticationFilter는 HttpServletRequest에서 사용자 이름과 암호를 추출하여 일종의 인증인 UsernamePasswordAuthenticationToken을 생성합니다.
2) 다음으로 UsernamePasswordAuthenticationToken이 인증을 위해 AuthenticationManager로 전달됩니다. AuthenticationManager의 모양에 대한 세부 정보는 사용자 정보가 저장되는 방식에 따라 다릅니다.
3) 인증이 실패하면,
1.
SecurityContextHolder가 지워집니다
2.
RememberMeServices.loginFail이 호출됩니다. 기억하기가 구성되어 있지 않으면 작동하지 않습니다.
3.
다시 보낼 WWW-Authenticate을 트리거하기 위해 AuthenticationEntryPoint가 호출됩니다.
4) 인증에 성공하면,
1.
SessionAuthenticationStrategy는 새 로그인에 대한 알림을 받습니다.
2.
인증은 SecurityContextHolder에서 설정됩니다.
3.
RememberMeServices.loginSuccess가 호출됩니다. 기억하기가 구성되어 있지 않으면 작동하지 않습니다.
4.
ApplicationEventPublisher는 InteractiveAuthenticationSuccessEvent를 게시합니다.
5.
AuthenticationSuccessHandler가 호출됩니다. 일반적으로 이것은 로그인 페이지로 리디렉션할 때 ExceptionTranslationFilter에 의해 저장된 요청으로 리디렉션되는 SimpleUrlAuthenticationSuccessHandler입니다.
WebAuthenticationDetailsSource
SecurityContextHolder
•
Principal: 보호 된 리소스에 접근하는 사용자 식별자(아이디)
•
Credentials: 인증을 위해 필요한 정보(비밀번호)
•
GrantedAuthority: 인증 된 사용자의 인증 정보(역할 등)을 표현
•
SecurityContext: Authentication 객체를 포함하고 있으며, SecurityContextHolder를 통해 접근할 수 있다.
Spring Security 주요 모듈 (3 기준)
1.
아이디/패스워드 사용자 정보를 넣고 실제 가입된 사용자인지 체크한 후 인증에 성공하면 사용자의 principal과 credential 정보를 Authentication에 담는다.
2.
Spring Security에서 Authentication을 SecurityContext에 보관한다.
3.
SecurityContext를 SecurityContextHolder에 담아 보관한다.