TIL - 20주차 코드

 

1. 인증과 권한 1 : 230410

1) Session 개념

a. Session 로직

a) 조건 1
  • 세션 / 쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함.
  • if 너 로그인 했니?
  • 아니요 -> 로그인하고 와


b) 조건 2
  • if 너 로그인 햇니?
  • 네 -> if(그럼 너 어드민 이니?)
  • 아니요 -> 권한이 없다~ 얘.


b. Session-Id 개념

  • 세션아이디는 서버자원을 사용하는 것이기 때문에 인증을 하면 그때 키(세션아이디, 세션키)가 부여받게 된다.**
package kr.co.rland.web.controller.admin;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import jakarta.servlet.http.HttpServletRequest;

@Controller("adminHomeController")
@RequestMapping("/admin")
public class HomeController {
	
	@GetMapping("index")
	public String index(HttpServletRequest request) {

		// **session id 부여**
		// 아직 테스트만 했다. 중요하지 않다. sessionkey가 언제만들어지는지 확인!!
 		request.getSession().setAttribute("test", "hehe");

		return "admin/index";
	}

}




2) 로그인 기본 방식

a. 기능

  • Member.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Member {

    private Long id;
    private String userName;
    private String pwd;
    private String email;
    private Long roleId;
    
}


  • UserController.java

@Controller
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private MemberService memberService;


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @GetMapping("login")
    public String login(String uid, String pwd){

        // Member member= memberService.getByUseName(uid);
        
        // 우리는 컨트롤러에서 입력만 받으면 된다. 
        // 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다. 
        // id를 가져와서 컨트롤러에서 처리하기 때문이다.
        boolean isValid = memberService.isValidMember(uid, pwd);

        return "user/login";
    }

}


  • MemberService.java
package kr.co.rland.web.service;

public interface MemberService {

    // Member getByUseName(String uid);

    boolean isValidMember(String uid, String pwd);
    
}


  • DefaultMemberService.java

@Service
public class DefaultMemberService implements MemberService {
   
    @Autowired
    private MemberRepository repository;

    @Override
    public boolean isValidMember(String uid, String pwd) {
        
        Member member = repository.findByUserName(uid);
        
        return false;
    }
}


  • MemberRepository.java

@Mapper
public interface MemberRepository {
    Member findByUserName(String uid);
}




2. 인증과 권한 2 : 230411

1) 로그인 방식 1 : 바닥부터 만들기

  • UserController.java
package kr.co.rland.web.controller;

import org.springframework.beans.factory.annotation.Autowired;
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.RequestMapping;

import kr.co.rland.web.service.MemberService;

@Controller
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private MemberService memberService;


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @GetMapping("login")
    public String login(){
        return "user/login";
    }


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @PostMapping("login")
    public String login(String uid, String pwd){

        // Member member= memberService.getByUseName(uid);
        
        // 우리는 컨트롤러에서 입력만 받으면 된다. 
        // 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다. 
        // id를 가져와서 컨트롤러에서 처리하기 때문이다.
        boolean isValid = memberService.isValidMember(uid, pwd);

        System.out.println(isValid);
        
        if(isValid)
            return "redirect:index";
        
        return "redirect:login";
    }

}

  • MemberService.java
package kr.co.rland.web.service;

public interface MemberService {

    // Member getByUseName(String uid);

    boolean isValidMember(String uid, String pwd);
    
}

  • DefaultMemberService.java
package kr.co.rland.web.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import kr.co.rland.web.entity.Member;
import kr.co.rland.web.repository.MemberRepository;

@Service
public class DefaultMemberService implements MemberService {
   
    @Autowired
    private MemberRepository repository;

    @Override
    public boolean isValidMember(String uid, String pwd) {
        
        Member member = repository.findByUserName(uid);
        
        if(member == null)
            return false;

        // '=='가 아니라 '.getPwd().equals' 이다.
        else if(!member.getPwd().equals(pwd))
            return false;

        return true;
    }


}

  • MemberRepository.java
package kr.co.rland.web.repository;

import org.apache.ibatis.annotations.Mapper;

import kr.co.rland.web.entity.Member;

@Mapper
public interface MemberRepository {
    Member findByUserName(String uid);
}

  • MemberRepositoryMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.rland.web.repository.MemberRepository">
	
	<select id="findByUserName" resultType="Member">
		select * 
		from member
		where username=#{uid}
	</select>
	
</mapper>



2) 로그인 실패 시, 에러 추가

  • ${param.data}로 값 넘겨주기
    • 타임리프의 편의 객체 이용


a. 실습 코드 :


  • UserController.java

@Controller
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private MemberService memberService;


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @GetMapping("login")
    public String login(){
        return "user/login";
    }


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @PostMapping("login")
    public String login(String uid, 
                        String pwd,
                        HttpSession session){

        // Member member= memberService.getByUseName(uid);
        
        // 우리는 컨트롤러에서 입력만 받으면 된다. 
        // 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다. 
        // id를 가져와서 컨트롤러에서 처리하기 때문이다.
        boolean isValid = memberService.isValidMember(uid, pwd);

        System.out.println(isValid);
        
        if(isValid) {
            
            // session 케비넷을 사용하고 있는 User가 많다면,
            // 과부하가 생긴다.
            session.setAttribute("username", uid);
            
            return "redirect:/index";
        }
        
        return "redirect:login?error=sdfsdfsdf";
    }

}


  • login.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <link href="/css/reset.css" type="text/css" rel="stylesheet" >
        <link href="/css/style.css" type="text/css" rel="stylesheet" >
        <link href="/css/layout.css" type="text/css" rel="stylesheet" >
        <link href="/css/header.css" type="text/css" rel="stylesheet" >
        <link href="/css/footer.css" type="text/css" rel="stylesheet" >
        
        <link href="/css/buttons.css" type="text/css" rel="stylesheet" >
        <link href="/css/icon.css" type="text/css" rel="stylesheet" >
        <link href="/css/deco.css" type="text/css" rel="stylesheet" >
        <link href="/css/index.css" type="text/css" rel="stylesheet">
        <link href="/css/utils.css" type="text/css" rel="stylesheet" >    
        <style>
            .btn{
                color:black;
            }
        </style>
    </head>
    
    <body>
        <header class="header">
            <div>
                <h1 class="header-title"><a href="/index.html"><img class="logo" src="../image/logo-w.png" alt="알랜드"></a></h1>
                
                <ul class="main-menu d-none d-inline-flex-sm">
                    <li><a class="" href="/menu/list.html">카페메뉴</a></li>
                    <li><a class="" href="/notice/list.html">공지사항</a></li>
                    <li><a class="" href="/user/login.html">로그인</a></li>
                </ul>
                <div class="d-none-sm"><a class="icon icon-menu icon-white" href="?m=on">메뉴버튼</a></div>
            </div>
        </header>

    <main>
        <section>
            <header class="header-default">
                <h1 class="text-title1-h1">로그인</h1>
            </header>
            <section class="login">
                <h1 class="d-none" th:text="${param.error}">일반 로그인</h1>
                <div class="d-none" 
                th:class="${param.error} ? '' : 'd-none'";
                        style="color:red;
                        font-weight: bold;
                        text-align: center;
                        margin-bottom: 20px;">
                    아이디 또는 비밀번호가 일치하지 않습니다.
                </div>
                <form method="post">
                    <div class="d-flex align-items-center">
                        <label class="d-none">아이디</label><input name="uid" class="btn btn-cancel" type="text" placeholder="로그인 아이디를 입력하세요">
                    </div>
                    <div class="d-flex align-items-center">
                        <label class="d-none">비밀번호</label><input name="pwd" class="btn btn-cancel" type="password" placeholder="비밀번호">
                    </div>

                    <div class="d-flex align-items-center justify-content-center">
                        <input type="checkbox">
                        <label>로그인 저장</label>
                    </div>

                    <div class="d-flex align-items-center justify-content-center">
                        <input class="btn btn-default" type="submit" value="로그인">
                        <a class="btn btn-cancel" href="">취소하기</a>
                    </div>
                </form>
            </section>

            <section class="register">
                <h1 class="d-none">회원가입</h1>
                <a href="signup.html">회원가입</a>
                <a href="">아이디 찾기</a>
                <a href="">비밀번호 찾기</a>
            </section>

            <section class="social-login">
                <h1 class="d-none">소셜 로그인</h1>
                <span>또는 다음으로 로그인</span>
                <div>
                    <a class="icon icon-naver mx-2" href="">네이버 로그인</a>
                    <a class="icon icon-kakao mx-2" href="">카카오 로그인</a>
                    <a class="icon icon-youtube mx-2" href="http://localhost/login/oauth2/code/google">구글 로그인</a>
                </div>
            </section>
        </section>
    </main>

    <footer class="footer">
        <h2>알랜드(Rland)</h2>
        <div>
            copyright @ rland.co.kr 2022-2022 All Right Reservved. Contact admin@rland.co.kr for more information
        </div>
    </footer>
</body>
</html>




3) session 값 넘겨서 로그인 유지하기

  • 쇼핑몰 예시처럼 로그인 안되면 index 페이지로 보내줄 것



a. 실습 코드 :

  • admin/UserController.java

@Controller
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private MemberService memberService;


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @GetMapping("login")
    public String login(){
        return "user/login";
    }


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @PostMapping("login")
    public String login(String uid, 
                        String pwd,
                        String returnURL,
                        HttpSession session){

        // Member member= memberService.getByUseName(uid);
        
        // 우리는 컨트롤러에서 입력만 받으면 된다. 
        // 컨트롤러에서 모든 것을 업무하면 안 된다. 그래서 위의 서비스는 사용하지 않는다. 
        // id를 가져와서 컨트롤러에서 처리하기 때문이다.
        boolean isValid = memberService.isValidMember(uid, pwd);

        System.out.println(isValid);
        
        if(isValid) {
            
            // session 케비넷을 사용하고 있는 User가 많다면,
            // 과부하가 생긴다.
            // 하지만, 로그인하면 이렇게 세션키를 발급!
            session.setAttribute("username", uid);
            
            // 
            if(returnURL != null)
                return "redirect:"+returnURL;

            return "redirect:/index";
        }

        return "redirect:login?error=sdfsdfsdf";
    }

}

  • admin/MenuController.java

@Controller("adminMenuController")
@RequestMapping("/admin/menu/")
public class MenuController {
	
	// 콩자루를 가져온다. 결합을 의미한다! 빈 객체 사용! 
	// 결합은 setInjection과 CompositionInjection이 있다.
	
	// 이것을 실행하려면 Application.java에서 실행해주자!
	// 객체가 같은 것이 2개가 있다면, DI하는데 문제가 있는 것이다. 그래서 @Qualifier로 구분해준다.
	// setter injection은 실행하거나 실행되는 영역이 생긴다. 바인딩 되기 전에 다른 작업을 처리할 수 있다.
	// 하지만, 코드 라인수가 가장 적은 '필드'에 @Autowired를 하여 DI를 한다. 

	@Autowired
	private MenuService service;
	
	// 400 에러는 데이터가 파라미터 인자가 없는 경우이다.
	// 403 에러는 권한 관련 에러이다. 
	// 405 에러는 처리메서드가 post요청인데 get요청만 있는 경우

	
	@RequestMapping("list")
	public String list(
			@RequestParam(name = "p", defaultValue = "1") int page, 
			@RequestParam(name = "c", required = false) Integer categoryId,
			@RequestParam(name = "q", required = false) String query,
			Model model,
			HttpSession session
			) throws UnsupportedEncodingException
	{
		// 세션 / 쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함.
		// if 너 로그인 했니?(인증한 적이 없으면 로그인을 다시 하러 가야 한다.)
		if(session.getAttribute("username")==null)
			return "redirect:/user/login?returnURL=/admin/menu/list";

		// 아니요 -> 로그인하고 와
		
		// if 너 로그인 햇니? 
		// 네 -> if(그럼 너 어드민 이니?)
		// 아니요 -> 권한이 없다~ 얘~
		
		// **세션아이디는 서버자원을 사용하는 것이기 때문에 인증을 하면 그때 키(세션아이디, 세션키)가 부여받게 된다.**

		List<MenuView> list = service.getViewList(page, categoryId, query);
		
		model.addAttribute("list", list);
		return "admin/menu/list";
	}
	
	@GetMapping("detail")
	public String detail(int id, Model model) {
		
		Menu menu = service.getById(id);
		
		model.addAttribute("menu", menu);

		
		return "admin/menu/detail";
	}
	
	// 등록폼을 주세요.
	// @RequestMapping("reg") 
	// -> service() 함수와 같다. : Get/Post를 내가 다 처리하는 매핑 
	
	@GetMapping("reg")
	public String reg() {
		return "/WEB-INF/view/admin/menu/reg.jsp";
	}
	
	// 폼입력해서 제출이요.
	@PostMapping("reg")
	public String reg(String title) {
		// 등록하고나서!
		System.out.println("메뉴 등록 완료");
		return "redirect:list";		
		// @Controller는 view단을 찾으므로 url을 입력해줘야한다. 
		// redirection은 reg라는 페이지에서 클라이언트가 list라는 url로 가라고 한다.
		// 경로가 더 이상 없다면 현재 경로에서 찾는다. 
		// 그 요청은 jsp파일의 form 태그에서 method post를 설정해줘야 한다. Post 요청이라서! 
	}
	
}




4) 초기의 필터 만들기

package kr.co.rland.web.config;

import java.io.IOException;

import org.springframework.stereotype.Component;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;

@Component
public class AuthFilter implements Filter {
    
    // 인증을 위한 필터 만들기!
    // 따로 설정을 안 하면, 수문장이라서 해당 서비스 내부로 들여보내질 않는다.
    
    // 예전에는 이런 방식으로 필터를 만들었었다.
    private static final String[] authUrls = {
        "/admin/**","/member/**"
    };

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        

        // 사용자가 입력하는 uri와 url을 체킹한다. 에러가 발생했는지 알 수 있다.(로그인 실패 등등)
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String uri = httpRequest.getRequestURI();
        System.out.println(uri);
        
        String url = httpRequest.getRequestURI().toString();
        System.out.println(url);



        // FilterChain은 여러개 쓸 수 있어서 다음 Chain도 설정 해준다!
        
        System.out.println("입구 필터가 실행되었습니다.");
        
        chain.doFilter(request, response);

        System.out.println("출구 필터가 실행되었습니다.");  
        // 이렇게 하면, 여러번 필터가 발생하는데 모든 파일들에 대해 필터 처리를 해준다.

    }
}




5) Spring Security

  • pom.xml 에 아래 라이브러리를 깔면,

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

package kr.co.rland.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class RlandSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
        .authorizeHttpRequests()            // 권한 요청 
        .requestMatchers("/admin/**")    // url에 대한 패턴 권한 부여 
        .hasAnyRole("ADMIN")           // 어떤 사용자에 대한 권한 부여
        // **가 재귀 경로이다. 하부 구조 전부를 말한다.
        .requestMatchers("/member/**").hasAnyRole("ADMIN,MEMBER")
        .anyRequest().permitAll();  // 권한 부여주기

        // .hasAnyAutnullhority()  : Role_""가 기본 권한 방식,   
        // .hasAnyRole(null) : 앞에 Role_를 붙여줘서 역할만 바로 쓰면 된다.
        return null;
    }

}




3. 스프링 시큐리티 적용 : 230412

1) 스프링 시큐리티 기본 이용

  • 스프링 시큐리티가 기본으로 주는 로그인 폼이 있다.


a. 내 코드 :

  • 스프링 시큐리티 이용!!(내 코드 에러남…)

package kr.co.rland.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class RlandSecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
        .authorizeHttpRequests()            // 권한 요청 
        .requestMatchers("/admin/**")    // url에 대한 패턴 권한 부여 
        .hasAnyRole("ADMIN")                // 어떤 사용자에 대한 권한 부여// **가 재귀 경로이다. 하부 구조 전부를 말한다.
        .requestMatchers("/member/**")
        .hasAnyRole("ADMIN,MEMBER")
        .anyRequest().permitAll()  // 권한 부여주기
        .and()
        .formLogin();

        // .hasAnyAutnullhority()  : Role_""가 기본 권한 방식,   
        // .hasAnyRole(null) : 앞에 Role_를 붙여줘서 역할만 바로 쓰면 된다.

        // build를 해야지 실행된다!!
        return http.build();
    }




    // SpringSecurity를 사용하기 위해서, PasswordEncoder가 필요하다!
    // 요즘에는 Bcrypt 해시 함수를 이용하는 암호화 패스워드를 이용한다!
    // 해시 함수는 식별자라고 하며 식별자를 의미한다. 
    // 해시 값으로 원문은 못만든다!!

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }






    // 사용자 데이터 서비스
    // 1. 인메모리 서비스 : 어디에 있는 것이 아니라 메모리에만 사용자가 있어서
    // 2. JDBC 서비스
    // 3. LDAP 서비스

    @Bean
    public UserDetailsService userDetailsService(){
        
        //import org.springframework.security.core.userdetails.UserDetails; 이용하기!

        // UserDetails은 어디에 있는 것이 아니라 
        // 메모리에만 사용자가 있어서 인메모리 서비스 라고 부른다.
        UserDetails newlec = User
                                .builder()
                                .username("newlec")
                                .password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
                                .roles("ADMIN","MEMBER")
                                .build();
        
        UserDetails dragon = User
                                .builder()
                                .username("newlec")
                                .password("111")
                                .roles("ADMIN","MEMBER")
                                .build();

        // ** Builder는 AllArg 생성자가 필요하다!! 
        // Builder는 원하는 인자만 따로 생성자를 만들 때, 사용한다.
        // Member member = Member.builder()
        //                 .id()
        //                 .username("newlec")
        //                 .build();   

        // Member member = Member.builder().id(1L).build();

     

        // 이렇게 원하는 사용자를 계속 담는다.
        return new InMemoryUserDetailsManager(newlec, dragon);
    }
}



b. 스프링 시큐리티 : 수정한 코드 :!!

package kr.co.rland.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class RlandSecurityConfig {
    
    @Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
        .authorizeHttpRequests()
        .requestMatchers("/admin/**").hasAnyRole("ADMIN")
        .requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
        .anyRequest().permitAll()
        .and()
            .formLogin();
        
        // /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }



    // 사용자 데이터 서비스
    //1. 인메모리 서비스
    //2. JDBC 서비스
    //3. LDAP 서비스

    @Bean
    public UserDetailsService userDetailsService(){
        UserDetails newlec = User
                .builder()
                .username("newlec")
                .password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
                .roles("ADMIN","MEMBER")
                .build();

         UserDetails dragon = User
                .builder()
                .username("dragon")
                .password("111")
                .roles("MEMBER")
                .build();
       

        return new InMemoryUserDetailsManager(newlec, dragon);
    }

}



2) 유저가 만든 로그인 페이지 이용하는 스프링 시큐리티

a. 유저가 만든 로그인 페이지 이용하는 방법


.and()
.formLogin()
.loginPage("/user/login");


b. 실습 코드


@Configuration
public class RlandSecurityConfig {
    
    @Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
        .authorizeHttpRequests()
        .requestMatchers("/admin/**").hasAnyRole("ADMIN")
        .requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
        .anyRequest().permitAll()
        .and()
            .formLogin()
            .loginPage("/user/login");
        
        // /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능

        return http.build();
    }



3) 크로스 사이트에서 원격요청 설정

@Configuration
public class RlandSecurityConfig {
    
    @Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        
        http
        .cors().and()   // 리소스 사이트를 의미한다. 
        .csrf().disable()   // 크로스 사이트에서 원격요청을 검사하는 작업을 disable하는 것이다. 
        .authorizeHttpRequests()
        .requestMatchers("/admin/**").hasAnyRole("ADMIN")
        .requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
        .anyRequest().permitAll()
        .and()
            .formLogin()                         // 무조건 인증을 안하면 redirect해서 여기로 와서 로그인을 해야 한다.
            .loginPage("/user/login")  // 이렇게 설정하면 사용자가 만든 로그인 페이지를 사용하게 해준다. 
            .loginProcessingUrl("/user/login"); // 여기에는 사용자가 만든 POST 요청 URL을 적자!
            
        
        // /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능

        return http.build();
    }




4) 주의 사항

  • 스프링이 로그인 기능을 만들어 주었기 때문에 html의 input 태그의 name 속성을 username과 password로 바꿔줘야 한다.

  • 로그인하면 어디로 가야하는가? 성공한 url 페이지

    • defaultSuccessUrl 이용하기

@Configuration
public class RlandSecurityConfig {
    
    @Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        
        http
        .cors().and()   // 리소스 사이트를 의미한다. 
        .csrf().disable()   // 크로스 사이트에서 원격요청을 검사하는 작업을 disable하는 것이다. 
        .authorizeHttpRequests()
        .requestMatchers("/admin/**").hasAnyRole("ADMIN")
        .requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
        .anyRequest().permitAll()
        .and()
            .formLogin()                         // 무조건 인증을 안하면 redirect해서 여기로 와서 로그인을 해야 한다.
            .loginPage("/user/login")  // 이렇게 설정하면 사용자가 만든 로그인 페이지를 사용하게 해준다. 
            .loginProcessingUrl("/user/login") // 여기에는 사용자가 만든 POST 요청 URL을 적자!
            .defaultSuccessUrl("/admin/index"); // 로그인 성공 시, 성공한 url 이동 가능
        
        // /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능

        return http.build();
    }




5) 예전 방식의 Logout 방식

  • Httpsession 이용!

@Controller
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private MemberService memberService;


    // 회원이 탈되되어도 사용자 이름이랑 식별자는 남긴다.
    @GetMapping("login")
    public String login(){
        return "user/login";
    }
    
    // 예전 방식의 logout!!(session $이용 )
    @RequestMapping("logout")
    public String logout(HttpSession session){
        session.invalidate();

        return "redirect:/index";
    }




5) 스프링 시큐리티 방식의 Logout 방식

a. 기본 로그아웃 설정(Spring Security)


.and()
    .logout()
        .logoutUrl("/user/logout")  // 사용자가 만든 로그아웃 화면 URL
        .logoutSuccessUrl("index"); // 로그아웃 성공하면, 이동하는 URL 설정
    

b. 실습 코드 :

@Configuration
public class RlandSecurityConfig {
    
    @Bean//=객체만들어서 리턴할거야 ,필터체인= 수문장
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        
        http
        .cors().and()   // 리소스 사이트를 의미한다. 
        .csrf().disable()   // 크로스 사이트에서 원격요청을 검사하는 작업을 disable하는 것이다. 
        .authorizeHttpRequests()
        .requestMatchers("/admin/**").hasAnyRole("ADMIN")
        .requestMatchers("/member/**").hasAnyRole("ADMIN","MEMBER")
        .anyRequest().permitAll()
        .and()
            .formLogin()                         // 무조건 인증을 안하면 redirect해서 여기로 와서 로그인을 해야 한다.
            .loginPage("/user/login")  // 이렇게 설정하면 사용자가 만든 로그인 페이지를 사용하게 해준다. 
            .loginProcessingUrl("/user/login") // 여기에는 사용자가 만든 POST 요청 URL을 적자!
            .defaultSuccessUrl("/admin/index") // 로그인 성공 시, 성공한 url 이동 가능
        .and()
            .logout()
                .logoutUrl("/user/logout")  // 사용자가 만든 로그아웃 화면 URL
                .logoutSuccessUrl("index"); // 로그아웃 성공하면, 이동하는 URL 설정
        
        // /*디렉토리 하나만, /**는 서브 디렉토리까지 내려가기 가능

        return http.build();
    }




6) 타임리프에서 Spring Security 적용!

  • 타임리프에 태그 추가 : xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity"

  • isAnonymous() : 익명의 사용자만 보인다.

  • isAuthenticated() : 로그인한 인증된 사용자만 보인다.


  • 실습 코드 :

<!DOCTYPE html>
<html 
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity"
>
   <header class="header">
        <div>
            <h1 class="header-title"><a href="/index"><img class="logo" src="/image/logo-w.png" alt="알랜드"></a></h1>
            
            <ul class="main-menu d-none d-inline-flex-sm">
            <li><a class="" href="/menu/list">카페메뉴</a></li>
                <li><a class="" href="/notice/list">공지사항</a></li>
                <li sec:authorize="isAnonymous()"><a class="" href="/user/login">로그인</a></li>
                <li sec:authorize="isAuthenticated()"><a class="" href="/user/logout">로그아웃</a></li>
            </ul>
         <div class="d-none-sm"><a class="icon icon-menu icon-white" href="?m=on">메뉴버튼</a></div>
        </div>
    </header>
</html>




4. 230413


1) JDBC 서비스

  • 권한 테이블((Role))에서 회원이 관리자나 회원이 된다면, 회원 테이블(Member)/과 권한 테이블(Role)은 다대다 관계여야 한다.

  • 현재 Rland의 경우에 회원이 권한이 1개 밖에 안된다.

  • 그렇다면, 권한이 여러개면 어떻게 할까?

  • 'pwd password' 이렇게 매핑 시킨다.(as 기능)


// DB의 칼럼명이 다른데 어떻게 매핑할까?
// 현재, username, password, enable라는 칼럼이 필요!
// 'pwd password' 이렇게 매핑 시킨다.(as 기능)
manager.setUsersByUsernameQuery("select username, pwd password, 1 enabled from member where username=?");
manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_MEMBER' autority from member where username=?");





2) 시큐리티로 유저정보 받기(준비 단계)

  • MenuController.java


@Controller("adminMenuController")
@RequestMapping("/admin/menu")
public class MenuController {
   
   @Autowired
   private MenuService service;

   @GetMapping("list")
   public String list(
         @RequestParam(name = "p", defaultValue = "1") int page,
         @RequestParam(name="c", required = false) Integer categoryId,
         @RequestParam(name="q", required = false) String query,
         Model model,
         HttpSession session
         ){
      
      //세션/쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함
      // if(너 로그인은 했니?)
      // if(session.getAttribute("username")==null)
      //    return "redirect:/user/login?returnURL=/admin/menu/list";
      //아니요 -> 로그인하구와

      // if(너 로그인은 했니? )
      //네->if (그럼 너 어드민이니?)
      //아니요 -> 권한 없다 얘~
      



      List<MenuView> list = service.getViewList(page,categoryId, query);
      model.addAttribute("list", list);
      
      return "admin/menu/list";
   }
   
   @GetMapping("detail")
   public String detail(
         int id,
         Model model) {
      
      Menu menu = service.getById(id);
      model.addAttribute("menu", menu);
      
      return "admin/menu/detail";
   }
   
   // /admin/menu/reg
   // /admin/menu/reg.html
   // /admin/menu/reg.jsp
   
   // 등록폼을 주세요
   //@RequestMapping("reg")//-> service():Get/Post를 내가 다 처리하는 매핑
   @GetMapping("reg")
   public String reg() {
      return "/WEB-INF/view/admin/menu/reg.jsp";
   }
   
   // 폼입력해서 제출이요
   //@RequestMapping("reg")
   @PostMapping("reg")
   public String reg(String title,
      Authentication authentication) {
	  
	  //방법3 : getPrincipal가 유저정보를 반환해준다!
	  UserDetails user = (UserDetails) authentication.getPrincipal();
      System.out.println(user.getUsername());
	
      //방법2
      String userName= authentication.getName();
      // authentication.getPrincipal();
      System.out.println(userName);


      // //방법1
      // Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      // String currentPrincipalName = authentication.getName();
      // // 등록하고
      // System.out.println("메뉴 등록 완료");
      return "redirect:list";
   }
}



  • SecurityConfig.java



// https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html

@Configuration // 콩자루에 담아야할 내용들을 설정함
public class RlandSecurityConfig {
   
   @Autowired
   private DataSource dataSource;
   
   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 보안 설정을 위한.. 주소에 대한 필터링을 하는.. 너는 통과, 너는 안 돼 
      
      // 2가지 url 제외하고는 모두 허가함
      // 로그인페이지로 보내는 작업을 내가 할 필요가 없음. 
      // 예외는 오류의 한 종류 - 구문오류, 논리오류, 예외오류(입력값이 잘못된 것) 
      http
         .cors()
         .and() // 리소스쉐어링 
            .csrf().disable() // 리퀘스트포절? 하는 작업을 디저블 하는거 
            .authorizeHttpRequests() // 권한을 요청 
               .requestMatchers("/admin/**").hasAnyRole("ADMIN")
               .requestMatchers("/member/**").hasAnyRole("ADMIN", "MEMBER") // admin은 member 페이지도 들어갈 수 있다고 가정함
               .anyRequest().permitAll()
         .and()
            .formLogin()
               .loginPage("/user/login")
               .loginProcessingUrl("/user/login")
               .defaultSuccessUrl("/admin/menu/list") // 로그인 성공 후에 기본 성공 URL을 설정
         .and() // 한 묶
            .logout()
               .logoutUrl("/user/logout")
               .logoutSuccessUrl("/index");
         
         
      return http.build(); // 설정을들 다 담고 한 번에 빌드하기가 트렌드 
   }
   
   @Bean
   public PasswordEncoder passwordEncoder() {
      
      return new BCryptPasswordEncoder();
   }
   
   // 인증과 권한이 필요한 url에 대한 정보와 사용자에대한 정보가 필요함
   // 사용자 데이터 서비스 --------------------------------------------------
   // 2. JDBC 서비스
   @Bean
   public UserDetailsService jdbcUserDetailsService() {
      
      JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
      manager.setUsersByUsernameQuery("select username, pwd password, 1 from member where username = ?"); // username, password, enabled 
      manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_ADMIN' autority from member where username = ?");
      //manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_MEMBER' autority from member where username = ?");
      
      return manager;
   }
   
   
   
   // 1. 인메모리 서비스
   //@Bean
   public UserDetailsService userDetailsService() {
      
      // 세터보다 빌더가 편하다. 빌더 패턴
      // 1. 인메모리 사용자 정보 
      // 관리자를 등록하는 유저, 슈퍼 관리자가 쓰거나 db를 쓰지 않는 경우에 인메모리 사용자 사용함 
      // 일반적으로 사용자 정보를 db에 넣고 사용함
      UserDetails newlec = User
            .builder()
            .username("newlec")
            //.password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
            .password(passwordEncoder().encode("111")) // 스프링이 사용자가 입력한 값을 암호화할 때 사용 
            .roles("ADMIN", "MEMBER")
            .build();
      
      UserDetails dragon = User
            .builder()
            .username("dragon")
            .password(passwordEncoder().encode("111"))
            .roles("MEMBER")
            .build();
      
      // 예시 
//      Member member = Member
//                     .builder()
//                     .id(1L)
//                     .userName("newlec")
//                     .build();
      
      return new InMemoryUserDetailsManager(newlec, dragon); 
   }
}




3) 커스터마이징한 사용자 정보를 DB에서 가져와서 스프링 시큐리티 사용

  • RlandSecurityConfig.java
    • 전역에서 설정 파일

// https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html

@Configuration // 콩자루에 담아야할 내용들을 설정함
public class RlandSecurityConfig {
   
   @Autowired
   private DataSource dataSource;
   
   @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 보안 설정을 위한.. 주소에 대한 필터링을 하는.. 너는 통과, 너는 안 돼 
      
      // 2가지 url 제외하고는 모두 허가함
      // 로그인페이지로 보내는 작업을 내가 할 필요가 없음. 
      // 예외는 오류의 한 종류 - 구문오류, 논리오류, 예외오류(입력값이 잘못된 것) 
      http
         .cors()
         .and() // 리소스쉐어링 
            .csrf().disable() // 리퀘스트포절? 하는 작업을 디저블 하는거 
            .authorizeHttpRequests() // 권한을 요청 
               .requestMatchers("/admin/**").hasAnyRole("ADMIN")
               .requestMatchers("/member/**").hasAnyRole("ADMIN", "MEMBER") // admin은 member 페이지도 들어갈 수 있다고 가정함
               .anyRequest().permitAll()
         .and()
            .formLogin()
               .loginPage("/user/login")
               .loginProcessingUrl("/user/login")
               .defaultSuccessUrl("/admin/menu/list") // 로그인 성공 후에 기본 성공 URL을 설정
         .and() // 한 묶
            .logout()
               .logoutUrl("/user/logout")
               .logoutSuccessUrl("/index");
         
         
      return http.build(); // 설정을들 다 담고 한 번에 빌드하기가 트렌드 
   }
   
   @Bean
   public PasswordEncoder passwordEncoder() {
      
      return new BCryptPasswordEncoder();
   }

   // 사용자 데이터 서비스 사용하기!!(사용하기 위해서 컨테이너에 @Bean를 사용하여 담아주기!)
   // 여기서 담아주거나 RlandUserDetailsService.java에서 @Service로 컨테이너에 빈 객체로 담아준다!
   // 3. Custom user Service 
   // @Bean
   public UserDetailsService rlanDetailsService() {
        return new RlandUserDetailsService();
   }

   
   // 인증과 권한이 필요한 url에 대한 정보와 사용자에대한 정보가 필요함
   // 사용자 데이터 서비스 --------------------------------------------------
   // 2. JDBC 서비스
   // @Bean
   public UserDetailsService jdbcUserDetailsService() {
      
      JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
      manager.setUsersByUsernameQuery("select username, pwd password, 1 from member where username = ?"); // username, password, enabled 
      manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_ADMIN' autority from member where username = ?");
      //manager.setAuthoritiesByUsernameQuery("select username, 'ROLE_MEMBER' autority from member where username = ?");
      
      return manager;
   }
   
   
   
   // 1. 인메모리 서비스
   //@Bean
   public UserDetailsService userDetailsService() {
      
      // 세터보다 빌더가 편하다. 빌더 패턴
      // 1. 인메모리 사용자 정보 
      // 관리자를 등록하는 유저, 슈퍼 관리자가 쓰거나 db를 쓰지 않는 경우에 인메모리 사용자 사용함 
      // 일반적으로 사용자 정보를 db에 넣고 사용함
      UserDetails newlec = User
            .builder()
            .username("newlec")
            //.password("$2a$10$H.HXCEd59CmUnp9luiKHwestJxuSIGRsJZNzCWfTfpJPgk6VGLm3O")
            .password(passwordEncoder().encode("111")) // 스프링이 사용자가 입력한 값을 암호화할 때 사용 
            .roles("ADMIN", "MEMBER")
            .build();
      
      UserDetails dragon = User
            .builder()
            .username("dragon")
            .password(passwordEncoder().encode("111"))
            .roles("MEMBER")
            .build();
      
      // 예시 
//      Member member = Member
//                     .builder()
//                     .id(1L)
//                     .userName("newlec")
//                     .build();
      
      return new InMemoryUserDetailsManager(newlec, dragon); 
   }
}



  • RlandUserDetails.java
    • 담아줄 그릇 : entity 느낌

@ToString
@Setter
@Getter
public class RlandUserDetails implements UserDetails{
    
    private Long id;
    private String email;
    private String username;
    private String password;
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
       return authorities;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }

    
}




  • MenuController.java
    • view와 서버를 연결해주는 컨트롤러
    • Principal가 유저 정보를 반환해준다.

@Controller("adminMenuController")
@RequestMapping("/admin/menu")
public class MenuController {
   
   @Autowired
   private MenuService service;

   @GetMapping("list")
   public String list(
         @RequestParam(name = "p", defaultValue = "1") int page,
         @RequestParam(name="c", required = false) Integer categoryId,
         @RequestParam(name="q", required = false) String query,
         Model model,
         HttpSession session
         ){
      
      //세션/쿠키를 이용해서 사용자가 인증되었던 적이 있는지를 확인함
      // if(너 로그인은 했니?)
      // if(session.getAttribute("username")==null)
      //    return "redirect:/user/login?returnURL=/admin/menu/list";
      //아니요 -> 로그인하구와

      // if(너 로그인은 했니? )
      //네->if (그럼 너 어드민이니?)
      //아니요 -> 권한 없다 얘~
      



      List<MenuView> list = service.getViewList(page,categoryId, query);
      model.addAttribute("list", list);
      
      return "admin/menu/list";
   }
   
   @GetMapping("detail")
   public String detail(
         int id,
         Model model) {
      
      Menu menu = service.getById(id);
      model.addAttribute("menu", menu);
      
      return "admin/menu/detail";
   }
   
   // /admin/menu/reg
   // /admin/menu/reg.html
   // /admin/menu/reg.jsp
   
   // 등록폼을 주세요
   //@RequestMapping("reg")//-> service():Get/Post를 내가 다 처리하는 매핑
   @GetMapping("reg")
   public String reg() {
      return "/WEB-INF/view/admin/menu/reg.jsp";
   }
   
   // 폼입력해서 제출이요
   //@RequestMapping("reg")
   @PostMapping("reg")
   public String reg(String title,
      Authentication authentication,
	  Principal principal
	  ) {

	  // 방법 4: 
	  System.out.println(principal.getName());


	  //방법3 : getPrincipal가 유저정보를 반환해준다!
	  UserDetails user = (UserDetails) authentication.getPrincipal();
      System.out.println(user.getUsername());
	
      //방법2
      String userName= authentication.getName();
      // authentication.getPrincipal();
      System.out.println(userName);


      // //방법1
      // Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      // String currentPrincipalName = authentication.getName();
      // // 등록하고
      // System.out.println("메뉴 등록 완료");
      return "redirect:list";
   }
}



  • RlandUserDetailsService.java
    • 서비스 구현 부이며, findByUserName에서 DB 값을 가져오며 SimpleGrantedAuthority는 GrantedAuthority 인터페이스의 구현체이다.
    • 권한은 여러개가 있을 수 있어서 List로 받음.

// 중요!!**
// 이렇게 @Service로 빈 객체를 만들어서 사용할 수 있도록 컨테이너에 담아주거나
// RlandSecurityConfig.java에서 UserDetailsService 객체에서 빈 객체를 만들어서 사용할 수 있도록 컨테이너에 담아주거나
@Service 
public class RlandUserDetailsService implements UserDetailsService {

    @Autowired
    private MemberRepository repository;
    
    @Override
    public UserDetails loadUserByUsername(String username) {

        // RlandUserDetails 그릇에 담을 데이터 준비!
        Member member = repository.findByUserName(username);

        List<GrantedAuthority> authorities = new ArrayList<>();

        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

        // 데이터 준비 되었으면 이제 RlandUserDetails 그릇 객체를 만들어서 데이터를 담아서 반환해주면 끝!
        RlandUserDetails user = new RlandUserDetails();
        user.setId(member.getId());
        user.setUsername(member.getUserName());
        user.setPassword(member.getPwd());
        user.setEmail(member.getEmail());
        user.setAuthorities(authorities);

        return user;
    }
}





4) 데이터 뽑아내기

  • authentication는 권한 준 아이디에서 데이터를 뽑아낸다.

  • authorize은 권한이 있는지 판단

<!DOCTYPE html>
<html 	
	xmlns:th="http://www.thymeleaf.org"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
   xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
	layout:decorate="inc/layout"
>
 	
   <main layout:fragment="adminmain" class="main-padding-none">
   
<!--   문서 안에 포함되어야 해서 스크립트 위치 주의! 그리고 defer 설정 중요!!-->
   <script src="/js/admin/menu/list.js" defer="defer"></script>
      <section id="main-section">
         <header class="search-header">            <!-- authentication는 권한 준 아이디에서 데이터를 뽑아낸다. -->
            <h1 class="text-title1-h1" sec:authorize="isAuthenticated()">알랜드 메뉴<span sec:authentication="principal.email" ></span></h1>
            <form>
               <input type="text">
               <input type="submit" class="icon icon-find">
            </form>
         </header>