본문 바로가기

study/java

[Spring Security] DB정보로 로그인/로그아웃하기 (2)

 

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 : 로그인 폼을 설정한다.
  • 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를 리턴하는 메소드를 가지고 있다.

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 을 입력하면 로그아웃이 되는데,

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



출처: https://ivory-room.tistory.com/26?category=875739 [개발로 자기개발]