카테고리 없음

Spring security + Mybatis + gradle + MariaDB로그인 구현

시루덖 2023. 11. 9. 23:23

 

보초라 야매로 작성했는데 잘 되실지는 모르겠습니다.

 

 

 

 

 

gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	// MYSQL
	implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.4'

	// MyBatis
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
	// log4jdbc
		implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'

	//security
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
	testImplementation 'org.springframework.security:spring-security-test'

}

 

 

 

 

application.properties 설정

spring.datasource.url=jdbc:log4jdbc:mariadb://localhost:3306/DB이름?serverTimezone=UTC&characterEncoding=UTF-8&allowMultiQueries=true
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.username= 데이터베이스 유저 아이디
spring.datasource.password= 데이터베이스 유저 패스워드

mybatis.mapper-locations=/mapper/**/*.xml   <- 매퍼를 넣을 경로
mybatis.configuration.map-underscore-to-camel-case=true

 

유저 아이디 root로 하면 안되더라구요 .

그래서 새로 DB유저를 생성한 후 권한을 주었습니다.

 

 

 

 

 

 

 

securityFilterChain

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig {

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


    //유저 서비스
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        return http
                .authorizeRequests()
                    .antMatchers("/login","/"   /*"/members/**"*/).permitAll()
                    .antMatchers("/bread/**").permitAll()
                    .antMatchers("/static/**","/assets/**","/css/**","/imgs/**","/js/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .loginProcessingUrl("/login")
                    .usernameParameter("memberid")
                    .passwordParameter("pwd")
                    .successHandler(new CustomLoginSuccessHandler())
                    .failureHandler(new CustomLoginFailHandler())
                    .and()
                .logout()
                    .logoutUrl("/logout")
                    .invalidateHttpSession(true)
                    .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                    .and()
                .csrf().disable()
                .build();

    }


}

 

- antMatchers (" 허용할 경로 ")    .permitAll()   <- 누구나 접근 가능

 

 

 

formLogin()

           .loginPage("/ 커스텀 로그인페이지 경로")

           .loginPage("/ form 서브밋 url")

           .usernameParameter(" html의 로그인 아이디 name 값")

           .usernameParameter(" html의 로그인 아이디 password 값")

           .~~핸들러( 핸들러 커스텀하실거면 넣으시면됨 )

 

 

등등 방법이 많으니 검색하셔서 입맛대로 고치시면됨  

 

 

 

 

프로젝트이름Application (이건 안하셔두됨)

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})

 

저는 처음 페이지 접근시 로그인페이지로 갈 필요가 없어서

(exclude = {SecurityAutoConfiguration.class}) 를 넣어주었습니다.

 

 

 

 

 

MemberVO 

@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberVO implements UserDetails {
    private String membercode;  //회원코드
    private int authority;   //권한식별
    private String memberid;    //아이디
    private String pwd; //비밀번호
    private String memberemail; //이메일
    private String memberaddr;  //기본주소
    private String memberdaddr; //상세주소
    private String portalcode;  //우편번호
    private String creatdate;   //생성일
    private String phone;   //휴대전화

    //권한
    private List<String> auths;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String auth : this.auths) {
            authorities.add(new SimpleGrantedAuthority(auth));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.pwd;
    }

    @Override
    public String getUsername() {
        return this.memberid;
    }

    @Override
    public boolean isAccountNonExpired() {  //계정 만료 여부
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {   //계정 잠김 여부
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {  //비밀번호 만료 여부
        return false;
    }

    @Override
    public boolean isEnabled() {    //계정의 활성화 여부
        return false;
    }
}

 

멤버 VO입니다.   implement UserDetails를 해주었습니다.

님들 DB에 맞는 컬럼으로 작성해주세요.

 

 

 

 

CustomUserDetailsService

@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserService userService;

    @Override
    public UserDetails loadUserByUsername(String memberid) throws UsernameNotFoundException {
        MemberVO memberVO = userService.getUserAccount(memberid);

        if(memberVO == null){
            throw new UsernameNotFoundException("유효하지 않는 로그인 정보입니다.");
        }

        return memberVO;



    }
}

 

UserService는 조금 내리시면있어요

 

 

 

CustomAuthenticationProvider

@RequiredArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final CustomUserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String memberid = authentication.getName();
        String pwd = (String) authentication.getCredentials();

        MemberVO memberVO = (MemberVO) userDetailsService.loadUserByUsername(memberid);

        if(!passwordEncoder.matches(pwd,memberVO.getPwd())){
            throw  new BadCredentialsException("비밀번호가 일치하지 않습니다.");
        }

        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

        return new UsernamePasswordAuthenticationToken(memberid,pwd,authorities);

    }

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

 

 

List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));  // ROLE_   <- 이게 안붙으면 인식을 못하더라구요

 

이부분은 권한 줄 방식을 입맛대로 하시면될듯 

(DB에서 권한 조회 해 오시면될듯해요)

 

 

 

 

 

UserMapper

@Mapper
public interface UserMapper {
    MemberVO getUserAccount (String memberid);

}

 

 

 

UserService

@RequiredArgsConstructor
@Service
public class UserService implements UserMapper{

    private final UserMapper userMapper;
    @Override
    public MemberVO getUserAccount(String memberid) {
        return userMapper.getUserAccount(memberid);
    }
}

 

 

 

 

 

 

Mybatis User Mapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="패키지.UserMapper">

    <select id="getUserAccount" resultType="패키지.MemberVO">
        SELECT *
        FROM member
        WHERE memberid = #{memberid};
    </select>


</mapper>

 

 

 

------------------------------------- 회원가입 처리 ---------------------------------------

 

 

MemberMapper

@Mapper
public interface MemberMapper {

    void saveMember(MemberVO memberVO);

}

 

멤버 저장을 위한 멤버 매퍼입니다.

 

 

 

 

MemberService

@RequiredArgsConstructor
@Service
public class MemberService implements MemberMapper{
    private final MemberMapper memberMapper;


    @Override
    public void saveMember(MemberVO memberVO) {
        memberMapper.saveMember(memberVO);
    }
}

 

멤버 서비스입니다. 

 

 

 

 

 

MemberController

@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
    private final BCryptPasswordEncoder passwordEncoder;
    private final MemberService memberService;


    @GetMapping("/test")
    @ResponseBody
    public String joinMember(){
        MemberVO memberVO = new MemberVO();
        memberVO.setMembercode("1"); <-이부분은 제가 따로 지정한거라 똑같이 하실필요x
        memberVO.setAuthority(1); <-이부분은 제가 따로 지정한거라 똑같이 하실필요x
        memberVO.setMemberid("admin");
        memberVO.setPwd(passwordEncoder.encode("admin1234"));

        memberService.saveMember(memberVO);
        return "값이 저장되었습니다.";
    }


}

 

로그인 테스트를 위한 어드민 계정을 생성해주는 컨트롤러입니다.

화면에 값이 저장되었다는 String 값을 출력해주기 위해 ResponseBody를 사용하였습니다.

 

spring security는 암호화된 비밀번호가 아니면 로그인을 할 수 없기 때문에

password를 암호화 시켜준 다음 저장합니다.

 

님들의 vo에 맞게 값을 세팅시켜주세요.

 

 

 

 

 

Mybatis Mapper 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="패키지.MemberMapper">

    <insert id="saveMember" parameterType="패키지.MemberVO">
        INSERT INTO member (membercode, authority, memberid, pwd, creatdate)
        VALUES(#{membercode},#{authority},#{memberid},#{pwd},NOW())
    </insert>


</mapper>

 

mapper namespace 는 아까 만든 MemberMapper가 있는 경로를 넣으시면됩니다.

 

parameterType 에 Vo가 있는 경로를 넣으시면됩니다.

 

 

 

 


 

로그인 html (Thymeleaf)

 <form action="/login" id="form" method="post">
                    <div class="col mb-2 text-center">
                        <input class="ct-input" placeholder="아이디" name="memberid">
                    </div>
                    <div class="col mb-4 text-center">
                        <input class="ct-input" placeholder="비밀번호" name="pwd">
                    </div>

                    <div class="text-center fb mt-5" th:if="${error}">
                        <span th:text="${exception}"></span>
                    </div>

                <div class="col mb-4 text-center mt2">
                    <button class="login-btn" type="submit"> 로그인 </button>
                </div>
                </form>

 

 

저는 로그인 실패시 에러메시지를 보여주기 위해 

<div class="text-center fb mt-5" th:if="${error}">
                        <span th:text="${exception}"></span>
                    </div>

 

이부분을 썼습니다.

 

 

 

 


 

부가적 처리

 

 

 

 

 

CustomLoginFailHandler

@Slf4j
public class CustomLoginFailHandler extends SimpleUrlAuthenticationFailureHandler {


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String errormsg = null;
        if(exception instanceof BadCredentialsException || exception instanceof InternalAuthenticationServiceException){
            errormsg = "아이디 또는 미밀번호가 맞지 않습니다. 다시 확인해 주세요.";
        }else if (exception instanceof InternalAuthenticationServiceException){
            errormsg = "내부적으로 발생한 시스템 문제로 인해 요청을 처리할 수 없습니다. 관리자에게 문의해주세요.";
        }else if (exception instanceof UsernameNotFoundException){
            errormsg = "존재하지 않는 아이디 입니다.";
        }else{
            errormsg = "알 수 없는 이유로 로그인에 실패하였습니다. 관리자에게 문의하세요.";
        }
        log.info("error msg = {}", errormsg);

        errormsg = URLEncoder.encode(errormsg,"UTF-8");
        setDefaultFailureUrl("/login?error=true&exception="+errormsg);
        super.onAuthenticationFailure(request,response,exception);
    }
}

 

블로그 참고하여 작성했습니다.

 

에러메시지를 /login에 넘겨

 

@Controller
public class LoginViewController {

    @GetMapping("/login")
    public String login(@RequestParam(value = "error", required = false)String error,
                        @RequestParam(value = "exception", required = false) String exception, Model model){
        model.addAttribute("error", error);
        model.addAttribute("exception", exception);

        return "/login";
    }


}

 

컨트롤러에서 받은 후 모델로 보내줍니다.

 

 

<div class="text-center fb mt-5" th:if="${error}">
                        <span th:text="${exception}"></span>
                    </div>

 

아까 이 부분이 출력

 

 

CustomLoginSuccessHandler 

@Slf4j
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("userId = {}",authentication.getName());
        HttpSession session = request.getSession();
        session.setAttribute("name", authentication.getName().toString());
        //여기서 권한별로 로그인처리 해주면될듯ㅇㅇ
        response.sendRedirect("/");
    }
}

 

로그인 성공 후 처리

 

 

만드셨으면 아까 config에 등록해주시면됩니다.

  @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        return http
                .authorizeRequests()
                    .antMatchers("/login","/"   /*"/members/**"*/).permitAll()
                    .antMatchers("/bread/**").permitAll()
                    .antMatchers("/static/**","/assets/**","/css/**","/imgs/**","/js/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .loginProcessingUrl("/login")
                    .usernameParameter("memberid")
                    .passwordParameter("pwd")
                    .successHandler(new CustomLoginSuccessHandler())
                    .failureHandler(new CustomLoginFailHandler())
                    .and()
                .logout()
                    .logoutUrl("/logout")
                    .invalidateHttpSession(true)
                    .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                    .and()
                .csrf().disable()
                .build();

    }