Spring Security 로그인 회원가입

이민재·2022년 8월 20일
0

스프링 시큐리티를 사용, 간단한 로그인 예제를 구현한다.

Dependencies

  • Spring web
  • Spring security
  • Spring Data JPA
  • Lombok
  • MySQL Driver
  • Mustache
  • Spring Boot DevTools

Spring security confige 파일 작성


@Configuration
@EnableWebSecurity// 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableGlobalMethodSecurity(securedEnabled =  true , prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/loginForm")
                .loginProcessingUrl("/login") // login 주소가 호출이 되면 시큐리티가 대신 낚아서 호출해준다
                .defaultSuccessUrl("/");
    }
}
  • @EnableGlobalMethodSecurity(securedEnabled = true , prePostEnabled = true)은 @Secured("ROLE_ADMIN")과
    @preAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN)")를 사용하기 위해 추가한 어노테이션이다.

  • antMatchers를 통해 해당 경로에 대한 접근 설정을 할 수 있다.

  • loginProcessingUrl은 로그인 진행 시 어떤 경로에서 인증 시스템을 진행할 것인지를 설정하는 메소드이다.

    • 스프링에서는 기본 경로가 /login으로 설정되어 있으며 이 프로세스는 UserDetailsService로 향한다.
  • WebSecurityConfigurerAdapter를 상속 받아 오버라이드 할 수 있다

    • http 관련 인증 설정 가능
    • 스프링부트 2.7.0 이상 버전에서는 Deprecated되어져있다.
  • 접근 설정

    • anyMatchers : 경로 설정과 권한 설정이 가능
    • permitAll() : 누구나 접근이 가능
    • hasRole() : 특정 권한이 있는 사람만 접근 가능
    • authenticated() : 권한이 있으면 무조건 접근 가능(종류 상관 X)
    • anyRequest : anyMatchers에서 설정하지 않은 나머지 경로를 의미
  • Login 관련 설정

    • loginPage() : 로그인 페이지 링크 설정
    • defaultSuccessUrl() : 로그인 성공 후 리다이렉트할 주소
    • 따로 별도의 설정이 없을 경우 security 자체 로그인 페이지로 이동한다.
  • Logout 관련 설정

    • logoutSccessUrl() : 로그아웃 성공 후 리다이렉트할 주소
    • invalidateHttpSession() : 로그아웃 이후 세션 전체 삭제 여부

암호화 코드

@Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

BCryptPasswordEncoder를 securityconfig 안에 작성하는 예시가 많았는데 몇몇 경우에서 순환 참조가 일어나 오류가 발생.

@Service
public class encoderService {
	
	
    // Password 인코딩 방식에 BCrypt 암호화 방식 사용
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

따로 분리하여 진행하였다.


User 모델과 repository

@Entity
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;
    private String role;

    @CreationTimestamp
    private Timestamp createDate;
}
public interface UserRepository extends JpaRepository<User, Integer> {
    User findByUsername(String username);
}

security 흐름을 파악하기 위해 최대한 간단한 형태로 진행했다.


UserDetails class 커스텀

public class PrincipalDetails implements UserDetails {

    private User user; // 콤포지션

    public PrincipalDetails(User user){
        this.user = user;
    }


    // 해당 User의 권한을 리턴하는 곳!!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        //user.getRole()이 String이라 변환해야 됨

        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
    //이 계정 만료 되었는가? false 만료/ true 유효
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    //잠겼는가
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    //비밀번호를 오래 썼는가
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    //활성화 여부
    public boolean isEnabled() {
        return true;
    }
}

시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.

로그인을 진행이 완료가 되면 시큐리티 session을 만들어준다.(Security ContextHolder)

Security Session => Authentication => UserDetails 순으로 전환해서 저장한다.


UserDetailsService 구현체 작성

로그인을 위한 username (혹은 id, email) 이 DB에 있는지 확인하는 메서드 loadUserByUsername 메서드를 작성합니다.

@Service
public class PrincipalDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);
        if(userEntity != null){
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
}
  • login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어있는 loadUserByUsername 함수 실행
  • findByUsername 메서드를 이용해 입력된 username이 유효한지 확인.
  • username이 유효하지 않다면 예외를 발생시키고 유효하다면 username으로 찾아온 userEntity를 이용해 Custom userDetails인 PrincipalDetails에 userEntity를 담아 생성하여 반환합니다.

user 계정하나를 만들어 테스트를 해보겠습니다.

test용 indexController 생성


@Controller
public class indexController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    @GetMapping({ "", "/" })
    public @ResponseBody
    String index() {
        return "index";
    }

    @GetMapping("/admin")
    public @ResponseBody String admin() {
        return "어드민 페이지입니다.";
    }

    //@PostAuthorize("hasRole('ROLE_MANAGER')")
    //@PreAuthorize("hasRole('ROLE_MANAGER')")
    //@Secured("ROLE_MANAGER")
    @GetMapping("/manager")
    public @ResponseBody String manager() {
        return "매니저 페이지입니다.";
    }

    @GetMapping("/loginForm")
    public String loginForm() {
        return "loginForm";
    }

    @PostMapping("/join")
    public @ResponseBody String join( User user) {
        user.setRole("ROLE_USER");
        String rawRassword = user.getPassword();
        String encPassword = passwordEncoder.encode(rawRassword);
        user.setPassword(encPassword);
        userRepository.save(user);
        return "redirect:/loginForm";
    }

    @GetMapping("/joinForm")
    public String joinForm() {
        return "joinForm";
    }

    @GetMapping("/info")
    public @ResponseBody String info(){
        return "개인정보";
    }

}

securityconfig에서 .loginPage("/loginForm") 설정을 했기 때문에 인증이 되지 않았을 때 loginForm이 실행된다.

만들어지 계정으로 로그인을 하면

index 페이지로 잘 이동하는 것을 확인 할 수 있다.
하지만 ROLE_USER 권한으로는 인가되지 안은 /admin으로 이동 시

403 forbidden 오류가 난 것을 확인 할 수 있다.


이상으로 정말 간단한 형태의 security의 세션 방식 로그인을 구현했습니다.
정리용으로 포스트 했고 아주 간단한 내용인데요 생각보다 시간이 오래 걸렸습니다.
다음 포스트는 oauth2로 sns인증과 jwt토큰을 사용한 인증을 구현하려고 합니다

profile
초보 개발자

0개의 댓글