https://www.youtube.com/playlist?list=PL93mKxaRDidERCyMaobSLkvSPzYtIk0Ah
(메타코딩님 Youtube 무료강의)★★★★
https://dev-coco.tistory.com/174
https://gngsn.tistory.com/160 (FilterChain)
https://jiwondev.tistory.com/246?category=891823 (스프링 시큐리티 인프런정리)
스프링 시큐리티의 내부동작에대해 자세하게 공부하려면 20시간가량의 강의를 100시간 정도 공부해야 할듯하다 그러므로 일단은 구현 위주로 필요한 부분까지만 공부해보자.
먼저 제일 중요해보이는 HttpSecurity 객체에 대해서 알아보자
코드에서 보이듯 보안설정을 커스텀하기 위한 객체이다.
이제 라인별로 무슨 커스텀을 하는지 뜯어보자
permitAll() : 인증 필요없음
authenticated() : 인증 필요한 페이지
hasRole() : ADMIN 권한을 가진 유저만 접근 가능한 페이지
hasAnyRole() : 명시된 권한중 하나만 가지면 접근가능
loginPage : 기본 로그인 페이지
loginProcessingUrl : 로그인 페이지가 결과를 보내는 경로
defaultSuccessUrl : 성공시 이동하는 경로
usernameParameter : 로그인 페이지에서 아이디에 해당하는 속성
passwordParameter : 로그인 페이지에서 비밀번호에 해당하는 속성
loginPage : 기본 로그인 페이지
userInfoEndpoint : 구글 로그인 후 후처리(DefaultOAuth2UserService형이 매개변수로 들어가는데 개발자가 구현해줘야한다. )
@Configuration
@EnableWebSecurity
public class SecurityConfig
{
@Autowired
PrincipalOauth2UserService principalOauth2UserService;
//리턴되는 오브젝트를 IoC로 등록해준다.
//순환참조 오류때문에 이파일에서 관리하면 안된다.(OtherConfig로 이동)
/*@Bean
public BCryptPasswordEncoder encodePwd()
{
return new BCryptPasswordEncoder();
}*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, PrincipalDetailsService principalDetailsService) throws Exception
{
System.out.println("실행은 되냐************************");
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login","/hello","/login/joinForm","/error").permitAll()
.requestMatchers("/weather/getweather").authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("MANAGER", "ADMIN")
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/loginForm")
.loginProcessingUrl("/loginProc")//login주소가 호출되면 시큐리티가 낚아채서 대신 로그인 진행
.defaultSuccessUrl("/hello")
.usernameParameter("username")
.passwordParameter("userpw")
.permitAll())//form 태그 안의 input태그의 name속성을 의미
.oauth2Login(oauth2 -> oauth2
.loginPage("/loginForm")
.userInfoEndpoint(endpoint-> endpoint.userService(principalOauth2UserService))
);
return http.build();
}
}
@Service어노테이션이 있으므로 해당객체 안쪽의 loadUserByUsername함수는 로그인 요청이 오면 자동적으로 실행된다.
그리고 해당 함수는 UserDetails형 객체(코드에서는 PrincipalDetails이 상속받음)가 반환되고 해당객체는 Authentication객체에 들어가고 Authentication객체는 SpringContext에 들어가 관리당한다.
위의 코드부분을 SpringSecurity 흐름측면에서 살펴보면
위의 빨간 화살표 부분이라고 할 수 있다.
https://dev-coco.tistory.com/174
//시큐리티 설정에서 loginProcessingUrl("/login"); 설정을 해놨기 때문에
// /login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername
//함수가 실행 된다.
@Service
public class PrincipalDetailsService implements UserDetailsService
{
@Autowired
private final UserRepository userRepository;
public PrincipalDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
//이 함수는 loginForm에서 로그인버튼 클릭하면 실행되는 함수임
//loginForm에서 user_name이라는 name속성을 가진것에 대응된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
Member memberEntity = userRepository.findByUsername(username);
if(memberEntity != null)//멤버가 존재하면 properties에 정해놓은 아이디 비번 쓸모없다.
{
return new PrincipalDetail(memberEntity);
//여기서 리턴된 UserDetail(PrincipalDetail)은 Authentication
//내부에 쏙들어가게 된다.(PrincialDetail필기 참조)
}
return null; //로그인 실패
}
}
userRequest는 앱이 사용자의 정보를 받아오기 위한 여러가지 정보를 가지고 있다. 이걸로 super.loadUser()을 호출하면 된다.
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService
{
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserRepository userRepository;
//구글로 부터 받은 userRequest 데이터에 대한 후처리되는 함수
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException
{// OAuth2User는 PrincipalDetail과 대응된다.
System.out.println("getClientRegistration : " + userRequest.getClientRegistration().getRegistrationId());//registrationID로 어떤 OAuth로 로그인 했는지 확인 가능.
System.out.println("getAccessToken : " + userRequest.getAccessToken());
//구글 로그인 버튼 클릭 -> 구글 로그인창 -> 로그인을 완료 -> code를 리턴(OAuth-Client라이브러리가 code를 받는다) -> AccessToken요청
//userRequest 정보 받는다-> userRequest를 통해서loadUser함수 호출 -> 구글로부터 회원프로필 받아준다.
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println("getAttributes : " + super.loadUser(userRequest).getAttributes());
String provider = userRequest.getClientRegistration().getClientId();
String providerId = oAuth2User.getAttribute("sub");
String username = provider+"_"+providerId;//google_12312312312 format
String password = bCryptPasswordEncoder.encode("의미업다");
String email = oAuth2User.getAttribute("email");
String role = "ROLE_USER";
Member userEntity = userRepository.findByUsername(username);
if(userEntity == null)//DB에 유저정보 없을시
{
userEntity = Member.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userRepository.save(userEntity);
}
//반환된게 Authentication객체에 들어가게 된다.
return new PrincipalDetail(userEntity, oAuth2User.getAttributes());
}
}
엔티티란 DB의 테이블을 자바 객체로 구현해 놓은것이다.
@Entity
@Data
public class Member
{
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String userpw;
private String email;
private String role; //ROLE_USER, ROLE_ADMIN
private String provider;
private String providerId;
@CreationTimestamp
private Timestamp createDate;
public Member() {
}
@Builder
public Member(int id, String username, String password, String userpw, String email, String role, String provider, String providerId, Timestamp createDate)
{
this.id = id;
this.username = username;
this.password = password;
this.userpw = userpw;
this.email = email;
this.role = role;
this.provider = provider;
this.providerId = providerId;
this.createDate = createDate;
}
}
UserDetails와 OAuth2user을 상속한 PrincipalDetail이다. 왜 두개를 상속했냐면
UserDetails(일반로그인용 유저정보 담음) OAuth2user(외부로그인유저정보 담음)
을 동시에 받을 수 있기때문이다.
IDE의 오버라이드 함수 자동생성하기로 모든 필요한 함수 오버라이드 가능하다
@Data
public class PrincipalDetail implements UserDetails, OAuth2User
{
private Member member;
private Map<String, Object> attributes;
//일반 로그인용 생성자
public PrincipalDetail(Member member)
{
this.member = member;
}
//OAuth 로그인용 생성자
public PrincipalDetail(Member member, Map<String, Object> attributes)
{
this.member = member;
this.attributes = attributes;
}
//해당 Member의 권한을 리턴하는 곳
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new GrantedAuthority(){
@Override
public String getAuthority() {
return member.getRole();
}
});
return authorities;
}
@Override
public String getPassword() {
return member.getUserpw();
}
@Override
public String getUsername() {
return member.getUsername();
}
@Override
public boolean isAccountNonExpired()
{
return true;//true로 바꿔줘야한다(만료되지 않음)
}
@Override
public boolean isAccountNonLocked()
{
return true;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return true;
}
@Override
public String getName() {
return null;
}
@Override
public Map<String, Object> getAttributes()
{
return attributes;
}
}