I can do it(Feat. DEV)

Spring Security 사용하여 로그인 구현하기 본문

개발자 모드/응용

Spring Security 사용하여 로그인 구현하기

까짓거 해보자 개발자 2022. 11. 3. 16:38
728x90

다들 아시다시피 Spring Security 5.7.x 버전 이후부터

WebSecurityConfigurerAdapter가 deprecate(사용되지 않게) 될 예정이기 때문에

Spring Security에서 지향하는 SecurityFilterChain을 @Bean으로 등록하는 방식으로 진행해보겠음.

 

Spring Security without the WebSecurityConfigurerAdapter에서 자세한 설명 확인 가능함.

 

필자는 먼저 단일 로그인 방식, 즉 하나의 security config로 로그인을 구현한 뒤,

멀티 로그인 방식으로 진행할 예정(다음 포스팅에서)임.

멀티 로그인 방식은 쉽게 말해 사용자, 관리자의 로그인 화면이 개별로 있고

spring security 설정도 사용자, 관리자 2개라는 말임.


1. 개발 스펙

- Spring Boot 2.7.3

- H2 DB

- Java 1.8

- Spring Security 5.7.3

- Maven 3.6.3

- Intellij IDEA 2021.2

- JSP

- JPA


2. 로그인 구현

2-1. 로그인 화면 구현(jsp)

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<c:set var="context" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
    <h1>사용자 로그인</h1>
    <form action="/login" method="post">
        <span>id : </span><input type="text" name="username" required />
        <span>password : </span><input type="password" name="password" required/>
        <input type="submit" value="로그인" />
    </form>
</body>
</html>

화면 구성 시 주의할 점이 있음.

1. form action 경로는 security config에서 loginProcessingUrl("경로")와 일치해야 한다는 점

2. id, password input 태그의 name 값을 username, password 이외에 다른 걸 사용할 땐 security config login 부분에  따로 설정해야 함.(name 값을 username, password로 설정 시 설정 안 해도 됨)

 

2-2. 로그인 화면

로그인 화면
다소 조촐한 로그인 화면🤭


2-3. Controller 구현

@Controller
@RequestMapping
public class UserMainController {    

    @GetMapping("/")
    public String main(){	//메인화면  
        return "user/index";
    }

    @GetMapping("/login")	
    public String login(){	//로그인화면
        return "user/login";
    }
}

이런 식으로 간단하게 구성해 줌.

메인 화면은 로그인 성공 시 넘어가는 화면으로 로그인 화면 만든 것처럼 만들면 됨!

참고로 필자는 user data를 미리 세팅했음.

user 테이블
h2 db User 테이블


2-4. Spring Security Config

아시다시피 설정은 개발자마다 하기 나름이라서 꼭 이 포스팅대로 안 하셔도 되며,

더 좋은 방법이 있다면 댓글로 알려주시면 감사!

@Configuration
public class UserSecurityConfiguration {	//유저 로그인 설정

    @Bean
    public UserDetailsService userDetailsService(){
        return new UserDetailService();
    }

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

    @Bean
    public SecurityFilterChain userFilterChain(HttpSecurity http) throws Exception {
        http
                .antMatcher("/**")		// "/" 경로로 들어오는 모든 요청을
                .authorizeHttpRequests()	// 검증하겠음
                .antMatchers("/")	//	"/" 경로는
                .permitAll()	//접근 허용
                .anyRequest()	//다른 요청들은
                .hasAnyAuthority("ROLE_USER")	//ROLE_USER 권한 있어야지 접근 가능
                .and()
                .formLogin()	
                .loginPage("/login")	//로그인 페이지 설정
                .loginProcessingUrl("/login")	//앞에서 말했던 form action 설정과 일치해야함.
                .failureUrl("/login?error=true")	//로그인 실패 시 설정
                .defaultSuccessUrl("/")	//로그인 성공 시 이동 화면
                .permitAll()	// 로그인은 모두 접근 허용
                .and().logout()	//로그아웃 설정
                .logoutUrl("/logout")	//로그아웃 경로 설정
                .logoutSuccessUrl("/")	//로그아웃 성공 시 이동화면
                .and()
                .csrf().disable();	//test서버이기 때문에 csrf 미사용
                

//아래 주석부분은 위 설정을 lambda식으로 변경한 것임.
//why? lambda 식 설정이 가독성에 좋다고해서 설정해봄.
//        http
//                .authenticationProvider(userAuthenticationProvider())
//                .antMatcher("/**")
//                .authorizeHttpRequests(authorize -> authorize
//                        .antMatchers("/").permitAll()
//                        .anyRequest().hasAnyAuthority("ROLE_USER"))
//                .formLogin(form -> form
//                        .loginPage("/login")
//                        .loginProcessingUrl("/login")
//                        .failureUrl("/login?error=true")
//                        .defaultSuccessUrl("/")
//                        .permitAll())
//                .logout(logout -> logout
//                        .logoutUrl("/logout")
//                        .logoutSuccessUrl("/"))
//                .csrf(csrf -> csrf.disable());

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {	//h2-console 모든 경로는 security 적용 안함.
        return (web) -> web.ignoring().antMatchers("/h2-console/**");
    }
}

WebSecurityConfigurerAdapter가 deprecate 되면서 바뀐 설정 중 한참을 헤맸던 부분이

바로 UserDetailsService, passwordEndoer를 어떻게 설정하는지를 모르겠다는 점이었음.

But

SPRING Security - WebSecurityConfigurerAdapter제목없음deprecated 관련 이슈 및 권장 방식 적용에서 해답을 찾음.

바로바로 Bean으로 등록만 하면 된다는 것!! 이렇게 간단한 걸 모르니 한참 헤맸음...😅

오히려 좋아😁

 

passwordEncoder 관련 설정 부분 설명이 필요하다면! (클릭)

2022.10.12 - [개발자 모드/Spring Security] - Spring Security passwordEncoder 설정 관련

 

Spring Security passwordEncoder 설정 관련

스펙 : Spring Boot, h2 db, jsp h2 db에 아래 표와 같이 관리자 데이터를 insert 후 id password admin 123 Spring security를 사용해서 로그인 로직 구현 중 로그인을 했는데 There is no PasswordEncoder mapped for the id "null"

precious-value.tistory.com


2-5. UserDetailsService

//@service
//@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {

    private final Logger logger = LoggerFactory.getLogger(UserDetailService.class);	//로그 관련

//    private final UserRepository userRepository;

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException{
        logger.info("userId === "+userId);
        User user = userRepository.findOneById(userId);	//로그인 시 form data에서 userId를 받아와 찾음.
        if(user == null){	//계정이 없으면 예외 메세지 출력
            throw new UsernameNotFoundException("해당 유저가 없습니다.");
        }

        ExamUser examUser = new ExamUser(user, buildAdminAuthority());

        return examUser;
    }

    private Set<GrantedAuthority> buildAdminAuthority() {	//ROLE_USER 권한 부여

        Set<GrantedAuthority> setAuths = new HashSet<GrantedAuthority>();

        setAuths.add(new SimpleGrantedAuthority("ROLE_USER"));

        return setAuths;
    }
}

혹시 필자처럼 security에 UserDetailService를 bean로 등록하고 상단에 @service 설정하면 안 됨!!!!

당연한 거지만 service 등록이 되면서 security에 설정한 bean을 찾지 못해 로그인이 안됨.🤬🤬

이런 바보 같은 허튼짓은 저만하기를...

 

@RequiredArgsConstructor을 사용해서 생성자 주입을 하는 것이 @Autowired를 사용해 필드 주입하는 것보다 스프링팀에서 권장하는 방식이며, 여러 장점이 있다고 해서 사용해볼려고 했으나.. 

Security config에서 userDetailsService에서 빈등록 시 에러가 나서 @Autowired 사용함..

이유 아시는 분 알려주시면 감사!

 

필자는 UserDetails를 커스터마이징 할 필요가 없어서

loadUserByUsername 메소드에서 EaxmUser라는 객체를 반환해줌.

//여기서 확장한 User는 UserDetails를 구현한 구현체임.
@Getter
@Setter
public class ExamUser extends User {
    String id;
    String name;

    public ExamUser(com.exam.domain.user.User user, Collection<? extends GrantedAuthority> authorities) {
        super(user.getId(), user.getPassword(), authorities);
        this.id = user.getId();
        this.name = user.getName();
    }
}

 

이와 같이 설정하면 간단한 security 로그인 구현 완성! 끝.


spring security는 정말 공부할 부분이 많은 것 같음.

해도해도 꼬리에 꼬리를 물고 공부할게 나오는 듯 👍

그래도 오늘도 열심히 하기.

최근 들은 명언 중 기억에 남아서 적어봄.

할 수 있다고 믿는 사람은 그렇게 되고,

할 수 없다고 믿는 사람도 그렇게 된다.

- 제너럴 샤를 드골 

다들 할 수 있음! 화이팅.

 

 

 

 

📢참조

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

 

Spring Security without the WebSecurityConfigurerAdapter

<p>In Spring Security 5.7.0-M2 we <a href="https://github.com/spring-projects/spring-security/issues/10822">deprecated</a> the <code>WebSecurityConfigurerAdapter</code>, as we encourage users to move towards a component-based security configuration.</p> <p

spring.io

https://velog.io/@csh0034/Spring-Security-Config-Refactoring

 

[Spring Security] Config Refactoring

WebSecurityConfigurerAdapter Deprecated, Lambda DSL 적용, Resource Filter Chain 설정

velog.io

 

728x90