# 들어가며
4주차는 주특기 숙련주차로, Springboot,JPA를 사용하여 기존 페이지에 Spring security와 JWT를 이용하여 로그인,회원가입을 붙여서 보안과 관련된 내용을 알수있던 주차였다.
# 개인 과제
개인 과제 요구사항은 저번주에 만든 메모장 타임라인 서비스에 로그인,회원가입을 Security,JWT를 사용하여 붙여보는 것이였다. 먼저 Security로 강의를 따라가며 차근차근 로그인,회원가입을 만들어보았는데 대부분 생소한 내용이여서 아직도 JWT관련 부분은 강의가 자세히 나와있지도 않았고, 덧붙여서 할사람은 하라는식의 내용이였기 때문에 큰틀에서만 정리하고 Security 관련 부분은 아래 내용에 하나씩 차근차근 정리해나갈 생각이다. 우선, 바로 저번주 메모장 프로젝트에 이어서 로그인,회원가입을 붙인뒤 JWT를 적용한 모습을 차근차근 정리해본다.
...
github ) https://github.com/HunDeveloper16/spring01
# 구현
🔻
(Login에서 Select Shop문구를 고치는걸 깜빡했다..)
저번 메모장에선 메모장 내에서 작성자,유저,패스워드를 입력했다면 이번엔 로그인기능이 추가됐기 때문에, 해당내용을 먼저JWT 토큰에 담아서 활용하고 Controller에서 UserDetails로 유저 정보를 가져온뒤, 메모 추가,수정,삭제등을 구현하였다.
# 게시글 작성 API
위는 메모등록 api로, Spring Security를 통해 유저의 정보가 담겨있는 UserDetails를 매개변수로 불러와, UserDetails가 없다면 로그인이 되지 않았음을 의미하므로 예외처리를 해주고, 현재 포스트를 입력하고 있는 writer를 UserDetails의 getUsername으로 설정하여 프론트에서 활용할 수 있게 처리해주었다.
# Security 설정
JWT의 두가지 필터 FormLoginFilter와 JWTAuthFilter를 적용하였다. 각각 필터를 Bean으로 저장하고 configure 메서드에서 세션을 막고 UsernamePasswordAuthenticationFilter가 적용되기 전, 두가지 필터를 적용되게 하였다.
또한 JwtAuthFilter에서 URL,파일들에 대한 접근지정을 해주었다. JWT를 설정하지 않는다면 configure에서 설정해줘야할 내용이지만 해당 필터를 따로 만들어 뒀기에 클라이언트에서 JwtAuthfilter로 직접적으로 요청이오게 된다. 따라서 JwtFilter메서드에서 접근지정을 따로 설정해줬다.
# UserDetilasService
UserDetilasService는 DB에서 유저정보를 받아온뒤 UserDetilas로 넘겨주는 역할을한다. 이후 인증관리자로 가게되는데 클라이언트로부터온 로그인요청과 값을 비교해 로그인을 하게된다.
# 인증(Authentication) vs 인가(Authorization)
👉 인증은 사용자 신원을 확인하는 행위이며, 인가는 사용자 권한을 사용하는 행위이다.
웹에서의 인증 및 인가는 인증은 로그인을 통해 본인임을 확인하는 것으로 볼수있고, 인가는 카페 사이트내에서 회원 랭킹 별 가능한 첨부파일 크기를 다르게 부여할수 있는점이있다.
# HTTP?
먼저 HTTP는 사용자를 구별하지 못한다.
1번,2번요청을 통틀어 하나의 HTTP 요청이라고 볼수있다. HTTP는 상태를 저장하지 않기때문에, 위 그림에서 1번과 2번이 같은 클라이언트의 요청인지 알수가 없다.
# 쿠키(Cookies) vs 세션(Session)
쿠키와 세션은 위와 달리 HTTP에 상태 정보를 유지하기 위해 사용한다.
👉 즉, 쿠키와 세션을 통해 서버에서 클라이언트 별로 인증 및 인가를 할수 있게 된다.
# 세션 동작 방식
서버에서 클라이언트 별로 유일무이한 '세션ID'를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장하는데, 서버에서 생성한 '세션 ID'는 클라이언트의 쿠키값('세션 쿠키'라고 부른다)으로 저장되어 클라이언트 식별에 사용된다.
# 회원가입
👉 회원가입 어떻게 구상할까?
회원가입은 생각해보면 간단하다. 회원 가입 페이지를 구성한 뒤, POST api로 서버에게 전달하면 서비스에 넘어가고, 늘 해왔던 방식대로 회원가입값을 DTO로 받은후, 서비스내에서 회원 ID 중복이나 관리자 가입 요청등 각 예외처리를 한뒤, Repository에 save해주고 api리턴 값을 로그인페이지로 redirect 시켜주면 된다.
여기서 security를 사용중이라면, 아래와 같이 설정해준다.
@Override
protected void configure(HttpSecurity http) throws Exception {
// 회원 관리 처리 API (POST /user/**) 에 대해 CSRF 무시
http.csrf()
.ignoringAntMatchers("/user/**");
http.authorizeRequests()
// image 폴더를 login 없이 허용
.antMatchers("/images/**").permitAll()
// css 폴더를 login 없이 허용
.antMatchers("/css/**").permitAll()
// 회원 관리 처리 API 전부를 login 없이 허용
.antMatchers("/user/**").permitAll()
// 그 외 어떤 요청이든 '인증'
.anyRequest().authenticated()
.and()
// 로그인 기능
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
.failureUrl("/user/login?error")
.permitAll()
.and()
// 로그아웃 기능
.logout()
.permitAll();
}
}
# 패스워드 암호화
회원가입을 한후,만약 패스워드가 유출되면 모두가 알게될 사실을 우려하여 패스워드 암호화를 해준다.
여기선 스프링 시큐리티에서 권고하고 있는 BCrypt 해시함수를 사용하여 패스워드를 암호화한다.
//WebSecurityConfig Class
@Bean
public BCryptPasswordEncoder encodePassword() {
return new BCryptPasswordEncoder();
}
#Service
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
...
@Autowired
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public void registerUser(SignupRequestDto requestDto) {
...
// 패스워드 암호화
String password = passwordEncoder.encode(requestDto.getPassword());
...
}
}
회원 가입 시 패스워드를 암호화하여 저장하고, 로그인 인증 시 스프링 시큐리티가 자동으로 가져다 사용하게 되며, 로그인 처리 시 사용자가 입력한 패스워드 평문을 암호화하여 암호화된 DB의 패스워드와 비교하게 된다.
# 로그인 ,로그아웃 기능 구현
Client의 모든 요청은 시큐리티를 거치게 된다.Spring Security 역할
인증 성공 시 : Controller 로 Client 요청 전달 👉 Client 요청 + 사용자 정보 (UserDetails)
인증 실패 시 : Controller 로 Client 요청 전달 X
# 로그인 처리 과정
1. 로그인을 시도하면 인증관리자에게 username ,password 정보를 HTTP body로 전달(👉POST요청)한다.
2. UserDetailsService에게 username 을 전달하고 회원상세 정보를 요청한다.
3. DB에 회원 정보가 존재하지 않으면 Error를 발생시키고, 존재시 조회된 회원 정보를 UserDetails로 변환한다.
4. UserDetails를 인증관리자에게 전달한뒤, client가 보낸 username,password 와 UserDetails의
username,password를 비교하여 인증 성공 시 세션에 로그인 정보를 저장하고, 인증 실패 시 Error를 발생시킨다.
# 로그아웃은?
아래와 같이 Security를 설정한다.
...
@Override
protected void configure(HttpSecurity http) throws Exception {
// 회원 관리 처리 API (POST /user/**) 에 대해 CSRF 무시
http.csrf()
.ignoringAntMatchers("/user/**");
http.authorizeRequests()
// image 폴더를 login 없이 허용
.antMatchers("/images/**").permitAll()
// css 폴더를 login 없이 허용
.antMatchers("/css/**").permitAll()
// 회원 관리 처리 API 전부를 login 없이 허용
.antMatchers("/user/**").permitAll()
// 그 외 어떤 요청이든 '인증'
.anyRequest().authenticated()
.and()
// [로그인 기능]
.formLogin()
// 로그인 View 제공 (GET /user/login)
.loginPage("/user/login")
// 로그인 처리 (POST /user/login)
.loginProcessingUrl("/user/login")
// 로그인 처리 후 성공 시 URL
.defaultSuccessUrl("/")
// 로그인 처리 후 실패 시 URL
.failureUrl("/user/login?error")
.permitAll()
.and()
// [로그아웃 기능]
.logout()
// 로그아웃 처리 URL
.logoutUrl("/user/logout")
.permitAll();
}
}
...
로그아웃 처리에서 /user/logout으로 POST요청을 보내게 되면 서버 세션에 저장되어 있는 로그인 사용자 정보를 삭제하게 되고, 로그인페이지로 넘어가게 된다.
여기서 /user/logout이 POST인 이유는 CSRF protection이 기본적으로 enable되어 있기 때문인데 CSRF protection을 disable 하면 GET /user/logout으로도 사용 가능하다.
👉CSRF는 자세히 나와있는 블로그가 몇 없지만 그 중 하나를 참고하였다.
https://itstory.tk/entry/CSRF-%EA%B3%B5%EA%B2%A9%EC%9D%B4%EB%9E%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-CSRF-%EB%B0%A9%EC%96%B4-%EB%B0%A9%EB%B2%95
#JWT란?
JWT(JSon Web Token)는 유저를 인증하고 식별하기 위한 JSON토큰 기반 인증이다.
JWT는 로그인 정보를 Server에 저장하지 않고, Client에 로그인정보를 JWT로 암호화하여 저장한다.
모든 서버에서 동일한 Secret Key를 소유하고, 이 키를 통하여 암호화 / 위조를 검증한다.
JWT는 동시 접속자가 위와 같이 많을 때 서버 측 부하를 낮추고, Client,Server 가 다른 도메인을 사용할 때(ex) 카카오 OAuth2 로그인 시 JWT Token 사용) 사용할수 있다는 장점이있다.
그러나 구현의 복잡도가 증가하고, Secret key 유출 시 JWT 조작이 가능하다는 단점이있다.
JWT를 사용하면 아래와 같은 순서로 진행된다.
1. 클라이언트 사용자가 아이디 ,패스워드를 통해 웹서비스 인증
2. 서버에서 서명된 JWT를 생성하여 클라이언트에 응답으로 돌려준다.
3. 클라이언트가 서버에 데이터를 추가적으로 요구할 때 JWT 를 HTTP Header에 첨부한다.
4. 서버에서 클라이언트로부터 온 JWT를 검증한다.
👉 JWT의 구성요소
JWT는 HEADER , PAYLOAD , SIGNATURE로 나뉜다.
간단하게 Header는 토큰의 타입(JWT)과 해시 알고리즘의 종류가 담겨있고, PayLoad는 Claims이라 표현하며, 사용자의 데이터나 권한이 담겨있다. Signature는 Header, Paylod를 대상으로 Base64 Url Safe Encode를 적용하고, Header에 명시된 해시함수를 적용한 뒤, 이를 대상으로 비밀키로 서명한 것이다.
...
👉 Base64 Url Safe Encode
간단하게 말해서 웹으로 전송하기 위한 형태의 문자로 바꾼다고 생각하면된다.
Base64는 컴퓨터 분야에서 사용하는 이진 데이터를 문자 코드에 영향을 받지 않는 공통ASCII 영역의 문자들로 바꾸는 인코딩 방식을 말하는데 여기서 62,63번 글자를 -(minus),_(underlin)으로 변경한 것이 Base64 Url Safe이다.
...
Reference : https://koonsland.tistory.com/57
https://pronist.dev/143
# JWTAuthFilter , FormLoginFilter를 이용한 JWT 로그인 방식 이해
JWT는 수많은 필터를 사용할수 있지만, 그중 기본적인 JWTAuthFilter와 FormLoginFilter 두가지를 이용한다면 아래와 같은 흐름으로 처리된다.
위에서 아래로 시간의 흐름이 이동한다고 보면된다. FormLoginFilter에 POST요청 후 로그인 성공 시 JWT를 생성하고, 응답에 JWT를 포함하여 클라이언트에게 내려준다. 그 후 다시 GET요청을하면 JwtAuthFilter에서 유효성을 검증하고 컨트롤러에 진입하게 된다.
# 느낀점
이번주는 JWT때문에 상당히 힘들었다. 기본적인 Spring Security는 강의를 따라가며 차근차근 배워나가서 적용을 해볼수 있었는데 JWT는 강의가 딱 하나에 선택수강이였기 때문에 큰틀에서는 이해가갔지만 내부 코드에서 오류가 난다면 어디서부터 손을 봐야할지 막막했기 때문이다. 나뿐만이 아니라 게더내에 있는 한두분을 제외한 모든사람이 해당부분을 이해가 안되고 적용하지 않은사람도 절반정도 되어보였다. 과제를 처음에 진행하면서 목표가 'JWT를 나혼자 짜서 완벽하게 구현해보자' 였기에 유튜브나 인프런에서 이것저것 강의를 찾아보았는데 시큐리티 부분은 비교적 쉽게 이해가 갔으나 JWT 코드자체는 솔직히 알기 힘들었다. Java문법이 생소한 부분이 많았고 용어자체가 낯설었기 때문에 그렇다고 생각한다. 다음주차 과제가 발제 됐을때, 해당 내용에 관한 매니저님의 개인프로젝트가 있었는데 열어보니 JwtAuthenticationFilter만 사용한뒤 토큰을 발급하는 형식으로 사용하셨는데 이게 이렇게 간단히 될수있구나..라는 생각이 들었다. 딱 필요한 기본적인 내용만 들어가있는것 같았다. 이 내용을 참고해서 다시 프로젝트를 짜려했으나 프론트단에서 ajax를 어떻게 이어줘야할지 막막해서 우선 같은 프로젝트를 만들어서 BackEnd를 우선 똑같이 구현해두었다. 만약 나중에 실전 프로젝트에서 JWT를 사용하게 된다면 이렇게 고친 프로젝트를 참고하여 모르는내용을 매니저님과 상의하여 수정해나갈것같다.
항상 느끼는것이지만 이렇게 한주차 회고록을쓸때 위와같이 배운내용을 정리하면 머릿속에 정리가 잘 된다는 느낌이다. 쿠키나 세션, 시큐리티의 처리등 확실하게 짚고가는것이 깊은 의미가 있다고 생각한다. 보통 다른 분들의 회고록을 보면 당시 느꼈던 생각이나 감정의 글이 대부분이던데, 나는 이렇게 배운점을 다시 블로그에 정리해가며 느낀점을 정리하면 한주간 배웠던 내용이 머리에 잘 정리되는 느낌이 든다. 회고라는 정의에 벗어나는 글일수 있으나 지식이 쌓인다는 느낌이 들어 개인적으로는 만족감이 드는 방식인것같다.
'항해 부트캠프 > 항해99' 카테고리의 다른 글
항해99 ) 3주차 회고록 (0) | 2022.07.09 |
---|---|
항해99 ) 2주차 회고록 (0) | 2022.07.03 |
항해99 ) 첫 1주차(팀 단위 미니프로젝트) 프로젝트 (0) | 2022.06.22 |