[기능] 스프링 시큐리티와 구글 로그인 구현

SeoYehJoon·2024년 5월 27일
0

웹 개인공부

목록 보기
9/22
post-thumbnail

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시간 정도 공부해야 할듯하다 그러므로 일단은 구현 위주로 필요한 부분까지만 공부해보자.




SecurityConfig


먼저 제일 중요해보이는 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();
    }

}




PrincipalDetailsService (UserDetailsService 구현)


@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; //로그인 실패
    }
}




PrincipalOauth2UserService (DefaultOAuth2UserService 상속)

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());
    }

}




Member(Entity)

엔티티란 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;
    }
}




PrincipalDetail

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;
    }
}






일반 로그인 실행




구글 로그인 실행

profile
책, 블로그 내용을 그대로 재정리하는 것은 가장 효율적인 시간 낭비 방법이다. 벨로그에 글을 쓸때는 직접 문제를 해결한 과정을 스크린샷을 이용해 정리하거나, 개념을 정리할때는 최소2,3개소스에서 이해한 지식을 정리한다.

0개의 댓글