본문 바로가기
국비학원/수업기록

국비 지원 개발자 과정_Day94

by 루팽 2023. 4. 13.

<스프링 시큐리티 로그인, 회원가입, 구글로그인 - SecurityConfig.java>

package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import com.example.demo.oauth.PrincipalOauth2UserService;

// 1-1에서틑 SecurityConfig 파일을 생성하지 않았다 -> 스프링시큐리티에서 제공하는 인증화면으로 처리 진행됨
// 이 클래스를 정의하지 않으면 스프링 시큐리티가 제공하는 DefaultLoginPageGeneratingFilter가
// 제어권을 가져가서 개발자가 만든 로그인 화면을 만날 수 없다
// 결론: 내가 주도하는 인증처리를 위해서는 반드시 선언해야함
@EnableWebSecurity(debug=true) // 요청이 지나가는 필터정보 확인 가능
@EnableGlobalMethodSecurity(securedEnabled=true ,prePostEnabled=true) // 권한을 체크하겠다는 설정 추가
public class SecurityConfig {
	// 암호화가 안된 비밀번호로는 로그인이 안됨
	// 패스워드를 암호화하기위한 코드 추가
	// Bean어노테이션을 적으면 해당 메소드의 리턴되는 오브젝트를 IoC로 등록해줌
	@Autowired
	private PrincipalOauth2UserService principalOauth2UserService;
	
	@Bean
	public BCryptPasswordEncoder encodePwd() {
		return new BCryptPasswordEncoder();
	}
	/*
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	*/
	
	// 만약 관리자 계정이면 하위인 유저 계정에 접근할 수 있도록 처리하려면 아래 코드를 추가함
	// 유저는 관리자에 접근이 안되지만 관리자는 유저페이지도, 관리자페이지 접근 가늘하도록 설정
	@Bean
	RoleHierarchy roleHierarchy() {
		RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
		roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
		return roleHierarchy;
	}
	
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
		AuthenticationManagerBuilder auth = 
				http.getSharedObject(AuthenticationManagerBuilder.class);
		/*
		auth.inMemoryAuthentication()
		// 아래 메소드가 데프리케이트 대상인 이유는 테스트용도로만 사용하라는 경고의 의미
		// 보안상 안전하지 않으니 쓰지말것을 당부 -  그러나 지원은 끊지 않음
		.withUser(User.withDefaultPasswordEncoder()
				.username("user")
				.password("123")
				.roles("USER")
		).withUser(User.withDefaultPasswordEncoder()
				.username("admin")
				.password("1234")
				.roles("ADMIN")
		);
		*/
		http.csrf().disable(); // csrf filter 비활성화하기
		http.authorizeRequests() // http요청으로 들어오는 모든 것에대해서 매칭하기
		// localhost:5000/user로 요청하면 403발동 - 403접근권한이 없을때
		.antMatchers("/user/**").authenticated() // 인증만 되면 들어갈 수 있는 주소
		// manager나 admin 권한이 있는 사람만 접근가능, securedEnabled=true속성추가해야함
		.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
		// admin 권한이 있는 사람만 접근가능
		.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
		.anyRequest().permitAll() // 위 세가지가 아닌 경우는 모두 허용함
		.and()
		.formLogin()
		.loginPage("/loginForm") // 2차 단위테스트 - 권한에따라서 페이지 제어
		.loginProcessingUrl("/login") // 이 URL이 요청되면 시큐리티가 인터셉트해서 대신 로그인 진행
		.failureUrl("/login-error")
		.defaultSuccessUrl("/")
		.and()
		// 구글 로그인 기능 구현 추가
		.oauth2Login()
		.loginPage("/loginForm")
		// 구글 로그인 후 후처리 필요함
		// 1. 인증받기 -> 2. 엑세스 토큰(권한) -> 3. 사용자 프로필정보 가져오기 -> 4. 그 정보로 회원가입 처리가능(여기선 미적용)
		// 구글 로그인이 성공되면 코드를 받는게 아니라 엑세스 토큰과 사용자에대한 프로필정보를 한번에 받아옴
		.userInfoEndpoint()
		.userService(principalOauth2UserService); // 구글 로그인 후처리 클래스 추가
		// 현재 페이지에서 로그아웃을 눌럿을 때 로그인 페이지가 아니라 메인 페이지에 있도록 해줌
		//.logout(logout -> logout.logoutSuccessUrl("/"))
		// 403번 접근제한 예외 발생시 이동할 페이지 요청URL 작성하기
		//.exceptionHandling(exception -> exception.accessDeniedPage("/access-denied"));
		
		return http.build();
	}// end of filterChain
}


/*
	테스트 시나리오
	localhost:5000 요청은 권한과 상관없이 열림
	localhost:5000/user
	localhost:5000/admin
	localhost:5000/manager
	로그인 페이지로 이동함
	
	첫번째 - user롤일때
	localhost:5000/user 출력되고
	localhost:5000/admin 403발동
	localhost:5000/manager 403발동
	
	두번째 - admin 로그인했을때
	lcalhost:5000/user 출력
	localhost:5000/admin 출력
	localhost:5000/manager 출력
	
	두번째 - manager 로그인했을때
	lcalhost:5000/user 403발동
	localhost:5000/admin 403발동
	localhost:5000/manager 출력
*/

 

<스프링 시큐리티 로그인, 회원가입, 구글로그인 - HomeController.java>

package com.example.demo.controller;

import javax.servlet.http.HttpServletRequest;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;

@Controller
public class HomeController {
	Logger logger = LogManager.getLogger(HomeController.class);
	
	@Autowired
	private UserRepository userRepository;
	
	@Autowired
	private BCryptPasswordEncoder bCryptPasswordEncoder;
	
	@GetMapping({"", "/"})
	public String index(HttpServletRequest req) {
		logger.info("index");
		/*
		logger.info("admin 호출: " + req.isUserInRole("ROLE_ADMIN"));
		logger.info("user 호출: " + req.isUserInRole("ROLE_USER"));
		logger.info("manager 호출: " + req.isUserInRole("ROLE_MANAGER"));
		if(req.isUserInRole("ROLE_ADMIN")) {
			return "forward:admin-index.jsp";			
		}
		else if(req.isUserInRole("ROLE_USER")) {
			return "forward:user-index.jsp";			
		}
		else {
			return "forward:index.jsp";			
		}
		*/
		return "forward:index.jsp";
	}
	
	@GetMapping("/test/oauth/login")
	public @ResponseBody String testOauthLogin(Authentication authentication
			 								 , @AuthenticationPrincipal OAuth2User oauth) {
		logger.info("authentication: " + authentication.getPrincipal());
		OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();
		logger.info("oAuth2User: " + oAuth2User.getAttributes());
		logger.info("oauth: " + oauth.getAttributes());
		return "구글세션정보 확인";
	}
	
	// 로그인 화면
	@GetMapping("/loginForm")
	public String loginForm() {
		logger.info("loginForm");
		return "redirect:/loginForm.jsp";
	}
	
	/*
	/login이 요청되면 스프링 시큐리티가 인터셉트해서 대신 로그인 진행해줌
	@GetMapping("/login")
	public @ResponseBody String login() {
		logger.info("login");
		return "로그인 후 페이지";
	}
	*/
	
	// 회원가입 화면 부르기
	@GetMapping("/joinForm")
	public String joinForm() {
		logger.info("joinForm");
		return "redirect:/joinForm.jsp";
	}
	
	// 회원가입
	@PostMapping("/join")
	public String join(User user) {
		logger.info("user");
		user.setRole("ROLE_USER");
		// 패스워드 암호화 처리
		String rawPwd = user.getPassword();
		// nullpointer or Autowired(required=true) -> SecurityConfig 빈등록 안했을때 뜨는 에러
		String encPwd = bCryptPasswordEncoder.encode(rawPwd);
		user.setPassword(encPwd);
		userRepository.save(user);
		return "redirect:/loginForm.jsp";
	}
	
	@GetMapping("/login-error")
	public String loginError() {
		logger.info("loginError");
		return "redirect:/loginError.jsp";
	}
	
	@GetMapping("/access-denied")
	public String accessDenied() {
		logger.info("accessDenied");
		return "redirect:/accessDenied.jsp";
	}
	
	// 아래의 @PreAuthorize를 사용하려면 반드시 SecurituConfig에
	// @EnableGlobalMethodSecurity(securedEnabled=true ,prePostEnabled=true) 추가할것
	// 첫번째 파라미터는 롤에따른 접근 제약 활성화해줌
	// 두번째 파라미터는 해당 메소드에 롤을 부여할 때 추가할 것
	@PreAuthorize("hasAnyAuthority('ROLE_USER')")
	@GetMapping("/user")
	public String user() {
		logger.info("user");
		return "forward:user-index.jsp";
	}
	
	@GetMapping("/manager")
	public @ResponseBody String manager() {
		logger.info("manager");
		return "manager";
	}
	
	@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
	@GetMapping("/admin")
	public String admin() {
		logger.info("admin");
		return "forward:admin-index.jsp";
	}
	
	@GetMapping("/auth")
	public @ResponseBody Authentication auth() {
		return SecurityContextHolder.getContext().getAuthentication();
	}
}

/*
	1. 스프링 시큐리티가 관리하는 세션이 따로 존재한다.
	2. 테스트 시나리오
		localhost:5000/test/login 엔터
		파라미터인 Authentication은 스프링 시큐리티로부터 의존성 주입받음
		스프링 시큐리티는 스프링 시큐리티 세션을 들고있다 -> 서버 세션 영역에 시큐리티가 관리하는 세션이 따로 존재
		시큐리티 세션은 반드시 Authentication 객체만 들어갈 수 있다
		Authentication이 시큐리티 세션안에 있다는 건 로그인된 상태라는 의미
*/

 

<스프링 시큐리티 로그인, 회원가입, 구글로그인 - PrincipalOauth2UserService.java>

package com.example.demo.oauth;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService{
	Logger logger = LogManager.getLogger(PrincipalOauth2UserService.class);
	
	// 구글로부터 받은 userRequest데이터에 대한 후처리 메소드 구현 - Profile정보 수집
	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		// 액세스 토큰 출력해보기
		logger.info("구글에서 발급하는 토큰값: " + userRequest.getAccessToken().getTokenValue());
		logger.info("구글에서 발급하는 id값: " + userRequest.getClientRegistration().getRegistrationId());
		return super.loadUser(userRequest);
	}
}

 

<스프링 시큐리티 로그인, 회원가입, 구글로그인 - PrincipalDetailsService.java>

package com.example.demo.auth;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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 org.springframework.stereotype.Service;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;

@Service
public class PrincipalDetailsService implements UserDetailsService {
	Logger logger = LogManager.getLogger(PrincipalDetailsService.class);

	@Autowired
	private UserRepository userRepository;
	
	// 아래 파라미터 username은 화면에서 사용하는, 즉 input type의 name과 반드시 일치해야함
	// -> /login 요청되면 시큐리티가 인터셉트해서 자동으로 진행
	// 만약 다르게 하려면 SecurityCongif에서 .usernameParameter("mem_name") 추가할 것!
	// loadUserByUsername 메소드의 리턴은 어디로 가는가?
	// 시큐리티 session(Authentication(내부 userDetails))
	// 메소드 종료시 @AuthentidationPrincipal 어노테이션 생성됨 
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		logger.info(username); // 사용자가 입력한 이름
		// jpa query method로 검색하기 - mysql
		// mysql서버에 요청하는 코드
		User userEntity = userRepository.findByUsername(username); // 검색하기
		logger.info(userEntity);
		logger.info(userEntity.getRole());
		if(userEntity != null) {
			return new PrincipalDetails(userEntity);
		}
		return null;
	}
}

 

<스프링 시큐리티 로그인, 회원가입, 구글로그인 - PrincipalDetails.java>

package com.example.demo.auth;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import com.example.demo.model.User;
import lombok.Data;

/*
 * Authentication을 제공하는 인증제공자는 여러개가 동시에 존재할 수 있고
 * 인증 방식에따라 ProviderManager도 복수로 존재할 수 있음
 * Authentication은 인터페이스로 아래와 같은 정보를 들고있음
 * 	Set<GrantedAuthority> authorities: 인증된 권한 정보
 * 	principal: 인증 대상에 관한 정보로 UserDetails 타입이 옴
 * 	credentials: 인증 확인을 위한 정보 주로 비번이 오지만 인증 후에는 보안을 위해 삭제함
 * 	details: 그 밖에 정보 IP, 세션정보, 기타 인증 요청에서 사용했던 정보들
 * 	UserDetails와는 다른 정보를 가짐
 * 	boolean authenticated: 인증이 되었는지를 체크함
 * 
 * 필터들 중 일부 필터는 인증 정보에 관여함
 * 필터가 하는 역할은 AuthenticationManager를 통해 Authentication을 인증하고
 * 그 결과를 SecurityContextHolder에 넣어주는 일을 한다
 * SecurityContextHolder는 인증 보관함 보관소이다
 * 
 * AuthenticationManager가 인증관리인데
 * 이것의 구현체 클래스가 ProviderManager이고 이 클래스는 여러개 사용이 가능하다
 * 
 * 인증 토큰(Authentication)을 제공하는 필터들
 * : Authentication은 인터페이스이고 이것의 구현체 클래스들이 접미어에 Token이라는 이름을 사용하고 있으니
 * 인증 토큰이라고 해도 되지 않을까?
 * 폼 로그인
 * UsernamePasswordAuthenticationFilter -> UsernamePasswordAuthenticationToken
 * AnonymousAuthenticationFilter : 로그인을 하지 않았다는 인증 -> AnonymousAuthenticitionToken
 * SecurityContextPersistenceFilter : 기존 로그인을 유지함(기본적으로 session 이용)
 * Oauth2LoginAuthenticationFilter : 소셜로그인 -> Oauth2LoginAuthenticationToken
 */
@Data
public class PrincipalDetails implements UserDetails, OAuth2User{ //오버라이드 해줌
	Logger logger = LogManager.getLogger(PrincipalDetails.class);
	private User user; //콤포지션
	//구글 로그인시 구글서버에서 넣어주는 정보가 Map의 형태인데 그것을 받을 Map변수 선언하는 것임
	private Map<String,Object> attributes;
	//일반로그인시 사용하는 생성자
	public PrincipalDetails(User user) {
		this.user = user;
	}
	//OAuth로그인시 사용하는 생성자임
	//그런데 어떻게 User정보를 갖게 되냐면 attributes를 통해서 User를 생성해 준다
	public PrincipalDetails(User user, Map<String,Object> attributes) {
		this.user = user;
		this.attributes = attributes;
	}
	//해당 User의 권한을 리턴하는 곳
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		Collection<GrantedAuthority> collect = new ArrayList<>();
		collect.add(new GrantedAuthority() {
			@Override
			public String getAuthority() {
				return user.getRole();
			}
		});
		return collect;
	}
	//아래 정보들이 데이터베이스 쪽과 매칭이 안되면 loginFail.jsp페이지 호출됨
	@Override
	public String getPassword() {
		// TODO Auto-generated method stub
		return user.getPassword();
	}

	@Override
	public String getUsername() {
		return user.getUsername();
	}
	//세션에 담을 다른 컬럼 정보도 추가 가능함
	public int getId() {
		return user.getId();
	}

	@Override
	public boolean isAccountNonExpired() {
		//계정이 파괴되지 않았니? 네
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		//계정이 잠겨 있는지 유무 체크함
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		//계정 사용 기간이 지났는지, 비번을 너무 오래 사용한거 아닌지 물어보는 것임
		return true;
	}
	
	//네 계정이 활성화 되어 있니? 물어보는 거임
	@Override
	public boolean isEnabled() {
	//그럼 이걸 언제 false하면 되는 건가? 만일 1년 동안 회원이 로그인을 안하면
	//휴면 계정으로 처리하기
		return true;
	}
	
	// 구글 로그인 요청 후 콜백URL을 통해서 개인 프로필 정보를 Map에 담아주는 메소드를 재정의했음
	@Override
	public Map<String, Object> getAttributes() {
		logger.info("getAttributes");
		return attributes;
	}
	
	// 개인 프로필 정보를 getAttributes에서 Map에 담아주니까 아래 메소드는 사용값이 없는 경우로
	// 리턴을 null로 처리함
	@Override
	public String getName() {
		logger.info("getName");
		//return attributes.get("sub").toString();//이러면 되는데 사용하는 값이 아니므로 null로 함
		return null;
	}
}

 

<스프링 시큐리티 로그인, 회원가입, 구글로그인 - index.jsp>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>시작페이지</title>
   <link rel="stylesheet" href="/css/bootstrap.css">
   <link rel="stylesheet" href="/css/index.css">
</head>
<body>
<div class="container center-contents">
    <div class="row">
        <h1 class="title display-5"> 메인 페이지 </h1>
    </div>
    <div class="links">      
        <div class="link">
            <a href="/loginForm">  로그인 </a>
        </div>    
        <div class="link">
            <a href="/login-error">  로그인 에러  </a>
        </div>
        <div class="link">
            <a href="/access-denied" >  접근 에러  </a>
        </div>
        <div class="link">
            <a href="/user">  유저 페이지  </a>
        </div>
        <div class="link">
            <a href="/admin">  관리자 페이지 </a>
        </div>       
        <div class="link">
            <a href="/logout">로그아웃</a>
        </div>   
    </div>
</div>
</body>
</html>

댓글