컬쥐네 다락방
2021.04.07~08 Spring 회원가입,로그인 기능 공부 본문
Spring
2일부터 시작된 클론 코딩 주간.
이번 팀에서는 넷플릭스 홈페이지에 도전하고 있는데, 프론트와 백엔드의 작업을 나눠서 하다보니 지금까지 배웠던 내용들에 대해서 깊게 찾을 수 있는 시간이 생겼어요.
그래서 이 시간동안 지난주에 급하게 만들다가 다 날아가버린 회원가입, 로그인 기능에 대해서 더 자세히 공부하고 원리를 이해하는 시간을 가졌습니다. 시간이 없어서 이해하기 보다는 따라하기에 급급했기에 꼭 시간이 났을 때 공부하고 싶은 내용이었어요..
Spring Security의 구조
1. 사용자가 로그인 정보와 함께 인증 요청(Http Request)
2. AuthenticationFilter가 이 요청을 가로챕니다. 이 때 가로챈 정보를 통해 UsernamePasswordAuthenticationToken이라는 인증용 객체를 생성합니다.
3. AuthenticationManager의 구현체인 ProviderManager에게 UsernamePasswordAuthenticationToken 객체를 전달합니다.
4. 다시 AuthenticationProvider에 UsernamePasswordAuthenticationToken 객체를 전달합니다.
5. 실제 데이터베이스에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보(아이디)를 넘겨줍니다.
6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만듭니다. 이 때 UserDetails 는 인증용 객체와 도메인용 객체를 분리하지 않고 인증용 객체에 상속해서 사용하기도 합니다.
7. AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교합니다.
8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.
9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.
10. Authentication 객체를 SecurityContext에 저장합니다.
최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장합니다.
이런 방식은 세션-쿠키 기반의 인증 방식을 사용하는 것으로 스플이 시큐리티의 가장 기초적인 방식입니다.
Spring의 WebSecurity Config
일단 강의를 통해 배웠던 회원가입은 Spring의 WebSecurityConfig를 이용한 방식입니다 ! Spring이 지원해주는 기능으로 이걸 사용하니 정말 편리하게 구현할 수 있었어요. 물론 실제 회사에서 사용하기에는 부족한 부분이 많이 보였지만 간단하게 원리를 공부하기에는 최고..
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함, 스프링 시큐리티 로그인 페이지
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers().frameOptions().disable();
http.authorizeRequests()
// 이미지 폴더와 CSS 폴더에 대해서도 login 없이 허용하도록 한다.
.antMatchers("/assets/img/**").permitAll()
.antMatchers("/css/**").permitAll()
// js 파일도 허용이 되도록 한다.
.antMatchers("/js/**").permitAll()
// 회원가입 페이지도 허용이 되도록 구현
.antMatchers("/user/**").permitAll()
.antMatchers("/h2-console/**").permitAll()
// 그 외 모든 요청은 인증 과정이 필요하다.
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/user/login") // 기본 로그인 페이지가 아닌, login.html 페이지가 출력되도록 한다.
.failureUrl("/user/login/error")
.defaultSuccessUrl("/") // 로그인이 성공했을 경우 이동하는 페이지
.permitAll() // 로그인시 메인 페이지 접근 허용
.and()
.logout()
.logoutUrl("/user/logout")
.permitAll();
}
// 패스워드 암호화 구현
@Bean
public BCryptPasswordEncoder encodePassword() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
bulid.gradle에 기초적인 implementation을 넣어주고, 다음과 같은 WebSecurityConfig를 만들었습니다.
여기서 저는 antMatchers().permitAll() 기능을 이용해서 로그인하지 않은 사용자에게 어디까지 허용할 것인지에 대해서 판단해주는 작업을 할 수 있었습니다. (물론 여기서 모든 사이트를 허용해주고 무언가 기능을 사용하려고 할 때, 유저의 Role을 확인해서 User가 아니라면 로그인 사이트로 이동시키는 방법도 있었어요!!)
또 여기서 로그인 페이지를 별도로 설정하지 않으면 Spring이 제공하는 매우 심플한 페이지가 자동으로 뜨는 점이 신기했습니다 .. 패스워드를 Spring이 암호화 시켜주는 기능도 대단하다고 생각했구요...
회원가입시 정보 확인 절차
WebSecurityConfig 기능을 이용하면 회원가입 페이지에서 Input을 받는 부분에서 값을 읽어와 DB에 저장하고 로그인할 때 그 아이디와 비밀번호의 일치함만 확인하고 바로 로그인 권한을 줄 수 있었어요. 하지만 실제 페이지라면 회원가입을 할 때 아이디의 중복 여부와 아이디의 길이, 패스워드의 정확성을 검사해줘야 합니다.
이를 위해 다음과 같은 작업을 해줘야 했어요.
@Transactional
public void checkName(Member member){
String pattern = "^(?=.*\\d)(?=.*[a-zA-Z])[0-9a-zA-Z]{3,20}$"; // 영문, 숫자
String username = member.getUsername();
if(username.isEmpty()) {
throw new IllegalArgumentException("닉네임을 입력해 주세요.");
}
Matcher match = Pattern.compile(pattern).matcher(username);
if(!match.find()) {
throw new IllegalArgumentException("닉네임은 숫자와 영문자 조합으로 3~20자리를 사용해야 합니다.");
}
}
먼저 입력받은 아이디가 비어있거나 글자 수가 맞지 않을 때 사용자에게 알려주기 위한 함수에요. 물론 throw new를 이용했기 때문에 사용자는 무슨 에러가 나는지 알 수 없지만요.
여기서 pattern을 만들어주고 Spring의 Pattern에 있는 내장 함수로 처리하면 단 한줄이면 된다는게 참 신기했어요.
@Transactional
public void checkPassword(Member member){
//암호화 되지 않은 비밀번호로 비교
String username = member.getUsername();
String passwordNen = member.getPasswordNen();
String passwordConfirm = member.getPasswordConfirm();
if(passwordNen.isEmpty() || passwordConfirm.isEmpty()) {
throw new IllegalArgumentException("패스워드를 입력해 주세요.");
}
if(passwordNen.length() < 4 || passwordNen.length() > 20) {
throw new IllegalArgumentException("비밀번호는 4~20자리를 사용해야 합니다.");
}
if(passwordNen.indexOf(username) != -1) {
throw new IllegalArgumentException("비밀번호에 닉네임을 포함할 수 없습니다.");
}
if(!passwordNen.equals(passwordConfirm)) {
throw new IllegalArgumentException("패스워드가 일치하지 않습니다!");
}
}
이 외에도 비밀번호를 암호화 하기 전에 패스워드를 잘 입력했는지, 비밀 번호의 유효성에 대해서 검사하는 함수도 만들어서 돌려줬고
// - 데이터베이스에 존재하는 닉네임을 입력한 채 회원가입 버튼을 누른 경우 "중복된 닉네임입니다." 라는 에러메세지가 발생합니다.
@Transactional
public void checkNameDuplication(Member member){
Member m = memberRepository.findByUsername(member.getUsername());
if(m != null) {
throw new DuplicateKeyException("중복된 닉네임 입니다.");
}
}
닉네임의 중복 확인도 해줬습니다. 이렇게 모든 검사를 끝내고 통과한다면 save를 이용해서 Repository에 저장 !
이때 모든 에러는 사용자가 보는 것이 아니라 서버에 경고 알림이 끄는 것이기에 사용자에게도 알려줄 필요가 있었어요. 이건 물론 프론트의 부분이지만, 어떤 방법을 이용해야 내가 서버에서 에러가 났을 때 이를 보여줄지 고민을 굉장히 많이 했어요. 그러던 중 서버와 통신하는 ajax 통신에서 서버에서 에러 메세지가 돌아왔을 때 어떤 행동을 해야되는지 지정할 수 있다는 걸 알아냈습니다.
$.ajax({
type:'POST',
url:'/api/register',
contentType:'application/json',
data: JSON.stringify(data),
async : false,
beforeSend : function(xhr, opts) {
},
success: function (response){
},
error : function(err){
}
});
다음과 같이 ajax 통신을 하기 전 어떤 행동을 하게 지정해주기도 하고, 성공했을 때와 실패했을 때를 나눠서 행동을 지정할 수도 있었어요. 에러 메세지가 돌아왔을 때는 그 메세지를 읽어서 출력해주면 되겠구나 라는 생각을 했습니다 !
또한 서버에서만 검사를 하는 것이 아니라 먼저 프론트에서 검사를 해서 알림창을 통해 사용자에게 경고를 주고, 프론트에서의 검사를 통과했을 때 서버에서 2중으로 체크하는 것이 안전하기도 하고 편하기도 하다 생각했어요 .
이런 회원 가입이 끝나면 로그인 할 때는 DB와 비교해서 유효하다면 권한을 부여하는 방식으로 구현할 수 있었습니다 !!
번외
이 외에도 소셜 로그인 기능을 가볍게 체험해봤어요. 어디에나 붙어있는 카카오 로그인 기능!
카카오에 개발자 가입을 하고, 신청만 하면 간단하게 카카오에게서 권한을 부여받을 수 있었어요.

** 참고
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#get-token-info
이게 카카오 로그인의 과정입니다. 코드를 받고 토큰을 받고 토큰의 유효성을 확인 받는 과정.. 정말 복잡했어요.
그래서 항해 99 측에서 제공해준 코드를 이용해서 가볍게 체험만 해보고 아직 원리는 정확히 이해하지 못했습니다.
다만 이 코드들을 사용하면 로그인 기능을 만들고 사용할 수는 있겠지요 ! 다음에 제 어플이나 웹 페이지를 만든다면 추가하고 싶어요 .
꼭 공부할 예정입니다.
'웹 개발' 카테고리의 다른 글
Spring Security 심화 공부 - JWT 회원가입, 로그인 기능 (1) | 2021.04.19 |
---|