<스프링 시큐리티 로그인, 회원가입, 구글로그인 - 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>
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day96 (0) | 2023.04.17 |
---|---|
국비 지원 개발자 과정_Day95 (0) | 2023.04.14 |
국비 지원 개발자 과정_Day93 (0) | 2023.04.12 |
국비 지원 개발자 과정_Day92 (0) | 2023.04.11 |
국비 지원 개발자 과정_Day91 (0) | 2023.04.07 |
댓글