ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Security] DB정보로 로그인/로그아웃하기 (2)
    DEV/Spring 2020. 8. 14. 14:21

    1. 로그인 로그아웃 처리를 해주기위해 우선 Configuration을 수정해준다.

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	@Autowired
    	CustomUserDetailService customUserDetailService;
    	
    	@Override
    	public void configure(WebSecurity web) throws Exception {
    		web.ignoring().antMatchers("/webjars/**");
    	}
    	
    	@Override
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		auth.userDetailsService(customUserDetailService);
    	}
    
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http
    			.csrf().disable()
    			.authorizeRequests()
    			.antMatchers("/", "/main","/members/loginerror","/members/joinform","/members/join","/members/welcome").permitAll()
    			.antMatchers("/securepage","/members/**").hasRole("USER")
    			.anyRequest().authenticated()
    			.and()
    				.formLogin()
    				.loginPage("/members/loginform")
    				.usernameParameter("userId")
    				.passwordParameter("password")
    				.loginProcessingUrl("/authenticate")
    				.failureForwardUrl("/members/loginerror?login_error=1")
    				.defaultSuccessUrl("/",true)
    				.permitAll()
    			.and()
    				.logout()
    				.logoutUrl("/logout")
    				.logoutSuccessUrl("/");
    	}
    	
    	//패스워드 인코더를 빈으로 등록.
    	//암호를 인코딩하거나 인코딩된 암호와 사용자가 입력한 암호가 같은지 확인할때 사용
    	@Bean
    	public PasswordEncoder encoder() {
    		return new BCryptPasswordEncoder();
    	}
    }

    SecurityConfig.java

     

    - 메소드 설명

    @Override
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		auth.userDetailsService(customUserDetailService);
    	}

    WebSecurityConfigurerAdapter가 가지고 있는 메소드 configure(AuthenticationManagerBuilder auth)를 오버라이딩 하고 있다. 해당 메소드를 오버라이딩 한 후 UserDetailsService인터페이스를 구현하고 있는 객체(customUserDetailService)를 auth.userDetailsService()메소드의 인자로 전달하고 있다.


    스프링 시큐리티 필터 중 AuthenticationFilter가 아이디/암호를 입력해서 로그인 할 때 처리해주는 필터이고 아이디에 해당하는 정보를 데이터베이스에서 읽어 들일 때 UserDetailsService를 구현하고 있는 객체를 이용한다.
    UserDetailsService는 인터페이스이고 해당 인터페이스를 구현하고 있는 빈(customUserDetailService)을 사용한다.

     

    @Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http
    			.csrf().disable()
    			.authorizeRequests()
    			.antMatchers("/", "/main","/members/loginerror","/members/joinform","/members/join","/members/welcome").permitAll()
    			.antMatchers("/securepage","/members/**").hasRole("USER")
    			.anyRequest().authenticated()
    			.and()
    				.formLogin()
    				.loginPage("/members/loginform")
    				.usernameParameter("userId")
    				.passwordParameter("password")
    				.loginProcessingUrl("/authenticate")
    				.failureForwardUrl("/members/loginerror?login_error=1")
    				.defaultSuccessUrl("/",true)
    				.permitAll()
    			.and()
    				.logout()
    				.logoutUrl("/logout")
    				.logoutSuccessUrl("/");
    	}

     

    • http.csrf().disable()는 crsf()라는 기능을 끄는 설정이다.
      • csrf는 보안 설정 중 post방식으로 값을 전송할 때 token을 사용해야하는 보안 설정입니다. csrf은 기본으로 설정되어 있는데 csrf를 사용하게 되면 보안성은 높아지지만 개발초기에는 불편함이 있다는 단점이 있습니다. 그래서 csrf 기능을 끄도록 한 것입니다.
      • disable()메소드는 http(여기에선 HttpSecurity)를 리턴합니다.
        이말은 disable().authorizeRequests()는 http.authoriazeRequests()와 같은 의미를 가집니다.
    • antMatchers("/","/main","/members/loginerror","/members/joinform","/members/join","/members/welcome").permitAll()
      • 로그인 없이 누구나 접근할 수 있는 경로를 추가했다.
    • antMatchers("/securepage","/members/**").hasRole("USER")
      • securepage와 members이하 경로엔 로그인이 필요하고 "USER"라는 권한도 가지고 있어야 접근이 가능하다.
    • formlogin : 로그인 폼을 설정한다.
      • loginPage("/members/loginform") : 로그인 페이지 경로
      • usernameParameter("userId") : 로그인 폼에서 input태그의 name이 userId로 설정되어야 함
      • passwordParameter("password") : 로그인 폼에서 password태그의 name이 password로 설정되어야 함
      • loginProcessingUrl("/authenticate") : 로그인 폼에서 아이디와 비밀번호를 받아 로그인 처리하는 경로
        * 이 경로는 직접 구현하는 것이 아니라 스프링 시큐리티 필터가 자동으로 처리해준다.
      • failureForwardUrl("/members/loginerror?login_error=1") : 로그인 실패시 포워딩하는 경로
      • defaultSuccessUrl("/",true) : 로그인 성공시 경로
      • permitAll() : 로그인 폼을 누구나 접근할 수 있도록 하기
    • logout : 로그아웃 처리 (스프링 시큐리티가 자동으로 처리)
      • logoutUrl("/logout") : /logout요청이 들어오면 세션에서 로그인 정보를 삭제한다
      • logoutSuccessUrl("/") : 로그아웃 성공시 "/"로 리다이렉트

     

    2. 로그인 처리를 위한 클래스 생성하기

     

    package securityexam.service.security;
    
    public class UserEntity {
        private String loginUserId;
        private String password;
    
        public UserEntity(String loginUserId, String password) {
            this.loginUserId = loginUserId;
            this.password = password;
        }
    
        public String getLoginUserId() {
            return loginUserId;
        }
    
        public void setLoginUserId(String loginUserId) {
            this.loginUserId = loginUserId;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    }
    

    UserEntity.java

     

     

    로그인 아이디와 권한(Role)정보를 가지는 UserRoleEntity클래스를 생성

    package securityexam.service.security;
    
    public class UserRoleEntity {
        private String userLoginId;
        private String roleName;
    
        public UserRoleEntity(String userLoginId, String roleName) {
            this.userLoginId = userLoginId;
            this.roleName = roleName;
        }
    
        public String getUserLoginId() {
            return userLoginId;
        }
    
        public void setUserLoginId(String userLoginId) {
            this.userLoginId = userLoginId;
        }
    
        public String getRoleName() {
            return roleName;
        }
    
        public void setRoleName(String roleName) {
            this.roleName = roleName;
        }
    }

    UserRoleEntity.java

     

     

    UserDbService인터페이스를 생성. 로그인한 사용자 id를 파라미터로 받아들여서
    UserEntity와 List<UserRoleEntity>를 리턴하는 메소드를 가지고 있다.

    package securityexam.service.security;
    
    import java.util.List;
    
    //스프링 시큐리티에서 필요로 하는 정보를 가지고 오는 인터페이스
    public interface UserDbService {
    	public UserEntity getUser(String loginUserId);
    	public List<UserRoleEntity> getUserRoles(String loginUserId);
    }

    UserDbService.java

     

     

    데이터베이스에서 읽어 들인 로그인 정보는 UserDetails인터페이스를 구현하고 있는 객체에 저장되어야 한다. UserDetails를 구현하고 있는 CustomUserDetails클래스를 생성합니다.

    package securityexam.service.security;
    
    import java.util.Collection;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    public class CustomUserDetails implements UserDetails {
    	private String username;
    	private String password;
    	private boolean isEnabled;
    	private boolean isAccountNonExpired;
    	private boolean isAccountNonLocked;
    	private boolean isCredentialsNonExpired;
    	private Collection<? extends GrantedAuthority>authorities;
    	
    	@Override
    	public String getUsername() {
    		return username;
    	}
    	public void setUsername(String username) {
    		this.username = username;
    	}
    	@Override
    	public String getPassword() {
    		return password;
    	}
    	public void setPassword(String password) {
    		this.password = password;
    	}
    	@Override
    	public boolean isEnabled() {
    		return isEnabled;
    	}
    	public void setEnabled(boolean isEnabled) {
    		this.isEnabled = isEnabled;
    	}
    	@Override
    	public boolean isAccountNonExpired() {
    		return isAccountNonExpired;
    	}
    	public void setAccountNonExpired(boolean isAccountNonExpired) {
    		this.isAccountNonExpired = isAccountNonExpired;
    	}
    	@Override
    	public boolean isAccountNonLocked() {
    		return isAccountNonLocked;
    	}
    	public void setAccountNonLocked(boolean isAccountNonLocked) {
    		this.isAccountNonLocked = isAccountNonLocked;
    	}
    	@Override
    	public boolean isCredentialsNonExpired() {
    		return isCredentialsNonExpired;
    	}
    	public void setCredentialsNonExpired(boolean isCredentialsNonExpired) {
    		this.isCredentialsNonExpired = isCredentialsNonExpired;
    	}
    	@Override
    	public Collection<? extends GrantedAuthority> getAuthorities() {
    		return authorities;
    	}
    	public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
    		this.authorities = authorities;
    	}
    }

    CustomUserDetails.java

     

     

    UserDetailsService인터페이스를 구현하는 CustomUserDetailsService를 생성한다.
    UserDetailsService인터페이스는 1개의 메소드만 선언하고 있는데
    loadUserByUsername(String loginId) throws UsernameNotFoundException 메소드이다.
    사용자가 로그인을 할 때 아이디를 입력하면 해당 아이디를 loadUserByUsername()메소드의 인자로 전달하고

    해당 아이디에 해당하는 정보가 없으면 UsernameNotFoundException이 발생한다.
    정보가 있을 경우엔 UserDetails인터페이스를 구현한 객체를 리턴 하게 된다.

     

    package securityexam.service.security;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    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.stereotype.Service;
    
    @Service
    public class CustomUserDetailService implements UserDetailsService {
    	
        //데이터베이스에서 로그인 아이디에 해당하는 정보를 읽어 들이기 위해서
        //UserDbService를 구현한 객체를 주입받고 있다.
    	@Autowired
    	UserDbService userdbService;
    	
    	@Override
    	public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
    		// loginId에 해당하는 정보를 데이터베이스에서 읽어 CustomUser객체에 저장한다.
            // 해당 정보를 CustomUserDetails객체에 저장한다
    		UserEntity customUser = userdbService.getUser(loginId);
    		if(customUser==null)
    			throw new UsernameNotFoundException("사용자가 입력한 아이디에 해당하는 사용자를 찾을 수 없습니다.");
    		
    		CustomUserDetails customUserDetails = new CustomUserDetails();
    		customUserDetails.setUsername(customUser.getLoginUserId());
    		customUserDetails.setPassword(customUser.getPassword());
    		
    		List<UserRoleEntity> customRoles = userdbService.getUserRoles(loginId);
    		// 로그인 한 사용자의 권한 정보를 GrantedAuthority를 구현하고 있는 SimpleGrantedAuthority객체에 담아
            // 리스트에 추가한다. MemberRole 이름은 "ROLE_"로 시작되야 한다.
    		List<GrantedAuthority> authorities = new ArrayList<>();
    		if(customRoles != null) {
    			for(UserRoleEntity customRole : customRoles) {
    				authorities.add(new SimpleGrantedAuthority(customRole.getRoleName()));
    			}
    		}
    		
    		// CustomUserDetails객체에 권한 목록 (authorities)를 설정한다.
    		customUserDetails.setAuthorities(authorities);
    		customUserDetails.setEnabled(true);
    		customUserDetails.setAccountNonExpired(true);
    		customUserDetails.setAccountNonLocked(true);
    		customUserDetails.setCredentialsNonExpired(true);
    		return customUserDetails;
    		
    	}
    }
    

    CustomUserDetailService.java

     

     

    회원 관련 처리를 하는 Service 생성 (회원가입 등)

    //회원관련 정보처리하는 서비스
    public interface MemberService extends UserDbService {
    
    }

    MemberService.java

     

    @Service
    public class MemberServiceImpl implements MemberService {
    	// 생성자에 의해 주입되는 객체이고, 해당 객체를 초기화할 필요가 이후에 없기 때문에 final로 선언하였다.
        // final로 선언하고 초기화를 안한 필드는 생성자에서 초기화를 해준다.
    	private final MemberDao memberDao;
    	private final MemberRoleDao memberRoleDao;
    	
    	// @Service가 붙은 객체는 스프링이 자동으로 Bean으로 생성하는데
        // 기본생성자가 없고 아래와 같이 인자를 받는 생성자만 있을 경우 자동으로 관련된 타입이 Bean으로 있을 경우 주입해서 사용하게 된다.
        public MemberServiceImpl(MemberDao memberDao, MemberRoleDao memberRoleDao) {
            this.memberDao = memberDao;
            this.memberRoleDao = memberRoleDao;
        }
    	
    	@Override
    	@Transactional
    	public UserEntity getUser(String loginUserId) {
    		Member member = memberDao.getMemberByEmail(loginUserId);
    		return new UserEntity(member.getEmail(),member.getPassword());
    	}
    
    	@Override
    	@Transactional
    	public List<UserRoleEntity> getUserRoles(String loginUserId) {
    		List<MemberRole> memberRoles = memberRoleDao.getRolesByEmail(loginUserId);
    		List<UserRoleEntity> list = new ArrayList<UserRoleEntity>();
    		
    		for(MemberRole memberRole : memberRoles) {
    			list.add(new UserRoleEntity(loginUserId, memberRole.getRoleName()));
    		}
    
    		return list;
    	}
    }
    

    MemberServiceImpl.java

     

     

    3. 로그인 처리를 위한 컨트롤러와 뷰 생성

    @Controller
    @RequestMapping(path = "/members")
    public class MemberController {
        // 스프링 컨테이너가 생성자를 통해 자동으로 주입한다.
        private final MemberService memberService;
    
        public MemberController(MemberService memberService){
            this.memberService = memberService;
        }
    
        @GetMapping("/loginform")
        public String loginform(){
            return "members/loginform";
        }
    
        @RequestMapping("/loginerror")
        public String loginerror(@RequestParam("login_error")String loginError){
            return "members/loginerror";
        }
    
    }

    MemberController.java

     

     

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
    <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>로그인</title>
    </head>
    <body>
    	<div>
    		<div>
    			<form method="post" action="/securityexam/authenticate">
    				<div>
    					<label>ID</label>
    					<input type="text" name="userId">
    				</div>
    				<div>
    					<label>PASSWORD</label>
    					<input type="password" name="password">
    				</div>
    				<div>
    					<label></label>
    					<input type="submit" value="로그인">
    				</div>
    			</form>
    		</div>
    	</div>
    </body>
    </html>

    members/loginform.jsp

     

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
    <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>로그인 오류</title>
    </head>
    <body>
    	<h1>로그인 오류가 발생했습니다. id나 암호를 다시 입력해주세요.</h1>
    	<a href="/securityexam/members/loginform">login</a>
    </body>
    </html>

    members/loginerror.jsp

     

     

    url과 데이터베이스정보(암호화된 비밀번호:1234)를 입력하고 로그인하면 main page로 이동하고

    securepage도 잘 접근이 되는 것을 확인할 수 있다.

     

    http://localhost:8080/securityexam/members/logout 을 입력하면 로그아웃이 되는데,

    로그아웃을 처리하는 기능을 하나도 구현하지 않았지만 로그아웃이 되는 이유는
    이미 로그아웃을 처리하는 필터가 동작하고 있기 때문이다.

    댓글

Designed by Tistory.