본문 바로가기

공부해야할 것/java

[SpringBoot] 웹 개발(8) - 로그인 기능 만들기(3)(로그인 관련 설정)

반응형

2023.11.11 - [공부해야할 것/java] - [SpringBoot] 웹 개발(7) - 로그인 기능 만들기(2)(회원 가입)

 

[SpringBoot] 웹 개발(7) - 로그인 기능 만들기(2)(회원 가입)

2023.11.11 - [공부해야할 것/java] - [SpringBoot] 웹개발(6) - 로그인 기능 만들기(1) [SpringBoot] 웹개발(6) - 로그인 기능 만들기(1) 2023.11.09 - [공부해야할 것/java] - [SpringBoot] 웹 프로젝트 만들기(5) - thymleaf

kkkkims.tistory.com

여기서 이어진다.

 


  1. 계정이 저장 될 DB 테이블
  2. 로그인 페이지 / 회원가입페이지
  3. 회원가입 기능 
  4. 로그인 기능 (관련되어 security 설정)
  5. 로그인시 올바른 정보를 입력해서 체크하는 기능 -> 올바른 정보를 입력했을때만 로그인하도록
  6. 권한체크(선택)

 

여기서 왜  로그인기능하고 로그인시 올바른 정보 입력하는거가 체크되있냐면...  글이 길어질꺼같기 때문이다. 이 글에서는 3가지 정도만 할 예정이다.

1. 무조건 로그인을 해야지 다른 페이지들을 이용할수 있도록 설정

2. 입력한 로그인 정보를 맞는지 틀린지 체크할 부분(다음 글에서 할내용이다)

3. 로그인 성공할때 어떻게 해야지

4. 로그인 실패시 어떻게 해야지

 

갯수로 봤을때는 적어보이는데 할께 은근이 많다. 우선 무조건 로그인을 해야지 다른 페이지들을 이용할수 있도록 설정을 진행해보도록 하자 현재 스프링부트를 그냥 켜보자.

그냥 메인페이지로 이동하는 것을 확인할수 있다. 그렇다면...이제 무조껀 이 사이트를 처음 들어가려면 지난번에 만들어 두었던 로그인페이지부터 나오겠금하려면 어떻게 해야할까 이는 이전에 만들어 두었던 MainController.java 부분을 건들이면 된다. 지난번의 MainController .java부분을 다음과 같이 바꿔보자

package com.kkkkim.forStudy.main.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MainController {

    @RequestMapping ("/")
    public String main(Model modele) {
        return "redirect:auth/login";
    }
    
}

이제 킬때마다 로그인 페이지로 이동함을 확인 할 수 있다. 

그리고 이제 로그인을 하고나서 무조건 해당 로그인한 이가 권한있는지 체크하는 기능이 있는 class를 만들것이다. 구체적 권한 체크하는것은 추후에 진행할 예정이다.  우선 auth폴더에 components란 폴더를 만들고 해당 폴더내에 CustomAuthorizationManager.java 를 만들자. 그리고 이를 다음과 같이 만들어 주자.

package com.kkkkim.forStudy.auth.components;

import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.security.access.AccessDeniedException;
import java.util.function.Supplier;

@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
    @Override
    public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        AuthorizationDecision decision = check(authentication, object);
        if (decision != null && !decision.isGranted()) {
            throw new AccessDeniedException("Access Denied");
        }
    }

    /** 권한 체크하는지 증명하는 함수 **/
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        AuthorizationDecision decision;
        boolean granted = false;
        decision = new AuthorizationDecision(granted);
        return decision;
    }
}

 

이글에서는 로그인후 권한까지 체크하는 부분까지는 안할것이기 때문에 decision을 false로 두었다. 무조건 CustomAuthorizationManager를 타면 false를 리턴하면서 페이지에 접근 못하겠금말이다.

이제 securityConfig로 돌아가서 다음과 같이 써보자

package com.kkkkim.forStudy.config;

import com.kkkkim.forStudy.auth.components.CustomAuthorizationManager;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
public class SecurityConfig {

    @Autowired
    CustomAuthorizationManager customAuthorizationManager;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        http.csrf((csrf)->csrf
                .disable()
        );

/**
 * 로그인 페이지 설정
 *
 */

        http.authorizeHttpRequests((requests) -> requests.anyRequest().access( customAuthorizationManager)
        );

        return http.build();
    }
}

모든 url은 customAuthorizationManager를 타겠금 해놨다. 이제한번 스프링을 켜보겠는가

이렇게 뜨면 설정이 잘적용된것이다. 이제 auth/login과 auth/join 페이지는 customAuthorizationManager를 안타겠금해볼까? 다음과 같이 SecurityConfig 설정을 바꿔보자

package com.kkkkim.forStudy.config;

import com.kkkkim.forStudy.auth.components.CustomAuthorizationManager;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
public class SecurityConfig {

    @Autowired
    CustomAuthorizationManager customAuthorizationManager;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        http.csrf((csrf)->csrf
                .disable()
        );

/**
 * 로그인 페이지 설정
 *
 */

        http.authorizeHttpRequests((requests) -> requests
                .requestMatchers(
                        mvcMatcherBuilder.pattern("/auth/**"),
                        mvcMatcherBuilder.pattern("/")).permitAll()
                        .anyRequest().access( customAuthorizationManager)
        );

        return http.build();
    }
}

 

다시켜보겠는가?

저거 복붙한다고 잘될줄 알았지? (코딩을 복붙으로 날먹할수 있을줄 알았는가)

왜 안돼냐면... 웹 리소스 파일 불러올때도 cutomAuthorizationManager를 타겠금 해놨기 때문이다. 그렇다면 웹리소스랑 관련된 모든 파일을 등록하여 해당 리소스를 불러올때는 로그인 안해도 불러오겠금해야한다. 이제 아까 securityConfig를 다음과 같이 바꿔주자

package com.kkkkim.forStudy.config;

import com.kkkkim.forStudy.auth.components.CustomAuthorizationManager;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
public class SecurityConfig {

    @Autowired
    CustomAuthorizationManager customAuthorizationManager;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        http.csrf((csrf)->csrf
                .disable()
        );

/**
 * 로그인 페이지 설정
 *
 */

        http.authorizeHttpRequests((requests) -> requests
                .requestMatchers(
                        mvcMatcherBuilder.pattern("/auth/**"),
                        mvcMatcherBuilder.pattern("/"),
                        mvcMatcherBuilder.pattern("/public/**"),
                        mvcMatcherBuilder.pattern("/dist/**"),
                        mvcMatcherBuilder.pattern("/assets/**"),
                        mvcMatcherBuilder.pattern("/static/**"),
                        mvcMatcherBuilder.pattern("/js/**"),
                        mvcMatcherBuilder.pattern("/css/**"),
                        mvcMatcherBuilder.pattern("/node_modules/**"),
                        mvcMatcherBuilder.pattern("/src/**")).permitAll()
                        .anyRequest().access( customAuthorizationManager)
        );

        return http.build();
    }
}

스프링을 다시켜보면 css도 다 잘 적용됨을 확인할 수 있을것이다. 이제 auth폴더밑에 components 폴더밑에 세가지 파일을 추가해보자

 

  • CustomAuthenticationProvider.java
  • CustomAuthenticationFailureHandler.java
  • CustomAuthenticationSuccessHandler.java

이파일들은 만들어 놓고 차후에 작성해줄것이다. 우선 이전에 cutomerUserDetailService의 loadUserByUsername함수를 null로 작성던 것을 기억하는가 그부분을 실제 사용하겠금 바꿔볼 것이다. 그러면 해당부분에 대해 username만 입력하면 해당 계정정보를 불러오는 sql이 필요할 것이다. AuthRepository로 이동하자. 거기에 userDetail을 return하는 함수를 하나 추가해주자.

package com.kkkkim.forStudy.auth.repository;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Repository;

@Repository
public interface AuthRepository {

	/** 회원정보 불러오기 **/
    public UserDetails getUserDetails(String username);
    
    /** 회원가입 void **/
    public void joinAccount(UserDetails userDetail);

}

 

이제 저 함수와 매핑할 sql문을 xml파일에 추가해주자

지난번에 만든 AUTH_SQL.xml파일에 select 구문을 추가한다.

    <select id="getUserDetails"
            resultType="com.kkkkim.forStudy.auth.dao.CustomUserDetail">
    SELECT
        acct_uid
        ,acct_usr_id as 'username'
        ,acct_usr_pwd as 'password'
        ,acct_usr_email
        ,acct_fail_cnt
        ,acct_lock_yn
        ,acct_use_yn
    FROM
        accountinfo
    WHERE
        acct_usr_id = #{username}

    </select>

 

이제 cutomUserDetailService에서 loadUserByUsername 함수를 잘 바꿔주자.

package com.kkkkim.forStudy.auth.service;


import com.kkkkim.forStudy.auth.dao.CustomUserDetail;
import com.kkkkim.forStudy.auth.repository.AuthRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service("customUserDetailService")
public class CustomUserDetailService implements UserDetailsService {

    @Autowired
    private AuthRepository authRepository;

    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        CustomUserDetail userDetails = (CustomUserDetail) authRepository.getUserDetails(username);
         if (userDetails == null) {
            throw new UsernameNotFoundException("유효하지 않는 로그인 정보입니다.");
        }
        List<SimpleGrantedAuthority> newVal = new ArrayList<>();
        SimpleGrantedAuthority simpleAuth =new SimpleGrantedAuthority("role_admin");
        newVal.add(simpleAuth);
        userDetails.setAuthorities(newVal);
       

        return userDetails;
    }

    public void joinNewUser(CustomUserDetail userDetails) throws Exception {

        String encryptPwd = passwordEncoder.encode(userDetails.getAcct_usr_pwd());
        userDetails.setAcct_usr_pwd(encryptPwd);
        authRepository.joinAccount(userDetails);
    }
}

 

이제 이 함수를 CustomAuthenticationProvider에서 사용기전에 securityConfig.java를 설정해주어야한다. 로그인하는 부분에 대해서 각 필요한 파라메터와 로그인성공 / 실패 시 무엇을 할것인지 말이다. 해당부분에 해당하는게 http.form로그인 부분의 람다함수이다. securityconfig의 전체 구문은 다음과 같다.

package com.kkkkim.forStudy.config;

import com.kkkkim.forStudy.auth.components.CustomAuthorizationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import com.kkkkim.forStudy.auth.components.CustomAuthenticationSuccessHandler;
import com.kkkkim.forStudy.auth.components.CustomAuthenticationFailureHandler;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
public class SecurityConfig {
    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    @Autowired
    private CustomAuthorizationManager customAuthorizationManager;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        http.csrf((csrf)->csrf
                .disable()
        );


/**
 * 로그인 페이지 설정
 *
 */
        http.formLogin((form) -> form
                .loginPage("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/auth/process")
                .successHandler(customAuthenticationSuccessHandler).failureHandler(customAuthenticationFailureHandler)


        );

        http.authorizeHttpRequests((requests) -> requests
                .requestMatchers(
                        mvcMatcherBuilder.pattern("/auth/**"),
                        mvcMatcherBuilder.pattern("/"),
                        mvcMatcherBuilder.pattern("/public/**"),
                        mvcMatcherBuilder.pattern("/dist/**"),
                        mvcMatcherBuilder.pattern("/assets/**"),
                        mvcMatcherBuilder.pattern("/static/**"),
                        mvcMatcherBuilder.pattern("/js/**"),
                        mvcMatcherBuilder.pattern("/css/**"),
                        mvcMatcherBuilder.pattern("/node_modules/**"),
                        mvcMatcherBuilder.pattern("/src/**")).permitAll()
                        .anyRequest().access( customAuthorizationManager)
        );

        return http.build();
    }
}

로그인을 /auth/login에서 하게되면 Authentication란 객체에 로그인 정보가 담긴다.

 

당장은 빨간불이 뜨겠지만 냅두고 당장 우리가 아까 만든 파일중 CustomAuthenticationSuccessHandler 파일로 이동하자 해당 파일은 로그인을 성공했을때 무엇을 할까이다. 지금당장은 다음과 같이 작성해주자

package com.kkkkim.forStudy.auth.components;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;


@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("우왕 로그인 성공이다제");
        HttpSession session = request.getSession();
        // 사용자가 세션에 있는지 확인

        // 로그인 성공한 사용자의 세션을 세션 목록에 추가

        session.setAttribute("adm_id", authentication.getName());
        session.setAttribute("usr_id", authentication.getName());
        response.sendRedirect("/test2");
    }
}

로그인을 성공하면 /test2로 이동하겠금되어있다. 이제 다음 CustomAuthenticationFailureHandler 도 만들어주자 이아이도 CustomAuthenticationSuccessHandler 와 동일한 방식으로 진행하면 된다.

package com.kkkkim.forStudy.auth.components;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("로그인 실패 했습니다.");
        response.sendRedirect("/auth/login");
    }
}

 

마지막으로 CustomAuthenticationProvider을 설정해 줄것이다. 해당글에서는 아이디만 있으면 당장 로그인이 되도록하려한다. 비밀번호 비교하는건 다음 글에서 할 예정이다. CustomAuthenticationProvider.java를 다음과 작성해주자

package com.kkkkim.forStudy.auth.components;

import com.kkkkim.forStudy.auth.dao.CustomUserDetail;
import com.kkkkim.forStudy.auth.service.CustomUserDetailService;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Resource(name = "customUserDetailService")
    private CustomUserDetailService customUserDetailService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        CustomUserDetail customUserDetails = (CustomUserDetail) customUserDetailService.loadUserByUsername(authentication.getName());
        Collection<SimpleGrantedAuthority> authorities = customUserDetails.getAuthorities();
        return new UsernamePasswordAuthenticationToken(username, password, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

이제 김에...아까 무조건 타면 false로 타겟금한 CustomAuthorizationManager도 바꿔주자 해당 자바파일에 check 함수를 다음과 같이 바꿔주자

    /** 권한 체크하는지 증명하는 함수 **/
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        AuthorizationDecision decision;
        Collection<? extends GrantedAuthority> bb=authentication.get().getAuthorities();
        boolean granted = false;
        if(bb.size()>0){
            granted = true;
        }
        decision = new AuthorizationDecision(granted);
        return decision;
    }

일단 권한이 있으면 무조건 true를 날리겠금해놓았다.

이제 login.html파일로 가서 로그인 폼부분을  바꿔주자 해당부분에 action과 method를 바꿔주면 된다. action에는 아까 securityConfig에서 loginProcessingUrl로 설정해놓은 /auth/process로 설정해준다

<form id="formAuthentication" class="mb-3"  method="post" action="/auth/process">

이제 스프링 부트를 켜서 로그인을 확인해보자

없는 아이디를 치면 CustomAuthenticationFailureHandler  타면서 spring로그단에 로그인 실패했다고 뜨는것을 확인할수있다.

반면에 있는 아이디를 치면 비밀번호를 아무거나 치더라도 로그인이 성공하면서 test2페이지로 이동함을 확인할 수 있을것이다.

만약에 login이 계속 실패가 뜬다면 html폼안에 input name이 'usename'으로 되있는지 확인해라 만약 사용자아이디를 입력하는 input값의 name이 다른걸로 바뀌어 있다면 해당 변수를 securityConfig.java 파일의 usernameParameter부분에 해당 변수값을 입력해주면된다.

 

반응형