회원가입을 위해 ddl-auto를 create로 바꿔서 테이블을 초기화 시켜준 후 update로 다시 바꾼다. 왜냐하면 비밀번호가 해쉬화 즉, 암호화가 되어야 하기 때문이다. 회원가입을 하는데 암호화가 안되어 있으면 로그인 자체가 안되도록 시큐리티가 막아준다.
해쉬암호
비밀번호를 해쉬암호로 바꿔서 넣어야 한다. 로그인을 요청하면 시큐리티가 지켜보다가 요청을 가로채서 파라미터인 username과 password를 가져온다. 이를 이용해 내부적으로 로그인을 진행시킨다. 로그인이 완료가 되면 시큐리티 전용 세션에 유저 정보를 등록한다. 이렇게 등록을 해두면 IoC에서 유저 객체를 관리하므로 필요할 때 DI로 가져올 수 있다.
유저 정보를 시큐리티 세션에 등록할 때 User 오브젝트를 지정된 타입인 UserDetails 타입으로 저장해야 한다.
- 방법 1) User 오브젝트가 UserDetails를 extends해서 부모타입으로 묶어버리는 방법 (다형성)
- 방법 2) BCryptPasswordEncoder
로그인을 대신 해줄 때 password를 해쉬로 암호화 시켜야지만 로그인 진행이 되도록 설정되어 있다. 해쉬 암호는 고정길이의 문자열 값인데 조금만 바꿔도 해쉬값이 바뀌어서 원본과 수정본을 잘 구분할 수 있는 장점이 있다.
SecurityConfig.java
// BCryptPasswordEncoder : 비밀번호를 암호화하는 데 사용할 수 있는 메서드를 가진 클래스
@Bean // IoC 등록
public BCryptPasswordEncoder encodePWD() {
return new BCryptPasswordEncoder();
}
UserService.java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder encoder;
@Transactional
public Integer 회원가입(User user) {
String rowPassword = user.getPassword(); // 원래암호
String encPassword = encoder.encode(rowPassword); // 해쉬암호
// 해쉬암호로 설정
user.setPassword(encPassword);
user.setRole(RoleType.USER);
try {
userRepository.save(user);
return 1;
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
}
스프링 시큐리티 로그인
loginform.jsp에서 로그인 버튼 눌렀을 때 /auth/loginProc로 가도록 설정해준다.
...
<div class="container">
<form action="/auth/loginProc" method="post">
...
</form>
</div>
...
근데 우린 UserApiContoller에서 /auth/joinProc 경로만 설정해주고 /auth/loginProc는 안만들었는데 어짜피 시큐리티에서 요청을 가로챌 것이기 때문이다.
SecurityConfig.java
@...
public class SecurityConfig extends WebSecurityConfigurerAdapter{
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.and()
.formLogin()
.loginPage("/auth/loginForm")
.loginProcessingUrl("/auth/loginProc") // 스프링 시큐리티가 로그인 가로채서 대신 로그인
.defaultSuccessUrl("/"); // 로그인이 정상적으로 끝나면 해당 주소로 이동
// .failuerUrl("/fail"); // 로그인 실패시 이동
}
}
로그인을 할 때 필요한 UserDetails 타입의 User 오브젝트를 만들어야 한다.
auth/PrincipalDetail.java
// 스프링 시큐리티가 로그인 요처을 가로채서 로그인을 진행하고 완료가 되면 UserDetails 타입의 오브젝트를 (PrincipalDetail)
// 스프링 시큐리티의 고유한 세션 저장소에 저장을 해준다.
@Getter
public class PrincipalDetail implements UserDetails{
private User user; // 콤포지션
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// 계정이 만료되지 않았는 지 리턴 (true : 만료안됨)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠겨있는 지 리턴 (true : 잠기지 않음)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호 만료 여부 (true : 만료안됨)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정 활성화 여부 (true : 활성화)
@Override
public boolean isEnabled() {
return true;
}
// 계정의 권한을 리턴
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 리턴은 GrantedAuthority을 상속받은 Collection타입 이어야만 함!!
Collection<GrantedAuthority> collectors = new ArrayList<>(); // ArrayList의 부모가 Collection이기 때문
collectors.add(()->{ return "ROLE_"+user.getRole(); });
/*
collectors.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return "ROLE_"+user.getRole(); // ROLE_ 꼭 넣어줘야함. 규칙!
}
});
*/
return collectors;
}
}
시큐리티가 대신 로그인해주는데 password 가로채기를 하는데 해당 password가 뭘로 해쉬가 되어 회원가입이 되었는지 알아야 같은 해쉬로 암호화해서 DB에 있는 해쉬랑 비교할 수 있다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(null).passwordEncoder(encodePWD());
}
누구한테 알려줘야 하냐면 아직 안만들어서 위의 null로 적어둔 오브젝트에게 알려줘야 한다.
이 오브젝트를 만들어보자
auth/PrincipalDetailService.java
// UserDetailsService 타입으로 만들기
@Service
public class PrincipalDetailService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
// 스프링이 로그인 요청을 가로챌 때, username, password 변수 2개를 가로채는데
// password 부분 처리는 알아서 한다.
// 해당 username에 DB에 존재하는지만 확인해주면 된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username)
.orElseThrow(()->{
return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다. :" + username);
});
// UserDetails 타입으로 리턴해야 하므로!!
// 근데 PrincipalDetail()은 User가 비어있으므로 생성자를 만들어줘야 한다.
return new PrincipalDetail(principal);
}
}
findByUsername 메소드는 직접 만들어줘야 한다.
UserRepository.java
public interface UserRepository extends JpaRepository<User, Integer>{
// SELECT * FROM user WHERE username = 1?;
Optional<User> findByUsername(String username);
}
PrincipalDetail()은 user 값이 null인 상태이므로 생성자를 만들어줘야 한다.
PrincipalDetail.java
...
public class PrincipalDetail implements UserDetails{
private User user;
//생성자
public PrincipalDetail(User user) {
this.user = user;
}
...
}
그럼 이제 로그인이 진행될 때 loadUserByUsername이 자동으로 실행되면서 Username이 DB에 있다는 것이 확인되면 리턴받은 PrincipalDetail(principal)이 시큐리티의 세션에 유저 정보로 저장이 되는 것이다. UserDetails 타입이므로 가능!!
이제 아까 그 null로 채워둔 자리에 UserDetails 타입으로 리턴해주는 PrincipalDetailService를 넣어줘서 해당 password가 뭘로 해쉬가 되어 회원가입이 되었는지 알려준 후 DB에서 알아서 비교하도록 한다. password는 지 알아서 다 해준다.
SecurityConfig.java
...
@...
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private PrincipalDetailService principalDetailService;
...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
}
...
}
그렇다면 BoardController는 세션을 어떻게 찾는걸까? 테스트해보자
BoardController.java
@Controller
public class BoardController {
@GetMapping({"", "/"}) // 컨트롤러에서 세션을 어떻게 찾는지?
public String index(@AuthenticationPrincipal PrincipalDetail principal) {
System.out.println("사용자 아이디 : "+principal.getUsername());
return "index";
}
}
앞으로 세션에 접근할 때 @AuthenticationPrincipal PrincipalDetail principal 로 접근하면 된다.
최종 정리
1. 로그인 요청
2. /auth/loginProc 로그인 요청 가로챔
3. 가로챈 username, password로 대신 로그인 진행 - loadUserByUsername으로 DB에서 아이디 찾고 비번은 알아서 비교
4. 다 성공적으로 비교가 끝나면 스프링 시큐리티 세션에 유저 정보가 등록
5. "/"로 이동
출처 : https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9
'Spring > Blog 만들기 with SpringBoot' 카테고리의 다른 글
회원 수정 (0) | 2022.07.25 |
---|---|
게시판 (0) | 2022.07.25 |
스프링부트 Spring Security (0) | 2022.07.14 |
전통적인 방식의 로그인 (0) | 2022.07.09 |
스프링 JPA의 OSIV 전략 (0) | 2022.07.09 |