Backend

[Spring Security] 동작방법 및 Form, OAuth 로그인하기 (Feat.Thymeleaf 타임리프)

연_우리 2022. 2. 1. 23:23
반응형

목차

     

     

    썸네일

    Spring Security란?

    Spring을 사용할 때 애플리케이션에 대한 인증, 권한 부여 등의 보안 기능을 제공하는 프레임워크이다.

    다양한 로그인 방법(Form태그, OAuth2, JWT...)에 대해 Spring이 어느정도 구현해두었으니 수정(확장)해서 사용만 하면된다.

     

    Spring Security 동작방법

     

    간단히 요약하면

    1. 아이디, 비밀번호를 가진 요청이 들어온다

    2. Form 로그인이면 UserDetailsService의 loadUserByUsername메서드가 실행되고

       OAuth2 로그인이면 OAuth2UserService의 loadUserByUsername메서드가 실행된다.

    3. loadUserByUsername메서드는 "이런 정보가 들어왔는데 얘 혹시 회원이야?" 라고 묻는 메서드이다.

        = loadUserByUsername에서는 회원을 찾아주는 로직을 구현하면된다.

    4. 이때, 회원정보는 Form 로그인이면 UserDetails타입으로, OAuth2 로그인이면 OAuth2User타입으로 반환해준다.

    5. UserDetails 또는 OAuth2User를 반환하면 Spring에서 알아서 Session에 저장해준다.

     

     

    조금 더 정확하게 말하자면,

    6. Spring Security의 in-memory 세션 저장소인 SecurityContextHolder에 인증객체를 저장한다.

    7. SecurityContextHolder에 들어갈 수 있는 인증객체는 Authentication타입 1가지이다.

    8. AuthenticationAbstractAuthenticationToken으로 구현되어있고, AbstractAuthenticationToken

       UsernamePasswordAuthenticationToken과 OAuth2LoginAuthenticationToken이 구현하고있다.

    9. 우리가 5번에서 UserDetails, OAuth2User를 반환하면 Spring이 알아서

        UserDetails는 UsernamePasswordAuthenticationToken로, 

        OAuth2User는 OAuth2LoginAuthenticationToken으로 변환하고

    10. UsernamePasswordAuthenticationToken과 OAuth2LoginAuthenticationToken은

         Authentication의 자식이니 SecurityContextHolder에 저장할 수 있게된다.

     

     

     

     

     

    Spring Security 및 Thymeleaf설정하기

    build.gradle > dependency 추가

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

     

     

    패키지 > DemoApplication > BCryptPasswordEncoder 빈으로 등록

    Spring Security는 BCryptPasswordEncoder를 반드시 사용해야한다. (비밀번호 보안때문)

    @SpringBootApplication
    public class DemoApplication {
    
    	@Bean
    	public BCryptPasswordEncoder encoder(){
    		return new BCryptPasswordEncoder();
    	}
    
    	public static void main(String[] args) {
    		SpringApplication.run(DemoApplication.class, args);
    	}
    
    }

     

     

     

    Spring Security - Form Login하기

    완성된 모습

     

     

     

     

    Form Login설정

    패키지 > config 폴더 > SecurityConfig.java

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.authorizeRequests()
                    .antMatchers("/user/**").authenticated()     
                    				// user주소에 대해서 인증을 요구한다
                    .antMatchers("/manager/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")	
                    				// manager주소는 ROLE_MANAGER권한이나 ROLE_ADMIN권한이 있어야 접근할 수 있다.
                    .antMatchers("/admin/**").hasRole("ROLE_ADMIN")	
                    				// admin주소는 ROLE_ADMIN권한이 있어야 접근할 수 있다.
                    .anyRequest().permitAll();	// 나머지주소는 인증없이 접근 가능하다
        }
    }
    .authorizeRequests() Security처리에 HttpServletRequest를 이용한다
    ExpressionUrlAuthorizationConfigurer을 불러온다.
    .antMatchers("경로") 특정 경로 지정
    .anyRequest() 설정한 경로 외의 모든 경로
    .authenticated() 인증된 사용자만 접근할 수 있다
    .permitAll() 인증없이 접근할 수 있다
    .denyAll() 인증없이는 접근할 수 없다
    .access(String str) SpEL표현식의 결과가 true이면 접근할 수 있다
    .hasRole(String role) 사용자가 해당되는 Role이 있다면 접근할 수 있다
    .hasAnyRole(String role) 사용자가 가진 Role 중 해당되는 Role이 하나라도 존재한다면 접근할 수 있다
    .anonymous() 익명사용자가 접근할 수 있다
    .rememberMe() rememberMe인증 사용자가 접근할 수 있다.

     

     

    패키지 > config 폴더 > SecurityConfig.java 내용 추가

    Spring Security는 별다른 설정 없이도 "/login", "/logout" URL을 등록하고 처리방법도 모두 구현해두었다.

    따라서 커스텀한 로그인페이지를 구성하고자하면 아래와 같이 추가한다.

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            
            http.authorizeRequests()
                    .antMatchers("/user/**").authenticated()     
                    .antMatchers("/manager/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")	
                    .antMatchers("/admin/**").hasRole("ROLE_ADMIN")	
                    .anyRequest().permitAll()
          	  .and()					//추가
                .formLogin()				// form기반의 로그인인 경우
                    .loginPage("/loginForm")		// 인증이 필요한 URL에 접근하면 /loginForm으로 이동
                    .usernameParameter("id")		// 로그인 시 form에서 가져올 값(id, email 등이 해당)
                    .passwordParameter("pw")		// 로그인 시 form에서 가져올 값
                    .loginProcessingUrl("/login")		// 로그인을 처리할 URL 입력
                    .defaultSuccessUrl("/")			// 로그인 성공하면 "/" 으로 이동
                    .failureUrl("/loginForm")		//로그인 실패 시 /loginForm으로 이동
                .and()
                .logout()					// logout할 경우
                	.logoutUrl("/logout")			// 로그아웃을 처리할 URL 입력
                    .logoutSuccessUrl("/");			// 로그아웃 성공 시 "/"으로 이동
        }
    }

     

    .formLogin() form기반으로 로그인 할 경우의 설정을 추가할 수 있다
    FormLoginConfigurer를 불러온다.
    .loginPage(String url) 로그인 페이지 경로를 호출한다. default값은 "/login" 이다.
    .usernameParameter(String str) 스프링 시큐리티에서 사용자를 구분할 수 있는 값을 가져온다.
    default값은 "username"으로, <form>의 <input name="username">이면 작성하지 않아도 된다.
    <input name="id">인 경우에 작성해준다.
    .passwordParameter(String str) 스프링 시큐리티에서 사용자를 인증할 수 잆는 값을 가져온다.
    default값은 "password"로, <form>의 <input name="password">이면 작성하지 않아도 된다.
    <input name="pw">인 경우에 작성해준다.
    .loginProcessingUrl(String url) 로그인을 처리할 url을 설정한다. default값은 "/login" 이다.
    <form> 태그의 action속성과 맞추어준다.
    .defaultSuccessUrl(String url) 로그인 성공 시 url로 이동한다
    .failureUrl(String url) 로그인 실패 시 url로 이동한다. default값은 "/login?error" 이다.

     

    .logout() 로그아웃 시 설정을 추가할 수 있다.
    LogoutConfigurer을 불러온다.
    .logoutUrl(String url) 로그아웃을 처리할 url을 설정한다. default값은 "/logout" 이다.
    로그아웃 버튼의 href속성과 맞추어준다.
    .logoutSuccessUrl(String url) 로그아웃 성공 시 url로 이동한다

     

     + 참고

    loginProcessingUrl은 사용자가 입력한 url에 인증과정을 추가해준다. 

    //AbstractAuthenticationFilterConfigurer.java
    
    public T loginProcessingUrl(String loginProcessingUrl) {
        this.loginProcessingUrl = loginProcessingUrl;
        this.authFilter.setRequiresAuthenticationRequestMatcher(createLoginProcessingUrlMatcher(loginProcessingUrl));
        // authFilter = UsernamePasswordAuthenticationFilter이다. (formLogin에 걸리는 체인이니까)
        return getSelf();
    }

     

     

    FormLogin Authentication UserDetails

    패키지 > auth 폴더 > PrincipalDetails.java

    Form Login에 사용되는 Authentication은 UserDetails이다.

    추후 OAuth2 확장을 위해 UserDetails를 implements한다

    package com.example.demo.auth;
    
    @Getter 
    @ToString
    public class PrincipalDetails implements UserDetails {
    
        private User user;
    
        public PrincipalDetails(User user) {
            this.user = user;
        }
    
        /**
         * UserDetails 구현
         * 해당 유저의 권한목록 리턴
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
    
            Collection<GrantedAuthority> collect = new ArrayList<>();
            collect.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    return user.getRole().toString();
                }
            });
            return collect;
        }
    
    
        /**
         * UserDetails 구현
         * 비밀번호를 리턴
         */
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
    
        /**
         * UserDetails 구현
         * PK값을 반환해준다
         */
        @Override
        public String getUsername() {
            return user.getUsername();
        }
    
    
        /**
         * UserDetails 구현
         * 계정 만료 여부
         *  true : 만료안됨
         *  false : 만료됨
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
    
        /**
         * UserDetails 구현
         * 계정 잠김 여부
         *  true : 잠기지 않음
         *  false : 잠김
         */
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
    
        /**
         * UserDetails 구현
         * 계정 비밀번호 만료 여부
         *  true : 만료 안됨
         *  false : 만료됨
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
    
        /**
         * UserDetails 구현
         * 계정 활성화 여부
         *  true : 활성화됨
         *  false : 활성화 안됨
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    }

     

     

    FormLogin loadUserByUsername 

    패키지 > auth 폴더 > PrincipalDetailsService.java

    UserDetailsService는 FormLogin시 loadUserByUsername 메서드로 로그인한 유저가 DB에 저장되어있는지를 찾는다.

    찾으면 앞에서 구현한 Authentication(UserDetails를 구현한 PrincipalDetails)을 반환하여 SecurityContextHolder에 저장할 수 있게 한다.

    @Service
    public class PrincipalDetailsService implements UserDetailsService {
        @Autowired private UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            User byUsername = userRepository.findByUsername(username);
            if(byUsername != null){
                return new PrincipalDetails(byUsername);
            }
            return null;
        }
    }

     

     

    FormLogin Domain구현

    패키지 > domain 폴더 > Role.java

    package com.example.demo.domain;
    
    public enum Role {
        ROLE_USER, ROLE_MANAGER, ROLE_ADMIN
    }

     

     

    패키지 > domain 폴더 > User.java 

    package com.example.demo.domain;
    
    import lombok.*;
    import org.hibernate.annotations.CreationTimestamp;
    
    import javax.persistence.*;
    import java.sql.Timestamp;
    import java.time.LocalDateTime;
    
    @Entity
    @Getter @ToString
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class User {
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String username;
    
        @Setter
        private String password;
    
        private String email;
    
        @Enumerated(EnumType.STRING)
        @Setter
        private Role role;
    
        @CreationTimestamp  //자동으로 만들어준다
        private Timestamp createTime;
    
    
        @Builder(builderClassName = "UserDetailRegister", builderMethodName = "userDetailRegister")
        public User(String username, String password, String email, Role role) {
            this.username = username;
            this.password = password;
            this.email = email;
            this.role = role;
        }
    }

     

     

     

    FormLogin Controller 구현(Controller에서 인증 객체 가져오기)

    패키지 > controller 폴더 > UserController.java

    package com.example.demo.controller;
    
    @Controller
    public class UserController {
        @Autowired private UserRepository userRepository;
        @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
    
       @GetMapping("/loginForm")
        public String loginForm(){
            return "login";
        }
    
        @GetMapping("/joinForm")
        public String joinForm(){
            return "join";
        }
    
        @PostMapping("/join")
        public String join(@ModelAttribute User user){
            user.setRole(Role.ROLE_USER);
    
            String encodePwd = bCryptPasswordEncoder.encode(user.getPassword());
            user.setPassword(encodePwd);
    
            userRepository.save(user);  //반드시 패스워드 암호화해야함
            return "redirect:/loginForm";
        }
    
        @GetMapping("/user")
        @ResponseBody
        public String user(){
            return "user";
        }
    
        @GetMapping("/manager")
        @ResponseBody
        public String manager(){
            return "manager";
        }
    
        @GetMapping("/admin")
        @ResponseBody
        public String admin(){
            return "admin";
        }
        
        
        // !!!! OAuth로 로그인 시 이 방식대로 하면 CastException 발생함
        @GetMapping("/form/loginInfo")
        @ResponseBody
        public String formLoginInfo(Authentication authentication, @AuthenticationPrincipal PrincipalDetails principalDetails){
    
            PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
            User user = principal.getUser();
            System.out.println(user);
            //User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
    
            User user1 = principalDetails.getUser();
            System.out.println(user1);
            //User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
            //user == user1
    
            return user.toString();
       }
    
    }

     

     

    FormLogin View구현 (타임리프)

    resources > templates > join.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>회원가입 페이지</title>
    </head>
    <body>
    <h1>회원가입 페이지</h1>
    <hr/>
    <form action="/join" method="post">
        <input type="text" name="username" placeholder="Username"/> <br/>
        <input type="password" name="password" placeholder="Password"/> <br/>
        <input type="email" name="email" placeholder="Email"/> <br/>
        <button>회원가입</button>
    </form>
    </body>
    </html>

     

     

    resources > templates > login.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
        <meta charset="UTF-8">
        <title>로그인 페이지</title>
    </head>
    <body>
    <h1>로그인 페이지</h1>
    <hr/>
    
    <h2>로그인 유저 : </h2>
    <p sec:authentication="principal"></p>
    
    <div sec:authorize="isAnonymous()" style="background-color:pink; padding:1em;">
        <form action="/login" method="post" >
            <input type="text" name="username" />
            <input type="password" name="password" />
            <button>로그인</button>
        </form>
        <br>
        <a href="/joinForm">회원가입하기</a><br>
    </div>
    
    <div sec:authorize="isAuthenticated()" style="background-color:pink; padding:1em;">
        <a href="/logout">로그아웃</a>
    </div>
    
    <br><br>
    <a href="/user">유저</a><br>
    <a href="/manager" sec:authorize="hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')">매니저</a><br>
    <a href="/admin" sec:authorize="hasRole('ROLE_ADMIN')">어드민</a><br>
    
    <br><br>
    <a href="/form/loginInfo">Form 로그인 정보</a>
    
    </body>
    </html>
    sec:authentication="principal" 인증(=로그인)된 사용자의 정보를 모두 출력한다
    sec:authorize="isAnonymous()" 인증되어있지 않다면 해당 부분을 볼 수 있다.
    (반대로 인증되어있으면 보이지 않는다)
    sec:authorize="isAuthenticated()" 인증되어있으면 해당 부분을 볼 수 있다.
    (반대로 인증되어있지 않다면 보이지 않는다)
    sec:authorize="hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')" ROLE_MANAGER 또는 ROLE_ADMIN 권한이 있다면 해당 부분을 볼 수 있다.

     

     

     

     

     

     

    OAuth2 동작과정

     

    [OAuth2] 동작과정

     

    lotuus.tistory.com

     

     

    Spring Security - OAuth Google 로그인

    너무 길어져서 다음 게시글로..

     

    [Spring Security] OAuth 구글 로그인하기

    목차 [이전 게시글] 꼭! 봐주세여 [Spring Security] 동작방법 및 Form, OAuth 로그인하기 (Feat.Thymeleaf 타임리프) 목차 Spring Security란? Spring을 사용할 때 애플리케이션에 대한 인증, 권한 부여 등의 보..

    lotuus.tistory.com

     

     

    Spring Security - OAuth 네이버 로그인

     

    [Spring Security] OAuth 네이버 로그인하기

    목차 이전글 https://lotuus.tistory.com/79 [Spring Security] OAuth 구글 로그인하기 목차 [이전 게시글] 꼭! 봐주세여 [Spring Security] 동작방법 및 Form, OAuth 로그인하기 (Feat.Thymeleaf 타임리프) 목차..

    lotuus.tistory.com

     

     

    Spring Security - OAuth 카카오 로그인

     

    [Spring Security] OAuth 카카오 로그인하기

    목차 이전글 https://lotuus.tistory.com/80 [Spring Security] OAuth 네이버 로그인하기 목차 이전글 https://lotuus.tistory.com/79 [Spring Security] OAuth 구글 로그인하기 목차 [이전 게시글] 꼭! 봐주세여..

    lotuus.tistory.com

     

     

     

     

     

    참고

     

    Springboot - 시큐리티 특강

    함께 사는 세상 하지만 믿지는마.. 왜냐면 남탓할꺼자낭!!

    www.youtube.com

     

    spring security 동작방법, spring security 동작원리, 스프링 시큐리티, Form로그인, FormLogin, OAuth로그인, OAuth2로그인, OAuthLogin, OAuth2Login, 스프링 시큐리티 구글로그인, 스프링 시큐리티 네이버로그인, 스프링 시큐리티 카카오로그인, spring security google login, spring security naver login, spring security kakao login, 구글로그인 연동, 네이버로그인 연동, 카카오로그인 연동, 스프링시큐리티 기본개념, 스프링시큐리티 기본원리

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기