ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 초보자를 위한 스프링 시큐리티 커스텀 구현하기(UserDetails, UserDetailsService, AuthenticationProvider customizing 하기)
    Study/Spring 2022. 12. 21. 16:01
    반응형

    앞서 스프링시큐리티 인증절차에 대해서 정리해 봤다.

    혹시 못보신분들은 아래 링크로 가시면 될듯

    https://cbn1218.tistory.com/10

     

    스프링 시큐리티 인증 절차

    스프링 시큐리티는 학원 프로젝트을 하면서 구현은 해봤으나, 실질적인 인증절차는 모르고 시큐리티기능을 가져다가 썼다. 이번기회에 시큐리티 인증 절차 정리겸 포스팅 해볼 예정 출처: https:

    cbn1218.tistory.com

    로그인form에서 "/login"으로 요청을 보내어 스프링시큐리티에 이미 구현한 기능을 사용하여 인증하면 좋겠지만 실제프로젝트에서는 인증부분을 직접 customizing 하여 인증절차를 진행 하는게 많기 때문에

    연습삼아 아주간단한 형태로 customizing 하여 인증절차를 진행해 보겠다.

     

    *환경

    IDE: 전자정부프레임워크기반

    DB: mariaDB, DB연결은 mybatis를 사용

     

    1.설명

    구현하기전 간단하게 UserDetails, UserDetailsService, AuthenticationProvider에 대해 알아 보자

    -UserDetails: Spring Security에서 사용자의 정보를 담는 인터페이스이다.

    -UserDetailsService: Spring Security에서 유저의 정보를 가져오는 인터페이스이다.

    -AuthenticationProvider: DB에서 가져온 정보와 input 된 정보가 비교되서 체크되는 로직이 포함되어있는 인터페이스이다.

     

    로그인form에서 /login 로 post로 보내면 시큐리티 인증 절차를 거쳐서 로그인이 되는데,

    인증절차를 관여하는 UserDetails, UserDetailsService, AuthenticationProvider 를 상속받아 customizing 할 예정이다.특이한점은 나는 마이바티스를 이용하여 mapper를 DAO를 대신해서 쓸 예정이다.

    2.구현

    (1) DB 구현

    CREATE TABLE CUSTOMMEMBER(
    ID VARCHAR(30) NOT NULL PRIMARY KEY,
    PASSWORD VARCHAR(30) NOT NULL,
    NAME VARCHAR(30) NOT NULL,
    AUTHORITY VARCHAR(30) DEFAULT 'ROLE_MEMBER',
    ENABLED INT DEFAULT '1'
    )DEFAULT CHARSET=UTF8

    (2) CustomUserDetails 구현

    보통 VO가 DB에서 가져온 정보를 담는 그릇같은역할을 하는데, 여기서  VO를 대신하여 UserDetails을 상속 받은 CustomUserDetails 클래스를 만들어 오버라이딩 메서드에 담아야될 정보들을 매칭 시킨다.

    package ddi.main.security;
    
    import java.util.ArrayList;
    import java.util.Collection;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    public class CustomUserDetails implements UserDetails{
    	
    	private String ID;
    	private String PASSWORD;
    	private String NAME;
    	private String AUTHORITY;
    	private boolean ENABLED;
    	
    	@Override
    	public Collection<? extends GrantedAuthority> getAuthorities() {
    		ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
    		auth.add(new SimpleGrantedAuthority(AUTHORITY));
    		return auth;
    	}
    	
    	@Override
    	public String getPassword() {
    		return PASSWORD;
    	}
    	
    	@Override
    	public String getUsername() {
    		return ID;
    	}
    	
    	@Override
    	public boolean isAccountNonExpired() {
    		return true;
    	}
    	
    	@Override
    	public boolean isAccountNonLocked() {
    		return true;
    	}
    	
    	@Override
    	public boolean isCredentialsNonExpired() {
    		return true;
    	}
    	
    	@Override
    	public boolean isEnabled() {
    		return ENABLED;
    	}
    	
    	public String getNAME() {
    		return NAME;
    	}
    
    	public void setNAME(String name) {
    		NAME = name;
    	}
    
    }

     

    (3)mapper.xml 구현

    여기서 주의해야 할점은 resultType을 CustomUserDetails로 설정 해야한다.

    그래야 DB에서 받은 정보를 CustomUserDetails클래스가 받을수 있기 때문이다.

    <?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="ddi.main.mapper.memberMapper">
    
    <select id="loginID" resultType="ddi.main.security.CustomUserDetails">
    	select * from CUSTOMMEMBER where ID=#{username}
    </select>
    
    </mapper>

     

    (4)memberMapper 구현

    mapper.xml 을 작성 했다면 쿼리를 불러오는 Mapper인터페이스를 작성하고 주의할점은 mapper.xml 의 select id와 

    메서드명이 일치해야 하며, return타입을 CustomUserDetails로 해줘야 한다.

    package ddi.main.mapper;
    
    import ddi.main.security.CustomUserDetails;
    import egovframework.rte.psl.dataaccess.mapper.Mapper;
    
    @Mapper("memberMapper")
    public interface memberMapper {
    	
    	public CustomUserDetails loginID(String ID);
    
    }

     

    (5)CustomUserDetailsService 구현

    UserDetailsService를 상속받아 구현 한것인데

    게시판 CRUD를 할때 mapper->service->serviceImpl->contllor 이런 절차로 진행하는데

    저기서 serviceImpl부분을 대체해서 구현한 부분이 CustomUserDetailsService라고 생각하는게 쉬울거 같다.

    아래코드 보면 mapper를 선언해주고 DB에서 사용자정보를 불러오는 메서드 부분을 선언해주는데 serviceImpl과 역할이 비슷한거 같다.

    package ddi.main.security;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    
    import ddi.main.mapper.memberMapper;
    
    public class CustomUserDetailsService implements UserDetailsService{
    	
    	@Autowired
    	private memberMapper mapper;
    
    	@Override
    	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    		
    		CustomUserDetails user = mapper.loginID(username);
    		if(username==null) {
    			throw new UsernameNotFoundException(username);
    		}
    		
    		System.out.println("CustomUserDetailsService 들어왔다!!!!!!!!!!!!!!!!");
    		return user;
    	}
    
    }

     

    (6)CustomAuthenticationProvider 구현

    CustomAuthenticationProvider는 로그인요청한 유져와 DB의 유져정보를 비교하는 실질적으로 인증을 담당하는 부분이다.

    유져정보 비교하고 맞으면  Authentication 객체로 반환하는데 하단에 보면  

    new UsernamePasswordAuthenticationToken를 생성하여 유져정보를 담는데  UsernamePasswordAuthenticationToken은 Authentication 인터페이스의 구현체이다.

    package ddi.main.security;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    public class CustomAuthenticationProvider implements AuthenticationProvider{
    	
    	@Autowired
    	private UserDetailsService userDetailsService;
    
    	@Override
    	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    		
    		String username=(String) authentication.getPrincipal();
    		String password=(String) authentication.getCredentials();
    		
    		CustomUserDetails user = (CustomUserDetails)userDetailsService.loadUserByUsername(username);
    		
    		if(!matchPassword(password, user.getPassword())) {
    			throw new BadCredentialsException("비밀번호 안맞음!!!!!");
    		}
    		
    		if(!user.isEnabled()) {
    			throw new BadCredentialsException("계정활성화 안되있음!!!!!");
    		}
    		
    		return new UsernamePasswordAuthenticationToken(username,password,user.getAuthorities());
    	}
    
    	@Override
    	public boolean supports(Class<?> authentication) {
    		return true;
    	}
    	
    	private boolean matchPassword(String loginPwd, String password) {
            return loginPwd.equals(password);
        }
    
    }

     

    (7)context-security 설정 

    나는 비밀번호 암호화는 따로 설정하지 않았기 때문에 customNoPasswordEncoder 를 설정했고,

    설정시 빼도 상관없는 부분이다.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans:beans xmlns="http://www.springframework.org/schema/security"
    				xmlns:beans="http://www.springframework.org/schema/beans"
    				xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    				xsi:schemaLocation="http://www.springframework.org/schema/security 
    									http://www.springframework.org/schema/security/spring-security-4.2.xsd
    									http://www.springframework.org/schema/beans 
    									http://www.springframework.org/schema/beans/spring-beans-4.2.xsd">
    	
    	<beans:bean id="loginSuccess" class="ddi.main.security.CustomLoginSuccessHandler" />
    	<beans:bean id="customNoPasswordEncoder" class="ddi.main.security.CustomNoPasswordEncoder"/>
        <beans:bean id="customUserDetailsService" class="ddi.main.security.CustomUserDetailsService"/>
        <beans:bean id="customAuthenticationProvider" class="ddi.main.security.CustomAuthenticationProvider"/> 
    																		
    	<http>
    	<intercept-url pattern="/login.do" access="permitAll" />
    	<intercept-url pattern="/admin.do" access="hasRole('ROLE_ADMIN')" />
    	<intercept-url pattern="/main.do" access="hasRole('ROLE_MEMBER')" />
    	
    	<form-login 
    		login-page='/login.do'
    		authentication-success-handler-ref="loginSuccess"
    	/>
    	</http>
    	
    	
        <authentication-manager> 
       		<authentication-provider ref="customAuthenticationProvider" />
         	<authentication-provider user-service-ref="customUserDetailsService">
        		<password-encoder ref="customNoPasswordEncoder"/>
        	</authentication-provider> 
    	</authentication-manager>
    	
    
    									
    </beans:beans>
    반응형

    댓글

Designed by Tistory.