본문 바로가기

Spring 수업 정리

Spring 7일

시큐리티 관련 디펜던시 추가

- spring-security core, web, config, taglibs  5.5.3

 

● 시큐리티 설정을 위해 config에 SecurityInitalizer 와 SecurityConfig를 생성해줌

- SecurityInitalizer 에 AbstractSecurityWebApplicationInitializer 를 상속받아야 시큐리티 관련 필터들이 활성화 됨.

- SecurityConfig에서 객체를 생성하고 필요한 설정세팅.

@Slf4j
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	// 비밀번호 암호화 객체 PasswordEncoder 빈 생성
	@Bean
	public PasswordEncoder bcPassWordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Bean
	// SuccessHandler 객체 빈 생성 => 사용자 커스텀 객체
	public AuthenticationSuccessHandler authSuccessHnadler() {
		return new LoginSuccessHandler();
	}
	
	// FailureHandler 객체 빈 생성 => 사용자 커스텀 객체 
	@Bean
	public AuthenticationFailureHandler authFailureHandler() {
		return new LoginFailureHandler();
	}
	
	// UserDetail 객체 빈 생성 => 사용자 커스텀 객체
	@Bean
	public UserDetailsService customDetailsService() {
		return new CustomAuthUserService();
	}
 }

 

 

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 화면에서 설정되는 권한에 따른 주소 맵핑 설정
		// 화면에서 오는 로그인 정보 구성
		// csrf() : 공격에 대한 설정 풀기
		http.csrf().disable();
		
		// 권한 승인 요청 
		// antMatchers : 접근을 허용하는 경로
		// permitAll : 누구나 접근이 가능한 경로 
		// hasRole('권한') : 해당 권한 확인
		// authenticated() : 인증된 사용자만 가능한 경로
		// ADMIN > MANAGER > USER 
		
		http.authorizeRequests().antMatchers("/user/list").hasRole("ADMIN")
		.antMatchers("/","/user/login","/user/register","/board/list","/board/detail","/upload/**","/comment/**","/resources/**").permitAll()
		.anyRequest().authenticated();
		
		// 로그인 페이지 구성 : id => email / pw => pwd 
		// Controller에 주소요청 맵핑은 같이 있어야 함. (필수) 
		http.formLogin()
			.usernameParameter("email")
			.passwordParameter("pwd")
			.loginPage("/user/login")
			.successHandler(authSuccessHnadler())
			.failureHandler(authFailureHandler());
		
		
		// 로그아웃 구성 : method ="post"
		http.logout()
			.logoutUrl("/user/logout")
			.invalidateHttpSession(true)
			.deleteCookies("JSESSIONID")
			.logoutSuccessUrl("/");         
  }

 

csrf 설정을 따로 해두지않았기떄문에 일단 disable해서 공격에 대한 설정을 풀어둠.

페이지별 필요한 권한을 설정해주고, 로그인과 로그아웃 페이지를 구성해줌. 

 

서블릿 Config 설정부분에도 컴포넌트 스캔을 읽을 수 있도록 시큐리티도 추가해줌.

@ComponentScan(basePackages = {"com.ezen.spring.controller","com.ezen.spring.service","com.ezen.spring.handler","com.ezen.spring.security"})

 

시크리티 패키지를 따로만들어서 로그인 성공,실패 핸들러와 CustomerAuthUserService()를 만들어줌

 

유저와 권한 DB를 생성 

-- user table
create table user(
email varchar(256),
pwd varchar(256),
nickName varchar(256),
reg_date datetime default now(),
lastLogin datetime default now(),
primary key(email));

-- 권한 테이블 (auth)

create table auth(
id bigint auto_increment,
email varchar(256) not null,
auth varchar(256) not null,
-- 외래키 지정
primary key(id),
foreign key(email) references user(email));

 

권한테이블은 하나의 이메일에 권한이 여러개들어가야하기때문에 이메일에 pk를 주지않음. 딱히 역할을 하지않지만 일단 기본키 역할을 할 수 있는 id를 만들어둠. 

●  UserVO, AuthVO를 만들고 유저 컨트롤러부터 매퍼까지 쭉 생성 많이 했으니 쭉 연결해서 생성한 코드는 생략

● view에 user폴더를 만들어서 register 회원가입 페이지 화면 구성

<jsp:include page="../layout/header.jsp" />
<div class="container-md">
	<h1>User Register Page...</h1>
	<hr>

	<form action="/user/register" method="post">
		<div class="mb-3">
			<label for="e" class="form-label">email</label>
			 <input type="text"
				class="form-control" id="e" name="email" placeholder="email...">
		</div>
		<div class="mb-3">
			<label for="p" class="form-label">password</label>
			 <input type="text"
				class="form-control" id="p" name="pwd" placeholder="password...">
		</div>
		<div class="mb-3">
			<label for="n" class="form-label">nickName</label>
			 <input type="text"
				class="form-control" id="n" name="nickName" placeholder="nickName...">
		</div>
		<button type="submit" class="btn btn-primary">JOIN</button>
	</form>
</div>
<jsp:include page="../layout/footer.jsp" />

 

인풋 태그안의 속성 name을 VO 구성에맞게 설정해줘야함.

 

만들어뒀던 헤더에 링크를 회원가입 로그인 

     	<li class="nav-item">
          <a class="nav-link" href="/user/register">회원가입</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/user/login">로그인</a>
        </li>

 

만든 컨트롤러에 GetMapping으로 register.jsp로 이동할 수 있도록하고 PostMapping 으로 form태그안의 정보를 uvo객체 로 받아와서 비밀번호를 암호화를 한다음에 서비스,다오 매퍼에 순서대로 연결해서 회원가입이 가능하도록함.

@RequiredArgsConstructor
@RequestMapping("/user/**")
@Slf4j
@Controller
public class UserController {
	private final UserService usv;
	private final BCryptPasswordEncoder bcEncoder;
	
	// mapping과 jsp의 경로가같다면 void 처리가능
	@GetMapping("/register")
	public void register() {}
	
	@PostMapping("/register")
	public String register(UserVO uvo) {
		
		log.info(">>> register UserVO > {}", uvo);
		// encode : 암호화
		uvo.setPwd(bcEncoder.encode(uvo.getPwd()));	
		int isOk = usv.register(uvo);
		return "redirect:/";	
	}
}

 

● 서비스에서 다오, 매퍼로 연결 할 때  받아온 uvo를 이용해서 user에 insert해주고 회원가입과 동시에 권한도 주기위해서 

UserVO 객체의 회원아이디인 email를 가져와서 권한을 insert해줌

	@Transactional
	@Override
	public int register(UserVO uvo) {
		int isOk = udao.insert(uvo);
		return udao.insertAuthInit(uvo.getEmail());
	}

 

<insert id="insert">
	insert into user(email,pwd,nickName) values(#{email},#{pwd},#{nickName})
</insert>
<!-- 회원가입 후 권한 부여 -->
<insert id="insertAuthInit">
	insert into auth(email,auth) values(#{email}, 'ROLE_USER')
</insert>

 

  회원가입해서 들어간 정보를 가지고 로그인이 가능하도록 로그인 화면을 구성

<jsp:include page="../layout/header.jsp" />
<div class="container-md">
	<h1>User Login Page...</h1>
	<hr>

	<form action="/user/login" method="post">
		<div class="mb-3">
			<label for="e" class="form-label">email</label>
			 <input type="text"
				class="form-control" id="e" name="email" placeholder="email...">
		</div>
		<div class="mb-3">
			<label for="p" class="form-label">password</label>
			 <input type="text"
				class="form-control" id="p" name="pwd" placeholder="password...">
		</div>
		<button type="submit" class="btn btn-primary">Login</button>
	</form>
	
</div>
<jsp:include page="../layout/footer.jsp" />

 

●  login.jsp의 form태그안에 있는 정보들은 필터가 가져가서 토큰을 생성해서 UserDetail Service에서 DB랑 비교해서 로그인이 가능하도록하기에 UserDetailService의 임플먼츠인 CustomAuthUserService를 수정해줘야함.

	@Autowired
	private UserDAO udao;

 

다오를 직접연결해주고

@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// username : 로그인을 시도하는 email
		UserVO uvo = udao.selectEmail(username);
		// 아이디가 없거나 잘못 입력되면...
		if(uvo == null) {
			throw new UsernameNotFoundException(username);
		}
			
		uvo.setAuthList(udao.selectAuths(username));
		
		log.info(">>>> userDetails >> {} " , uvo);
		
		return new AuthUser(uvo);
	}

 

비밀번호는 알아서비교를해주니 아이디를 비교할 수 있게 username을 받아서 UserVO객체를 가져올 수 있도록하고  아이디가없어서 객체가 null이라면 exception을 일으킬 수 있도록함.

객체가 null이아니라면 권한을 가져와서 set해줄 수 있도록함. return을 유저정보와 권한을 같이가지고있는 AuthUser로 리턴할건데 AuthUser는 생성하지않았으니까 생성해주도록함.

 

● AuthUser 생성

@Getter
public class AuthUser extends User{
	
	private static final long serialVersionUID = 1L;
	private UserVO uvo; 

	public AuthUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
		
	}
	
	public AuthUser(UserVO uvo) {
		
		super(uvo.getEmail(), uvo.getPwd(),
				uvo.getAuthList().stream()
				.map(AuthVO -> new SimpleGrantedAuthority(AuthVO.getAuth()))
				.collect(Collectors.toList())
				);
		
		this.uvo = uvo;
	}
}

 

이메일, 비밀번호, 권한리스트를 받을 수 있도록 해야하는데 

권한리스트는 타입이다르기때문에 stream,map,collect을 사용해서 타입을 맞춰주어야함

 

● 로그인 성공 핸들러 구현

다오 직접연결해주고,  Authentication authentication 에서 받을 이메일과 경로를 정의

lastLogin 갱신, 로그인 성공시 실패 Exception 기록삭제, 로그인 하기전 Url을 연결을 순서대로 구현

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

	@Autowired
	private UserDAO udao;
    
	@Getter
	@Setter
	private String authEmail;
	
	@Getter
	@Setter
	private String authUrl;
	
	// redirect 데이터를 가지고 리다이렉트 경로로 이동하는 역할
	private RedirectStrategy redStr = new DefaultRedirectStrategy();
	
	// 세션의 캐시 정보, 경로
	private RequestCache reqCache = new HttpSessionRequestCache();
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		//1. lastLogin 기록 , 로그인 후 가야하는 경로 설정
		// authentication => username => getName()
		setAuthEmail(authentication.getName());
		int isOk = udao.updateLastLogin(getAuthEmail());
		setAuthUrl("/board/list");
		// 시큐리티에서 로그인을 시도해서 실패하면 기록이 남게 됨.
		//2. 로그인에 성공하면, 기존에 실패했던 기록을 삭제 
		// 세션 가져오기
		HttpSession ses = request.getSession();
		if(isOk == 0 || ses == null) {
			return;
		} else {
			// removeAttribute : 세션의 객체 삭제
			ses.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
		}
		//3. 로그인 직전 URL 연결
		SavedRequest saveRequest = reqCache.getRequest(request, response);
		redStr.sendRedirect(request, response,
				saveRequest != null ? saveRequest.getRedirectUrl() : getAuthUrl());
		
	}
}

 

● 로그인 실패 핸들러 구현

@Getter
@Setter
@Component
@Slf4j
public class LoginFailureHandler implements AuthenticationFailureHandler {
	private String authEmail;
	private String errorMessage;
    
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		// BadCredentialException ||  InternalAuthenticationServiceException
		setAuthEmail(request.getParameter("email"));
		
		// exception 발생시 메시지를 저장
		if(exception instanceof BadCredentialsException) {
//			setErrorMessage(exception.getMessage().toString());
			setErrorMessage("아이디 또는 비밀번호가 잘못되었습니다.");
		} else if(exception instanceof InternalAuthenticationServiceException) {
			setErrorMessage("관리자에게 문의해주세요.");
		} else {
			setErrorMessage("관리자에게 문의해주세요.");
		}
		log.info(" >>> err Msg >> {} ", getErrorMessage());
		
		request.setAttribute("email", getAuthEmail());
		request.setAttribute("errMsg", getErrorMessage());
		
		request.getRequestDispatcher("/user/login?error")
		.forward(request, response);		
	}
}

 

로그인이 실패한 아이디를 저장할 authEmail , 에러메세지를 저장할 errorMessage를 만들어주고

request.getParameter로 가져와서 Email 저장, 에러는 발생한게 아이디,비밀번호가 잘못됐을 때 날려주는 에러인

BadCredentialException ,InternalAuthenticationServiceException이 발생하면 원하는 에러메세지를 set해서 날려줄 수 있도록함.

 

  컨트롤러 login 구현 request를 받아와서 log에 에러메세지를 띄울 수 있도록하고,

RedirectAttributes로 addAttrubute해서 login.jsp에 email과 errMsg를 보낼 수 있도록함.

	@GetMapping("/login")
	public void login() {}
	
	@PostMapping("/login")
	public String login(HttpServletRequest request, RedirectAttributes re) {
		// 실제 로그인은 Security의 필터에서 가져감.
		// 로그인 실패시 다시 로그인 페이지로 돌아와 오류 메시지를 전송
		// 재 로그인을 유도
		log.info(">>> errMsg >> {}" , request.getAttribute("errMsg").toString());
		
		re.addAttribute("email", request.getAttribute("email"));
		re.addAttribute("errMsg", request.getAttribute("errMsg"));
		return "redirect:/user/login";	
	}

 

  header에 권한이 없으면 로그인과 회원가입을 띄울 수 있게하고, 권한이 있으면 게시판 글쓰기와 로그아웃이 나오도록설정.

 

시큐리티 taglib 추가해주고

<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

 

isAnonymous()는 권한이 없을 때, isAuthenticated()는 권한이 있을 때

sec:authorize를 이용해서 access에 권한이 있을 때, 없을 때를 설정

sec:authentication을 이용해서 property에 인증된 principal안에 로그인 정보를 가져올 수 있도록해서

닉네임과 이메일을 띄우고 누르면 회원정보수정으로 넘어가도록 만들예정.

        <sec:authorize access="isAnonymous()">          
	        <li class="nav-item">
	          <a class="nav-link" href="/user/register">회원가입</a>
	        </li>
	        <li class="nav-item">
	          <a class="nav-link" href="/user/login">로그인</a>
	        </li>
	    </sec:authorize> 
	    
        <sec:authorize access="isAuthenticated()">
        <!-- 인증 객체가 만들어져 있는 상태 -->
        <!-- 인증된 객체 가져오기 => 현재 로그인 정보는 : principal -->
        <sec:authentication property="principal.uvo.email" var="authEmail"/>   
        <sec:authentication property="principal.uvo.nickName" var="authNick"/>  
	        <li class="nav-item">
	          <a class="nav-link" href="/board/register">게시판 글쓰기</a>
	        </li>
	        <li class="nav-item">
	          <a class="nav-link" href="#">${authNick }(${authEmail })</a>
	        </li>
	        <li class="nav-item">
	          <a class="nav-link" href="/user/logout">로그아웃</a>
	        </li>
        </sec:authorize>

 

어드민권한, 유저리스트보기, 회원삭제 등등은 이어서 다음에 

'Spring 수업 정리' 카테고리의 다른 글

Spring 8일  (0) 2024.11.06
Spring 6일  (3) 2024.11.04
Spring 5일  (1) 2024.11.01
Spring 3일차.  (0) 2024.10.30
Spring 1일차.  (0) 2024.10.28