@Service
public class MyUserDetailsService implements UserDetailsService {
	@Autowired
	private IF_MemberDao memberdao;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		try {
			MemberVO member = memberdao.selectOne(username);
		    return User.builder().username(member.getId()).password(member.getPass()).roles(member.getRole()).build();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

}

 

 

return User.builder().username(member.getId()).password(member.getPass()).roles(member.getRole()).build();

이코드에서 

다음부분을

roles(member.getRole())

다음과 같이 수저한다.

roles(member.getRole(),"ADMIN")

 

이번에는

사용자 로그인시

실제 데이터 베이스에 저장된 값으로 로그인을 하도록 커스터 마이징을 합니다.

 

데이터베이스는 오라클을 

퍼시스턴스 프레임워크는 mybatis를 사용하고 있습니다.

 


데이터 베이스를 먼저 살펴보겠습니다.

 

테이블 구조입니다.

 

 

튜플입니다. 튜플을 보면 ROLE이라는 권한이 저장 되어 있습니다. 이 글에는 저장되어 있는 경우와

저장되어 있지 않은 경우(즉, 컬럼이 없는 경우)도 기록해 놓았습니다.

 

또 튜플을 보면 비밀번호가 암호화 되어 있습니다. 이 글에는 저장된 비밀번호가 평문일 경우도

해결하는 방법도 기록이 되어 있습니다. 


 

 

 

먼저 Spring Security 에서 이해해야 할 인터페이스가 있습니다. 

이전에 언급한 UserDetailsService입니다. 


 UserDetailsService   - 아이디로 디비 조회 후 사용자 정보 리턴(체크, 아이디만 가지고 디비 조회)
       - 주요설정 -
        a. 서비스 로직을 통해 아이디 디비 조회
        b. 사용자정보 리턴  -  리턴시 User객체를 builder, 또는 User객체를 커스트마이징 한 객체 타입으로 리턴

 

UserDetailService는 구현받은 클래스를 객체로 만드는 설정만 있다면 정상적으로 시큐리티가 알아서 동작시켜준다.

구현받은 클래스 작업외에는 설정하지 않을 것이다. @Service 어노테이션으로 객체로 생성되도록 처리한다.

@Service
public class MyUserDetailsService implements UserDetailsService {

 


먼저 UserDetailService로 완성 된 소스를 살표 보겠습니다.


@Service
public class MyUserDetailsService implements UserDetailsService {
	@Autowired
	private IF_MemberDao memberdao;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		try {
			MemberVO member = memberdao.selectOne(username);
		    return User.builder().username(member.getId()).password(member.getPass()).roles(member.getRole()).build();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

}

 

디비를 조회할 객체가 필요합니다. < 이 부분은 미리 데이터베이스 작업이 필요합니다 >

@Autowired

private  IF_MemberDao memberdao;

 

UserDetailsService  인터페이스가 실행되면 자동으로 loadUserByUsername(String username) 메서드가 실행되고

메서드는 UserDetails 타입을 리턴하게 됩니다. 

소스를 보겠습니다.

 

MemberVO member = memberdao.selectOne(username);
이 코드에서 username은 클라이언트가 보낸  username을 스프링 시큐리티가 저장한 변수입니다.
즉, 클라이언트가 보낸 username으로 데이터베이스를 조회합니다. 
return User.builder().username(member.getId()).password(member.getPass()).roles(member.getRole()).build();
이 코드는 리턴하는 코드입니다.
loadUserByUsername 메서드는 UserDetails(인터페이스) 타입을 리턴하게 되어 있습니다.
코드는 User 객체 (import org.springframework.security.core.userdetails.User;)를 리턴합니다.
User 디테일은 스프링시큐리티에서 제공하는 객체이고  Userdetails 인터페이스를 상속 받은 것입니다.

User 객체로 리턴한다는 것을 이해했으니
세부적으로 User 객체의 값을 셋팅하는 부분을 살펴 봅니다. 
usename(디비에서검색한 결과의 아이디),
password(디비에서 검색한 결과의 암호),
role(디비에서 저장된 회원의  Role)으로 user객체를  builder합니다.

이렇게 하면
자동으로 
클라이언트가 입력한 비밀번호와
디비에서 조회한 비밀번호의 일치 여부를 판단하여 최종 인가가 완료 됩니다.


주의점
UserDetails에서 인식하는 비밀번호는 반드시 암호화가 되어 있어야 합니다.
그래서 데이터 베이스에 저장할 때 암호화 시켜야 하는 것이 좋습니다.

만약, 데이터베이스에 저장된 암호가 평문일 때는 
User.builder().username(member.getId()).password(member.getPass()).roles(member.getRole()).build();
이 코드에서 password(member.getPass()를 암호화 작업함)를 빌드 하기 전 암호화 작업을 합니다.

 

 

User.builder().username(member.getId()).password(member.getPass()).roles(member.getRole()).build();

에서 암호화 하는 방법도 설명해 봅니다.

먼저, 스프링에서 제공하는 암호 객체를 사용하기 위해 bean을 등록합니다.

 

 

 

이렇게 빈을 등록하면 필요한 곳에서 주입 받아서 쓰면 됩니다.

지금 필요한 곳은 UserDetails 이니 MyUserDetailsService 에서 작업해 봅니다. 

 

 

@Service
public class MyUserDetailsService implements UserDetailsService {
	
	@Autowired
	private PasswordEncoder passwordEncoder;
    
	@Autowired
	private IF_MemberDao memberdao;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		try {
			MemberVO member = memberdao.selectOne(username);
			String passwordEncoding = passwordEncoder.encode(member.getPass());
			return User.builder().username(member.getId()).password(passwordEncoding).roles(member.getRole()).build();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

 

코드를 살펴 보겠습니다

@Autowired
private PasswordEncoder passwordEncoder;

SecurityConfig 에서 설정한 Bean의 주소를 주입 받습니다.
String passwordEncoding = passwordEncoder.encode(member.getPass());
디비에서 조회한 password를 암호화 합니다.


return User.builder().username(member.getId()).password(passwordEncoding).roles(member.getRole()).build();
암호화된 passwordEncoding  변수를 리턴할 때 사용합니다,

참고,
member.getRole(), 즉 멤버의 ROLE 이 데이터 베이스에 없을 경우 
문자열로 직접 기입하되 됩니다.. 예) roles("USER")

 

* 이번장에서는 스프링 시큐리티에서 제공하는 로그인 화면이 아닌

   개발자가 직접 만들 로그인 화면으로 커스터마이징을 할 것이다.

   이때, 로그인 화면의 변경과 같이 클라이언트가 파라미터로 전송하는 아이디와 패스워드도 저장 할 것이다 **

 

 

IDE : STS4 - Spring Tool Suite-4

 

** 개발환경

spring boot 3.3.5

spring security 6.0

oracle 11g

mybatis

gradle

 

-


spring security작업에 앞서

login view와 컨트롤러 작업을 먼저 한다.

 

login.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h2>Login</h2>
	<form action="login" method="post">
		ID<input type="text" name="username">
		pw<input type="password" name="password">
		<input type="submit" value="login">
	</form>
</body>
</html>

 

login url 처리 추가 (컨트롤러)

 

<확인> 서버를 재시작하고 http://www.localhost:8080/login 이라고 입력해 본다. 

결과 : 막혀있을 것이다.. 이유는 spring scurity의 인가정책에서 거부 되어 있을 것이다. 이 부분을 다음과 같이 수정한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http	.csrf((csrfConfig) -> csrfConfig.disable())
                .authorizeHttpRequests(auth -> auth
                         .requestMatchers("/","/login").permitAll()
                         .requestMatchers("/freeboard*").hasRole("USER")
                         .anyRequest().authenticated() 
                )
                ;  
        return http.build();
    }	

}

 

이제 정상적으로 로그인 화면이 보여진다.

 

 


login 관련 Spring security를 작성하자

 

SecurityFilterChain 에 로그인관련 설정을 한다. 

 

<Check>

1. usernameParameter("username")  username은 클라이언트가 넘겨 주는 아이디를 저장하고 있는 변수이다.

2. passwordParameter("password")  password는 클라이언트가 넘겨 주는 비밀번호 변수이다.

3. loginPage("/login")  인가받지 않은 사용자에게 login url로 이동하게 하는 것. 즉, 인가 받지 않은 url인 경우 자동으로 

                                     login url로 이동하고 컨트롤러는 해당 뷰로 응답한다.

4. defaultSuccessUrl("/") 로그인 성공시 이동하려는 url 정의

5. failureForwardUrl("/login") 로그인 실패시 이동하려는 url정의

  login.html 과 securityFilterChain과 비교  - 서로 일치해야 한다.
                                                                        - Spring security 내부에서는 username과 password 변수로 인식 됨
                                                                        -  이런 이유로 html파일부터 변수명을 같게 코딩 함
 

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http	.csrf((csrfConfig) -> csrfConfig.disable())
                .authorizeHttpRequests(auth -> auth
                         .requestMatchers("/","/login").permitAll()
                         .requestMatchers("/freeboard*").hasRole("USER")
                         .anyRequest().authenticated() 
                )
                .formLogin(formLogin->formLogin     
                        .loginPage("/login")        // 로그인 url
                        .defaultSuccessUrl("/")     // 성동하면 이동할 url
                        .usernameParameter("username")  //  클라이언트가 넘겨 주는 아이디 변수값
                        .passwordParameter("password")  //  클라이언트가 넘겨 주는 비밀번호 변수값
                        .failureForwardUrl("/login") // 인증 실패시 이동할 url
                )
                ;  
        return http.build();
    }	

}

 

 

 정상 작동하는지 테스트 해 보자. 

테스트 url 결과 설명
http://www.localhost8080/ home.html 나옴 인가 정책에서   permitAll
http://www.localhost8080/freeboard login.html 나옴 인가가 되어 있지 않아서 
loginPage("/login") 설정에 의해
로그인 화면이 나옴
http://www.localhost8080/login login.html 나옴 인가 정책에서   permitAll

 

 

> 위 테스트 내역 중 2번째 테스트를 실행해 보겠습니다.

 

이렇게 입력하면 아래와 같이 이동한다.

 

 

 

 

 

 

 

 

 

ID는 user 

PW는 암호를 입력해보자

이제는 로그인 창이 아닌 다음과 같은 창이 나올 것이다.  정상이다.. 이유는 로그인은 성공했으나 freeboard는 USER권한 자만 가능하다.. 즉 아직 권한을 부여 받지 않은 상태이므로 아직까지는 접근할 수 없다. 

 

 

더 정확하게 테스트를 하기 위해서

컨트롤러에 url 매핑을 추가하고 뷰를 하나더 만들어 보자

 

study.html도 만들어 보자

 

 

다시 테스트 해 보자

테스트 url 결과 설명
http://www.localhost8080/ home.html 나옴 인가 정책에서   permitAll
http://www.localhost8080/freeboard
로그인은 되었으나 USER권한이 없음
http://www.localhost8080/login login.html 나옴 인가 정책에서   permitAll
http://www.localhost8080/study study.html 나옴 인가정책에서
anyRequest().authenticated()

 

 

다음에는 인증과 권한 부여를 실제 데이터베이스에서 조회하는 방법으로 커스터마이징을 할 것이다.

 

~~

IDE : STS4 - Spring Tool Suite-4

 

** 개발환경

spring boot 3.3.5

spring security 6.0

oracle 11g

mybatis

gradle

 

Spring Security 이해 맟 암기.

* 다음 내용은 이해도 중요하지만 암기가 맞다. 암기해야 한다. 

 먼저, Spring security에서 사용하는 객체를 이해한다.
  1. SecurityFilterChain 
       - 주요설정 -
        a. csrf 설정                               - csrf기능을 비활성화 또는 사용과 관련된 설정, (이번장에서는 비활성화)
        b. authorizeHttpRequests        - url 요청에 따른 인가정책 
        c. formLogin                             - 클라이언트가 로그인 요청시 파라미터 값 받기등 부가작업
        d. logout                                   - 클라이언트가 로그아웃 요청시 처리
        e. userDetailsService (생략가능)  - 아이디로 디비 조회 후 사용자 정보 리턴(User 또는 User객체를 커스터마이징한 객체)
  2. UserDetailsService   - 아이디로 디비 조회 후 사용자 정보 리턴(체크, 아이디만 가지고 디비 조회)
       - 주요설정 -
        a. 서비스 로직을 통해 아이디 디비 조회
        b. 사용자정보 리턴  -  리턴시 User객체를 builder, 또는 User객체를 커스트마이징 한 객체 타입으로 리턴

  3. AuthenticationProvider - 사용자 정보가 리턴되면 자동으로 클라이언트가 입력한 암호와 디비 조회에서 가져온
                                             암호를 비교 함. 이때 둘 다 암호화 되어있어야 함.
       특징 : 커스트마이징 하지 않아도 자동
       - 주요설정 -
        a. 클라이언트가 전송한 아이디와 비번을 저장하는 객체
        b. 디비 조회로 가져온 객체의 정보
        c. a와 b의 암호가 같은지 판단 (아이디는 이미 userDetailsService에서 조회 함)
        d. 권한부여... -여기서 부여하는 권한은  1번 SecurityFilterChain의 b. authorizeHttpRequests 의 정책에 적용된다.
  

 

그럼,

1.  SecurityFilterChain 작업을 시작해 보자. 

가. package만들기 






나. SecurityFilterChain class만들기 (클래스명은 SecurityConfig로 한다.)   * 어노테이션은 spring security 설정이라는 의미
   



다.  csrf와 authorizeHttpRequests 의 설정을 먼저 한다. 
     --------------------------------------------------------
    - 주요설정 -
        a. csrf 설정                               -  비활성화
        b. authorizeHttpRequests        - / 요청은 모두 허용, freeboard로 시작하는 요청은 USER권한만 가능, 그외 요청은 허가 
                                                             받은 유저만 가능
     --------------------------------------------------------
 

 

2. 위에서 설정한 작업이 정상인지 확인해 보자

서버를 재시작한다. 

브라우저 창에  다음 순서로 입력한다.

url 주소 결과 설명
http://www.localhost:8080        home.html 문서가 보인다. requestMatchers("/").permitAll()
http://www.localhost:8080/freeboard 액세스가 거부 됨. 그 밑에는
HTTP ERROR 403
requestMatchers("/freeboard*").hasRole("USER")

 

 

위 표와 같이 테스팅이 나오면 spring security의 인가 정책은 정상적으로 동작하는 것이다. 

 

 

 

다음장은 login 커스트마이징 입니다 ~

IDE : STS4 - Spring Tool Suite-4

 

** 개발환경

spring boot 3.3.5

spring security 6.0

oracle 11g

mybatis

gradle

 

- spring boot project가 이미 만들어진 후 spring  security를 적용하는 방법으로  기록한다.

 

1. 의존성 추가

build.gradle 파일을 클릭하여 의존성을 추가 한다. > gradle을 업데이트 한다.

<소스>  implementation 'org.springframework.boot:spring-boot-starter-security'

 

build.gradle 파일을 클릭하여 의존성을 추가 

 

Gradle 업데이트

 

 

확인방법-----------------------------------------

서버를 다시 시작하고 주소창에 주소를 입력하면 로그인 창이 나온다.

주소창에 localhost:8080 이라고 입력(포트번호는 다를 수 있음, 스프링 부트 콘솔을 참고)하면 자동으로 로그인 창으로 이동된다. 아래 화면에 보이는 로그인 창은 spring boot가 내장하고 있는 화면.

 

http://www.localhost:8080   이라고 브라우저 주소창에 입력한다.. 여기서 포트 번호를 다를 수 있으니

스프링 부트의 콘솔을 확인해 보자.

이렇게 되면 spring security 설치는 완료 된 것.

 

 

http://www.localhost:8080 또는 http://www.localhost:8080/freeboard로 입력하고 로그인 화면으로 전환된다. 

즉, 어떤  url을 요청해도 spring security는 필터링하여 인증을 하도록 한다.

 

 

맛보기 - 인증과 인가를 이해해 보자.

서버를 재시작하고 콘솔을 보면 임시 비밀번호가 있다.



임시 비밀번호로 로그인을 할 것이다.
 username부분에는  user를 입력하고, password부분은 콘솔에 출력된 암호를 입력한다. 
Sing in을 입력하면 다음과 같이 인가가 된 것을 확인 할 수 있다.
인가 받은 user이기 때문에 다른 url요청도 모두 인가해 준다. 

맛보기 -------------------------------------------- 끝

 

 

IDE : STS4 - Spring Tool Suite-4

 

오늘은  spring security를 프로젝트에 적용시킬 것이다.

적용 시킬 프로젝트를 먼저 간단하게 만들어 보자

 

1. 프로젝트 생성

 

2. Dependency는 간단하게 4개만 추가 한다.

    spring security는 추가하지 않고 프로젝트 완성 후에 추가할 예정

 

생성 된 프로젝트의 구조를 먼저 살펴본다

많이 다루게 될 부분을 빨간색으로 표시함

 

 

서버를 구동시키고 콘솔창을 확인한다. 다음과 같은 failed가 나온다.

이유를 잘 읽어 보면, 프로젝트 생성 시 mybatis 퍼시스턴스 프레임워크를 추가 했는데 관련 된 설정 파일을 로딩할 수 없어서 생긴 문제이다.

 

      - 해결방법 -

다음 설정 내용을 application.properties 파일에 추가한다.

#Oracle DataSource

spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver

spring.datasource.url=jdbc:oracle:thin:@127.0.0.1:1521/orcl

spring.datasource.username=system

spring.datasource.password=11111111

 

# Mapper Xml Location

mybatis.mapper-locations=classpath:mappers/*.xml

 

 

다시 서버를 부팅하고 콘솔창을 확인하면 fail이 사라졌다.

 

여기까지 설정은 완료 되었다

이제 간단한 컨트롤러를 만들어 보자.

 

이제 컨트롤러에서 응답할 뷰를 만들어 보자.

 

home.html 소스

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Document</title>

</head>

<body>

<h2>Main Page</h2>

</body>

</html>

 

freeboard.html 소스

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Document</title>

</head>

<body>

<h2>freeboard</h2>

</body>

</html>

 

 

컨트롤러 동작이 되는지 확인해 본다. 

 

 

이제 부터는 spring security를 추가하여 권한별로 접근이 가능하도록 설정한다.

간단하게 생각해 보자

 

웹서비스란 ? 

우리가 네이버 카페 서비스를 이용할때,

첫번째, 네이버 메인화면 요청

두번째, 로그인 요청

세번째, 카페 메뉴 요청

 

클라이언트는 요청을 하고 서버는 요청에 대한 응답을 한다.

이때 요청은 웹주소이다.  요청이 잘못된 경우, 즉 웹요청이 잘못된 경우 404에러 메시지가 웹브라우저에 나타난다..

 

단어정리 - 다음 단어는 매우 중요함.

1. 클라이언트 : 서비스를 요청하는 주체

2. 웹브라우저 : 웹서비스를 요청하고 결과를 보여주는 프로그램

3. 웹서비스의 절차 : 첫번째 : 클라이언트 요청, 두번째 : 서버의 처리, 세번째, 서버의 응답, 네번째 : 클라이언트 웹브라우저에서 랜더링한다. 

(심화, 위와 같은 절차가 끝나면 서버와 클라이언트의 연결은 물리적으로 종료된다.

이것이 좀 어려운데 이렇게 생각하자

음식점이 서버이고 손님이 클라이언트 같은 개념이다. 손님이 음식을 먹고 계산하고 나가면 음식점에는 사람이 없는 것이다.

이게 연결이 끊긴 것이다. 그런데 가게 주인은 손님을 기억하고 싶어서 별도로 손님의 전화번호를 저장했다고 하자.

이렇게 기억하는 것을 논리적으로 연결된것이라고 한다. 여기서 끊겼다는 것은 물리적인 접속이 끊긴것이다. 물리적인것은 현재 손을 잡고 있던가, 전화통화가 진행중임을 의미하고, 논리적으로 연결됨은 옆에는 없지만 기억하고 있다 이런 의미이다.)

 

4. http 프로토콜 : 클라이언트가 서버에서 웹서비스를 요구한다는 의미. 서버는 웹서비스로 이해함.. 이런 약속, 프로토콜이라 함

(심화, 기본 포트 번호 80번을 사용함

좀 어려운데... 깊게 파면 외우는 게 될 테니 일단 이해해 보자.

아파트가 있다. 아파트는 층이 있다. 각 층의 집은 집번호가 있다.   아파트 주소  + 각 층의 동호수가 최종 주소가 된다.

컴퓨터가 있다. 컴퓨터는 여러개의 통신 프로그램이 있다.   컴퓨터주소 + 통신프로그램 번호가 주소가 된다.

여기서 통신프로그램주소가 포트번호라고 생각하면 된다.

여기서 중요한 것 !!!

아파트 단지에서 동호수가 중복될 수 없다. 

하나의 컴퓨터에서도 여러개의 통신 프로그램있다. 인터넷, 카톡, 네이트온, 게임 등.. 이 프로그램의 통신용 포트도 중복되면 안된다. 꼭 기억하시길 포트번호가 충돌 나면 안된다.

)

 

 

정리해보자 

 

클라이언트요청  > 서버에서 요청접수 > 클라이언트 요청을 처리해 줄 전문프로그램 > 서버의 응답 > 응답받은 클라이언트에서 웹브라우저로 화면에 보여줌

 

이것을 프로그램으로 매칭한다면

클라이언트는 웹브라우저를 사용

요청을 접수하는 프로그램이 아파치톰캣

클라이언트 요청을 처리해 줄 전문 프로그램이 스프링

서버에서 응답해주는 화면을 만들어 주는 것은 스프링

만들어준 화면을 스프링 > 서버 > 클라이언트 순으로 전송

 

 

웹브라우저, 톰캣, 스프링이 어떤 역활을 하는지 정리를 꼭 하고 다음 장을 읽어보자~~~

 

 

 

 

'spring > regacy' 카테고리의 다른 글

[spring] 먼저 읽기, 스프링 공부하기, 스프링 이해  (0) 2024.01.11

스프링..

기록할 도구는

-------------------------------------------

IDE : spring

Build-tool : maven

server : apach tomcat 8.5

--------------------------------------------

spring boot가 아닌 spring를 선택한 이유는 

spring을 사용하는 입장과 spring을 설정도 직접해 볼 수 있는 기회를 통해 흐름을 이해하는데 도움이 될거라고 판단함.

실제, 교육을 spring으로 해 보니 다른 framework도 잘 적응함, 

 

자 그럼 시작해 봅시다.

 

'spring > regacy' 카테고리의 다른 글

[spring] 웹이해, http프로토콜 이해  (1) 2024.01.11

+ Recent posts