-
[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 을 입력하면 로그아웃이 되는데,
로그아웃을 처리하는 기능을 하나도 구현하지 않았지만 로그아웃이 되는 이유는
이미 로그아웃을 처리하는 필터가 동작하고 있기 때문이다.'DEV > Spring' 카테고리의 다른 글
전자정부프레임워크기반 게시판 만들기 (1) 시작 (1) 2020.12.28 [Spring Security] 회원가입하기 (0) 2020.08.20 [Spring Security] DB정보로 로그인/로그아웃하기 (1) (0) 2020.08.13 [Spring Security] 스프링 시큐리티 설정 (0) 2020.07.15 [Spring Security] 스프링 시큐리티 개념 (0) 2020.07.15 - http.csrf().disable()는 crsf()라는 기능을 끄는 설정이다.