스프링 시큐리티 로그인, 로그아웃, 회원가입 구현 [Spring Boot + Maven]

호두·2023년 1월 11일
0

스프링 시큐리티

목록 보기
1/1
post-thumbnail

GitHub ::

https://github.com/s2ljeun/nikee/tree/main/src/main/java/com/nikeedev/nikee/security

기능

  1. 유저 권한에 따른 페이지 접속 제한(일반 멤버, 관리자)
  1. 회원가입 - 비밀번호를 암호화해서 DB에 저장

  2. 로그인 - 사용자가 입력한 비밀번호를 암호화해서 DB의 비밀번호와 대조

  3. 로그인 실패 - ID와 PW가 DB에 저장된 내용과 틀리면 에러메시지를 표시

  4. 로그아웃


파일구조

login 패키지 안에 SecurityConfig , LoginPwValidator, LoginSuccess/FailHandler 클래스를 구현. 여기에 로그인할 때 LoginController, 회원가입할 때 MemberController를 사용한다.

*처음 시큐리티를 적용하며 패키지명을 login이라고 했는데 config, security 등의 패키지명이 더 적절해보인다.


SecurityConfig.java

@SuppressWarnings("deprecation")
@Configuration // 스프링 환경 세팅을 돕는 어노테이션
@EnableWebSecurity // 스프링 시큐리티 설정할 클래스라고 알려주는 어노테이션
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
    LoginIdPwValidator loginIdPwValidator;
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        		.csrf().disable() // POST방식 허용
                .authorizeRequests()
                	.antMatchers("/", "/index", "/login", "/join").permitAll() // 이 URI는 누구든 접근가능
                	.antMatchers("/admin/**").hasRole("ADMIN") // ADMIN role만 접근 가능
                	.antMatchers("/member/**").hasRole("MEMBER") // ADMIN role만 접근 가능
                	.anyRequest().authenticated() // 어떤 URI로 접근하던 인증이 필요함
                .and()
                    .formLogin()
                    .loginPage("/login")
                    .loginProcessingUrl("/loginProc") // 이 URI 호출시 스프링 시큐리티로 폼 정보를 제출 / form의 action
                    .usernameParameter("id") // 폼 input name값: default - username
                    .passwordParameter("passwd") // 폼 input name값: default - password
                    .successHandler(loginSuccessHandler()) // 로그인 성공을 다룰 핸들러
                    .failureHandler(loginFailHandler()) // 로그인 실패를 다룰 핸들러
                    .permitAll()
                .and()
                    .logout()
                    .logoutSuccessUrl("/") // 로그아웃 성공시 이동할 URL
        			.logoutRequestMatcher(new AntPathRequestMatcher("/logoutProc")); // 이 URI 호출시 로그아웃
    }
    
    //인증 예외 추가
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/js/**", "/resources/css/**", "/resources/img/**", "/resources/icon/**");
    }
    
    //입력한 ID/PW가 DB와 일치하는지 확인
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(loginIdPwValidator);
    }

    //로그인 성공 핸들러
    @Bean
    public LoginSuccessHandler loginSuccessHandler(){
        return new LoginSuccessHandler();
    }
    
    //로그인 실패 핸들러
    @Bean
    public LoginFailHandler loginFailHandler(){
        return new LoginFailHandler();
    }
}
  1. WebSecurityConfigurerAdapter를 상속받아 기본적인 security 설정을 한다.
    form로그인의 post방식 제출을 허용하기 위해 .csrf().disable()을 해주고,
    .authorizeRequests().antMatchers()로 각 URL에 접근가능한 권한을 설정해준다.

  2. .formLogin()으로 폼 방식 로그인을 활성화하고, .loginPage()로 커스텀 로그인 페이지 매핑을 지정해준다. 그 외 form과 관련된 설정을 해준다.

  3. 로그인 성공, 실패를 다룰 핸들러를 .successHandler()와 .failureHandler()로 지정해주는데, 각 인자는 하단에서 Bean으로 주입하고 있다.
    또한 configure 이름의 메소드를 override해서 유저가 입력한 정보와 DB정보가 일치하는지 확인하기 위한 loginIdPwValidator 클래스를 연결해준다.


LoginIdPwValidator

@Service
public class LoginIdPwValidator implements UserDetailsService {
	@Autowired
    private MemberMapper memberMapper;

	// DB의 pw(암호화된)와 유저가 입력한 pw를 암호화하여 자동으로 비교
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

	@Override
	public UserDetails loadUserByUsername(String insertedId) throws UsernameNotFoundException {
		// 사용자가 입력한 id가 인자로 들어옴 -> 유저id로 DTO꺼내오기
		MemberDTO mdto = memberMapper.getMemberById(insertedId);
	        
	        if (mdto == null) {
	            return null; // ID 혹은 PW가 잘못되었습니다.	            
	        }
	        
	        String passwd = mdto.getMem_passwd();
	        String roles = mdto.getMem_role();

	        return User.builder()
	                .username(insertedId)
	                .password(passwd)
	                .roles(roles)
	                .build();
	}

}

Bean으로 PasswordEncoder를 주입해주고 memberMapper를 autowired한다. loadUserByUsername메소드를 Override해 유저가 입력한 id로 dto가 존재하는지 판단하고 UserDetails 객체를 return한다. 패스워드가 일치하는지 여부는 PasswordEncoder가 자동으로 판단해준다.


LoginSuccessHandler

public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
	@Override
	public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res,
			Authentication auth) throws IOException, ServletException {
		//로그인 성공시 기본 페이지 설정
		String url = "/index";
		
		//인증된 사용자의 user객체 추출
		User user = (User) auth.getPrincipal();
		
		//user객체에서 role 목록 추출
		Collection<GrantedAuthority> authlist = user.getAuthorities();
		Iterator<GrantedAuthority> authlist_it= authlist.iterator();
		
		while(authlist_it.hasNext()) {
			GrantedAuthority authority= authlist_it.next();
			//설정되어 있는 권한 중 ROLE_ADMIN이 있다면
			if(authority.getAuthority().equals("ROLE_ADMIN")) {
				url="/admin";
			}
		}
		
		res.sendRedirect(url);
		req.getSession().setAttribute("userDetail", user);
	}
}

로그인 성공 후 처리는 SimpleUrlAuthenticationSuccessHandler를 상속받아 만든다.
로그인 성공한(인증된) 사용자의 user객체를 auth.getPrincipal()로 추출해오고, 그 안에서 권한(USER_ROLE)을 확인하기 위해 authlist를 불러온다. 권한 중 "ADMIN"이 존재한다면 url을 어드민페이지로 매핑, 그 외에는 index페이지로 매핑한다.
*여기서 권한은 가입할 때 DB에 ADMIN, MEMBER 등으로 넣어주어야 한다.

*세션에 "userDetail"이라는 이름으로 user객체를 저장해 추후 로그인 한 상태인지 판단하는 데 사용한다. 로그아웃을 진행하면 세션에서 자동으로 삭제되는 것을 확인했다.

LoginFailHandelr

public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler {
	@Override
    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
		String url = "/login?error";
		res.sendRedirect(url);
    }
}

LoginController

@Controller
public class LoginController {
	@GetMapping("/login")
	public String login(HttpServletRequest req,
						@RequestParam(value="error", required=false) String error) {
		
		if(error != null) {
			req.setAttribute("errorMsg", "아이디 또는 패스워드를 확인해주세요.");
		}
		return "login";
	}
	
}

로그인 실패 후 처리는 SimpleUrlAuthenticationFailureHandler를 상속받아 만든다.
url만 지정하고 redirect 시켜주었다.
컨트롤러에서 "/login" 매핑을 받을 때 "error" 이름의 파라메터가 존재한다면 "아이디 또는 패스워드를 확인해주세요." 문구를 띄우도록 처리했다.

MemberController

@Controller
public class MemberController {
	@Autowired
    private MemberMapper memberMapper;
	@Autowired
	private LoginIdPwValidator loginIdPwValidator;
	
	@GetMapping("/join")
	public String goJoin() {
		return "join";
	}
	
	@PostMapping("/join")
	public String joinOk(HttpServletRequest req, @ModelAttribute MemberDTO mdto) {
		//패스워드 암호화
		String encodedPasswd = loginIdPwValidator.passwordEncoder().encode(mdto.getMem_passwd());
		mdto.setMem_passwd(encodedPasswd);
		
		int res = memberMapper.insertMember(mdto);
		if(res > 0) {
			req.setAttribute("msg", "회원가입이 완료되었습니다.");
			req.setAttribute("url", "/index");
		}else {
			req.setAttribute("msg", "회원가입에 실패했습니다.");
			req.setAttribute("url", "/join");
		}
		
		return "message";
	}
}

BCryptPasswordEncoder()가 사용자가 입력한 비밀번호와 DB의 비밀번호를 비교하려면 DB에 있는 비밀번호부터 암호화처리가 되어있어야 한다. 따라서 회원가입할 때 loginIdPwValidator의 passwordEncoder() 메소드를 사용해 DB에 저장한다. (여기서는 굳이 메소드를 불러오지 않고 new BCryptPasswordEncoder()를 생성해 처리해줘도 될 것 같다.)


레퍼런스 1
레퍼런스 2

profile
web developer

0개의 댓글