일단 Member 엔티티에서 memberId가 중복되지 않도록 수정했다.
@Column(unique = true)
private String memberId;
다음 MemberRepository에서 memberId로 Member를 찾아오는 코드를 만들어주었다.
public Member findByMemberId(String memberId) {
return em.createQuery("select m from Member m where m.memberId= :memberId", Member.class)
.setParameter("memberId", memberId)
.getSingleResult();
}
SecurityConfig도 수정했다.
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // 사이트 위변조 요청 방지
.authorizeHttpRequests((authorizeRequests) -> { // 특정 URL에 대한 권한 설정.
authorizeRequests.requestMatchers("/user/**").authenticated();
authorizeRequests.requestMatchers("/admin/**")
.hasRole("ADMIN"); // ROLE_은 붙이면 안 된다. hasRole()을 사용할 때 자동으로 ROLE_이 붙기 때문이다.
authorizeRequests.anyRequest().permitAll();
})
.formLogin((formLogin) -> {
formLogin
.loginPage("/loginForm") // 권한이 필요한 요청은 해당 url로 리다이렉트
.usernameParameter("memberId") // PrincipalDetailsService에서 userName 대신 memberId 받도록 수정.
.loginProcessingUrl("/login") // login 주소가 호출되면 시큐리티가 낚아채서 대신 로그인을 해준다.
.defaultSuccessUrl("/"); //로그인 성공시 /주소로 이동
})
.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
.formLogin((formLogin) -> {
formLogin
.loginPage("/loginForm") // 권한이 필요한 요청은 해당 url로 리다이렉트
.usernameParameter("memberId") // PrincipalDetailsService에서 userName 대신 memberId 받도록 수정.
.loginProcessingUrl("/login") // login 주소가 호출되면 시큐리티가 낚아채서 대신 로그인을 해준다.
.defaultSuccessUrl("/"); //로그인 성공시 /주소로 이동
})
이 부분이 추가됐다.
.loginPage : user 혹은 admin 등의 권한이 필요한 페이지에서 해당 권한을 가지지 않은 사용자가 접속하려 할때 돌려보낼 페이지이다.
.usernameParameter("memberId") : PrincipalDetailsService를 만들어줄건데 여기서 userName을 가지고 로그인할 때 userName 대신 memberId를 받도록 수정해주는 코드이다.
.loginProcessingUrl : /login api로 가려고 할 때 security가 데이터를 낚아채서 대신 로그인을 해준다.
.defaultSuccessUrl : 로그인 성공시 이동할 url이다.
다음 security패키지의 auth패키지 안에 PrincipalDetails를 만들었다.
PrincipalDetails는 로그인을 도와주는 클래스다.
로그인이 완료되면 session이 만들어지는데
session안에는 Authentication 타입만 들어갈수 있고
Authentication 타입 안에는 UserDetails 타입만 들어갈 수 있는데
UserDetails에는 로그인하는 Member 객체가 들어가야 하기 때문에
UserDetails 를 implements 해주는 것이다.
import com.pr.boardproject.member.Member;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
//시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킨다
//로그인이 진행이 완료가 되면 session을 만들어 준다. (Security ContextHolder라는 키값에 세션정보를 저장시킨다. )
// Security ContextHolder에 들어갈 수 있는 객체는 Authentication 타입의 객체만 들어갈 수 있다.
//Authentication 안에는 Member 정보가 있어야 한다.
//Member 오브젝트 타입은 UserDetails 타입의 객체 여야 한다.
//Security Session > Authentication > UserDetails
// PrincipalDetails 클래스에 UserDetails를 implements하면 Authentication에 PrincipalDetails를 넣을 수 있게 된다.
// PrincipalDetails이 UserDetails가 되기 때문
public class PrincipalDetails implements UserDetails {
private Member member;
//일반 로그인 시 생성자
public PrincipalDetails(Member member) {
this.member = member;
}
public Member getMember() {
return member;
}
//해당 유저의 권한을 리턴하는 곳
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//getAuthorities 에는 user의 auth가 들어가야 하는데
//리턴 타입이 Collection<? extends GrantedAuthority>이기 때문에
//Collection<? extends GrantedAuthority> 객체를 생성하고 거기에 user.getAuth()를 넣어주었다.
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return member.getAuth();
}
});
return collect;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
//일년 동안 로그인을 안하면 휴면 계정으로 하기로 함
// 현재시간 - 마지막 로그인 시간
//해서 1년이 지났다면 false 이런식으로 사용할 수 있다.
return true;
}
}
auth패키지 안에 PrincipalDetailsService도 만들었다.
PrincipalDetailsService는 PrincipalDetails에서 사용할
로그인 하는 Member 객체를 찾아주기 위한 기능을 한다.
import com.pr.boardproject.member.Member;
import com.pr.boardproject.member.MemberRepository;
import lombok.RequiredArgsConstructor;
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;
// security config 에서 loginProcessingUrl("login"); 되어있는 코드는
// login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어있는
// loadUserByUsername 함수가 실행된다.
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
// 여기서 리턴하면 Authentication안에 UserDetails가 들어가고 security session에 그 Authentication이 들어가게 된다.
// security session(내부 Authentication(내부 UserDetails))
@Override
public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException {
Member memberEntity = memberRepository.findByMemberId(memberId);
if(memberEntity != null) {
return new PrincipalDetails(memberEntity);
}
return null;
}
}