이전 글에서 시큐리티를 적용하여 사용자가 입력한 암호를 인코딩하는 작업을 진행하였다. 이번 글에서는 이제 이 암호화 된 암호로 로그인하는 코드를 구현해보자!!
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manager/**").access("hasAnyRole('ROLE_MANAGER','ROLE_ADMIN')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
// 추가
.loginProcessingUrl("/login")
.defaultSuccessUrl("/");
return http.build();
// "/login"이라는 url이 호출되면 시큐리티가 낚아채 대신 로그인을 진행해준다.
그래서 Controller에 /login을 호출하는 메소드를 만들지않아도 된다.
<h1>로그인 페이지</h1>
<hr/>
<form action="/login" method="POST"> // 추가
<input type="text" name="username" placeholder="Username"/> <br/>
<input type="password" name="password" placeholder="Password"/> <br/>
<button>로그인</button>
</form>
입력한 아이디, 패스워드를 가지고 /login으로 이동한다.
security01 > config > auth
package com.cos.security01.config.auth;
import com.cos.security01.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class PrincipalDetails implements UserDetails {
private User user; //콤포지션
public PrincipalDetails(User user){
this.user = user;
}
// 해당 유저의 권한을 리턴한다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 우리가 지정한 Role은 String타입이라 user.getRole(); 하여 지정할 수 없다.
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
앞선 설정을 통해 시큐리티가 /login이라는 요청을 낚아 채 로그인을 진행할 수 있도록 했다.
로그인이 완료되면 세션이 만들어지는데 시큐리티는 시큐리티 자신만의 세션 저장공간을 가지고있다.
이때, Security ContextHolder
라는 Key
값에 세션 정보를 저장시키는데 여기에 들어갈 수 있는 정보는 오브젝트 타입은 Authentication
타입으로 정해져있다.
또, 이 Authentication
안에는 User정보가 있어야하는데 그 유저 오브젝트의 타입은 UserDetails
타입이어야한다.
위에 true true라고 적어놓은 것들은 일단 신경 안쓰기위해 true로 지정하였는데 false가 되는 경우를 설명하면, 우리 사이트에 1년동안 로그인 하지않은 회원을 휴먼계정 처리 하기로했다면? 저기에 현재시간 - 최종로그인 시간을 해 일년을 초과하면 false를 리턴하게 로직을 짤 수 있다.
시큐리티 설정에서 loginProcessingUrl을 ("/login")으로 지정해놧는데 이 요청이 오면 자동으로 UserDetailsService로 IoC되어있는 loadUserByUsername 함수가 실행된다.
package com.cos.security01.config.auth;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
UserDetails에 보면 넘어오는 파라미터 이름이 username인 것이 보인다.
loginForm에서 name으로 지정햇던 username과 이 이름은 동일해야하고 만약 이가 다르면 인식하지 못한다.
그럼에도 loginForm에서 name을 굳이 다른 이름으로 지정하고자한다면??
// SecurityConfig에 추가
.usernameParameter("newName");
PrincipalDetailsService까지의 과정을 살펴보면
- loginForm에서 로그인 버튼을 눌러 /login으로 이동
- Spring은 IoC 컨테이너에서 UserDetailsService로 작성된 클래스가 있는지 찾음
- 있다면 거기서 loadUserByUsername메소드를 호출하는데 이때 넘어온 파라미터 username을 가지고온다.
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
return null;
}
}
파라미터로 넘어온 username을 가지고 그 이름이 DB에 존재하는지 확인하기위해 repository에 findByUsername을 호출한다.
public interface UserRepository extends JpaRepository <User, Integer> {
public User findByUsername(String username);
}
findBy까지는 규칙이고 Username 문법인데(자세한건 JPA QueryMethods 검색) 아무튼 이렇게 하면 호출 시 아래 쿼리문이 실행된다.
select * from user where username = ? (username)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if(userEntity!=null){
return new PrincipalDetails(userEntity);
}
}
repository에서 username을 검색하고 해당 값이 null이 아니라면 PrincipalDetails로 간다.
아까 앞에서 설명할 때
시큐리티에 세션 정보를 저장시키기 위해서는 Authentication타입이 들어가야하고, 또 이 Authentication 안에는 User정보가 있어야하는데, 그 타입은 UserDetails타입이라 했다.
위에서처럶 PrincipalDetails(userEntity)를 리턴하게되면 리턴 된 값이 Authentication내부에 들어가고, 세션 내부에는 다시 Authentication가 들어가게된다.
SecuritySession(Authentication(UserDetails))
이제 회원가입과 패스워드 인코딩 그리고 로그인이 잘 되는지 확인해보자.
localhost:8080/user url을 입력하면
로그인 폼 페이지로 이동한다.
콘솔
User(id=0, username=posasac, password=1234, email=posasac@aaa.com, role=null, createDate=null)
Hibernate:
insert
into
User
(createDate, email, password, role, username)
values
(?, ?, ?, ?, ?)
success
User(id=3, username=posasac, password=$2a$10$6Gd6rjqwSgcV5FG3qKXAgurKsFfzDOC0ExonoGLXpDTm7VvpKNVZW, email=posasac@aaa.com, role=ROLE_USER, createDate=2023-03-10 13:21:56.211)
회원가입과 패스워드 인코딩 처리가 잘 된 것을 볼 수 있다.
가입시 입력한 이름과 비밀번호로 로그인을 시도하면
정상적으로 로그인 성공@!!!!@!@