오늘은 스프링 시큐리티를 이용한 로그인 기능을 구현해보도록 하겠습니다
처음 프로젝트 생성했을때 시큐리티 라이브러리를 추가하지 않았기 때문에
제일 먼저 pom.xml을 열어 시큐리티 라이브러리를 추가해줍시다
또 view에서 현재 로그인한 사용자의 정보를 사용할수 있도록 시큐리티 태그
라이브러리도 추가해줍시다
<!-- 스프링 시큐리티 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 시큐리티 태그 라이브러리 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
</dependency>
시큐리티 라이브러리를 추가하게 되면 이제 모든 페이지에 대한 접근을 시큐리티가
가로채기 때문에 회원가입 페이지로 접속하려고 해도 무조건 시큐리티 로그인 화면으로
넘어가게 됩니다 하지만 비회원이라도 우리가 구현한 회원가입페이지와 로그인페이지에
대한 접근은 허용해줘야 하기 때문에 시큐리티 설정을 해줘야 합니다
최상위 패키지에 config패키지를 생성한후 SecurityConfig클래스를 추가해주세요
package com.han.delivery.config;
import org.springframework.context.annotation.Bean;
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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder encode() {
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest()
.permitAll()
.and()
.formLogin()
.loginPage("/auth/signin")
.loginProcessingUrl("/auth/signin")
.defaultSuccessUrl("/")
.failureUrl("/auth/failed");
}
}
스프링 시큐리티를 활용하기 위해서는 패스워드를 암호화 해야 하고
패스워드를 해쉬로 암호화주는게 BCryptPasswordEncoder에 encode메서드입니다
CSRF 공격이란, 인터넷 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위(등록, 수정, 삭제 등)를 특정 웹사이트에 요청하도록 만드는 공격으로 스프링 시큐리티에서
CSRF는 기본값이 enable인데 rest api를 이용하는 서버는 session 기반 인증과 달리 stateless 하기 때문에 따로 서버에 인증정보를 보관하지 않습니다
따라서 매번 api 요청에 있는 csrf 토큰을 받지 않기 때문에 disable해줍니다
ROLE은 권한을 말하는데 USER일 경우에 접근 가능한 페이지 요청과
ADMIN(관리자)일 경우에 접근 가능한 페이지 요청을 설정해줘야 합니다
이제 스프링시큐리티를 설정은 끝났고 실제로 시큐리티를 사용하기 위해서는
시큐리티가 구현해놓은 객체를 상속받아 메서드들을 재정의해줘야합니다
방금 생성한 config패키지안에 auth패키지를 추가하고
CustomUserDetails 클래스를 추가해줍니다
package com.han.delivery.config.auth;
@Data
public class CustomUserDetails implements UserDetails{
private static final long serialVersionUID = 1L;
private int id;
private String username;
private String password;
private String nickname;
private String phone;
private int point;
private String role;
//계정이 가지고 있는 권한을 리턴
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority(this.role));
}
@Override
public String getUsername() {
return this.username;
}
@Override
public String getPassword() {
return this.password;
}
//계정이 만료되었는지를 리턴 true : 만료되지 않음
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return true;
}
//계정이 잠겨있는지를 리턴
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
}
//패스워드가 만료되었는지를 리턴
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
//계정이 사용가능한지를 리턴
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
}
위의 메서드들은 반드시 Override 되어있어야합니다 하나라도 빠지면 오류가 발생합니다
private static final long serialVersionUID = 1L의 경우 직렬화와 관련이 있는데
직렬화는 단기간 혹은 장기간 데이터를 보존하는 용도로 나눌수 있습니다
이 직렬화를 해제하기 위해서는 직렬화 시점의 클래스와 해제 시점의 클래스가 일치해야
하는데 저장기간이 길어지면서 클래스의 내용이 바뀌게 되면 직렬화 시점의 클래스와
해제 시점의 클래스가 일치하지 않아 직렬화를 해제 할수 없는 경우가 생깁니다
이때 serialVersionUID를 선언해두면 클래스의 내용이 바뀌게 되더라도
serialVersionUID의 변수의 값이 같으면 동일한 클래스로 간주하여 해제가 가능합니다
이제 실제 로그인처리를 하기 위해서 사용자가 입력한 아이디가 DB에 있는지를
확인해서 존재할 경우 그 회원의 정보를 방금 추가한 CustomUserDetails에 담아
가져올겁니다 AuthMapper.java와 AuthMapper.xml에 코드를 추가해주세요
public CustomUserDetails getUser(String username);
<select id="getUser" resultType="com.han.delivery.config.auth.CustomUserDetails">
SELECT * FROM DL_USER WHERE USERNAME = #{username}
</select>
resultType에 반드시 풀패키지명을 적어주셔야 합니다
이제 로그인 처리를 위한 서비스를 추가해줘야 합니다
auth패키지안에 CustomUserDetailsService 클래스를 추가해줍시다
package com.han.delivery.config.auth;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
AuthMapper authMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
CustomUserDetails principal = authMapper.getUser(username);
if(principal == null) throw new UsernameNotFoundException("회원이 존재하지 않습니다");
return principal;
}
}
UserDetailsService를 상속받아 loadUserByUsername를 반드시 재정의해줘야 합니다
이 안에서 사용자로부터 입력받은 아이디를 통해 DB에 해당 아이디가 존재하는지의
여부를 확인하여 CustomUserDetails객체에 해당 정보를 담아옵니다
만약 해당 아이디를 가진 데이터가 테이블에 존재한다면 이 객체를 반환하고
이 객체에는 유저의 password가 존재하므로 시큐리티가 사용자로부터 받은 패스워드를 BCryptPasswordEncoder encode()를 통해 해시화하여 두 정보를 비교하여
로그인 처리를 하게 됩니다
그러나 우리는 회원가입을 구현할때 사용자로부터 입력받은 비밀번호를 해시화해주지
않고 DB에 바로 저장했기 때문에 기존에 회원가입한 정보로 로그인을 시도해도
로그인이 불가능합니다 따라서 테이블안에 기존에 저장되어있는 데이터가 있다면
모두 지우고 회원가입을 요청할때 사용자가 입력한 패스워드를 해시화하여 DB에
저장하도록 기존에 AuthService부분의 회원가입 로직을 수정하도록 합시다
//추가
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
//기존
@Transactional
public void signup(SignupDto signupDto) {
authMapper.signup(signupDto);
}
//수정
@Transactional
public void signup(SignupDto signupDto) {
String encPassword = bCryptPasswordEncoder.encode(signupDto.getPassword());
signupDto.setPassword(encPassword);
authMapper.signup(signupDto);
}
bCryptPasswordEncoder를 이용해 사용자로부터 입력받은 패스워드를 해시화 한 다음
Dto의 setter를 통해 기존의 password위에 덮어씌운후 DB에 저장합니다
시큐리티 로그인에 대한 부분은 모두 끝났습니다 이제 마지막으로 로그인을 위해
로그인 화면을 구현해봅시다 views/auth폴더안에 signin.jsp를 추가해주세요
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="/WEB-INF/views/layout/link.jsp" %>
<link rel="stylesheet" href="/css/auth/signin.css">
</head>
<body>
<main>
<div class="login_box">
<a href="/"><img src="/img/bamin2.png" alt="이미지" class="bm_img"></a>
<form action="/auth/signin" method="post">
<div class="input_aera"><input type="text" name="username" value="" required placeholder="아이디를 입력해주세요" maxlength="30" ></div>
<div class="input_aera"><input type="password" name="password" value="" required placeholder="비밀번호를 입력해 주세요" maxlength="30"></div>
<input type="submit" value="로그인" class="login_btn" >
<div class="box">
<div class="continue_login">
<label for="continue_login">
<span>로그인 유지하기</span>
<input type="checkbox" id="continue_login" name="remember-me" >
<i class="fas fa-check-square"></i>
</label>
</div>
<div>
<span class="id_search"><a href="/find/id">아이디</a></span>
<span> ㅣ </span>
<span><a href="/find/password">비밀번호 찾기</a></span>
</div>
</div>
</form>
<div id="oauth_login">
<div>
<a href="/oauth2/authorization/kakao"></a>
</div>
<div>
<a href="/oauth2/authorization/naver"></a>
</div>
<div>
<a href="/oauth2/authorization/google"></a>
</div>
</div>
<div class="join"><a href="/auth/signup" >회원 가입하러 가기</a></div>
</div>
</main>
</body>
</html>
항상 감사하게 잘 보며 공부하고있습니다! 제가 로그인 기능 구현을 하고있는데 DB에 있는 아이디와
비밀번호를 알맞게 입력을해도 계속 아이디와 비밀번호가 맞지않습니다라고 나옵니다. 그렇다면
CustomUserDetails 부분이 잘못된걸까요? 도움주시면 감사하겠습니다 ㅠㅠ