ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 인증, 인가를 프로젝트에 끼워넣기
    Spring/SpringSecurity 2022. 4. 23. 04:15

    =================

    Spring Security+jwt 입니다

     

    로그인해서(/v1/admin(auth)/signin으로 세팅돼있습니다)

    연습용 토큰을 발급하고(POSTMAN의 헤더 부분에 떠있습니다)

    다른 API호출할 땐 POSTMAN의 Authorization에 Type을 Bearer Token으로 해놓고 발급받은 토큰을 넣어주시면 됩니다

     

    =================

    로그인이 필요한 admin, auth 도메인은 인증, 인가 둘다 필요하고

    나머지는 인가만 필요하니까 두개 나눠서 적었습니다. 둘 중에 하나만 보시면 됩니다

     

    =================

    [1]인가

    [2]인증+인가

     

    =================

    [1]인가 : admin, auth 외 나머지

     

    이걸 하면 사용자가 헤더에 jwt 토큰을 갖고 있을 때만 그 설정한 API를 부를 수 있습니다~

     

    1.

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

    추가

     

    2.

    WebSecurityConfigurerAdapter를 상속하는 클래스 만들기

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        static public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .cors();
            http
                    .authorizeRequests()
                    .antMatchers("/v1/admin/health-check").permitAll()
                    .antMatchers("/v1/admin/signup").permitAll()
                    .antMatchers("/v1/admin/findid").permitAll()
                    .antMatchers("/v1/admin/findpw").permitAll();
    
            http
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .addFilter(getAuthenticationFilter());
        }
        @Bean
        public CorsConfigurationSource corsConfigurationSource(){
    
            CorsConfiguration configuration = new CorsConfiguration();
    
            configuration.addAllowedOriginPattern("*");
            configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","PATCH","OPTIONS","DELETE"));
            configuration.setAllowedHeaders(Arrays.asList("*"));
            configuration.setAllowCredentials(true);
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
    
            return source;
        }
    }

    - antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣는다

     

    3.

    token.expiration_time = 86400000
    token.secret = usertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkoreausertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkorea

    application.properties에 이것 추가

     

    4.

    HandlerInterceptorAdapter를 상속하는 클래스 만들기

    package com.cloudlibrary.admin.ui.security;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.env.Environment;
    import org.springframework.http.HttpHeaders;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import io.jsonwebtoken.Jwts;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    @SuppressWarnings("deprecation")
    public class TokenValidationInterceptor extends HandlerInterceptorAdapter {
    
        Environment env;
    
        public TokenValidationInterceptor(Environment env) {
            this.env = env;
        }
    
        private boolean isJwtValid(String jwt) {
            boolean returnValue = true;
    
            String subject = null;
            try {
                subject = Jwts.parser().setSigningKey(env.getProperty("token.secret")).parseClaimsJws(jwt).getBody()
                        .getSubject();
            } catch (Exception e) {
                returnValue = false;
            }
            if (subject == null || subject.isEmpty())
                returnValue = false;
    
            return returnValue;
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
    
            String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
            if (authorizationHeader == null) {
                throw new AccessDeniedException("인증 정보 누락");
    
            }
    
            String jwt = authorizationHeader.replace("Bearer", "").trim();
            if (!isJwtValid(jwt)) {
                throw new AccessDeniedException("인증 오류");
    
            }
            return super.preHandle(request, response, handler);
        }
    }
    

     

    5.

    WebMvcConfigurer implements하는 클래스 추가

    @Configuration
    public class WebMvcConfiguration implements WebMvcConfigurer {
    
        @Autowired
        Environment env;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new TokenValidationInterceptor(env))
                    .addPathPatterns("/**")
                    .excludePathPatterns("/v1/lending");
        }
    
    	@Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**").allowedMethods("*").allowedOriginPatterns("*");
        }
    }

    - 아까 antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣은것처럼 여기에서도 exclude에 넣기

    왜 두번하는진 나중에 찾아보겠습니다

     

    ---인가 끝---

     

    =================

    [2]인증+인가 : admin, auth

     

    1.

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

    추가

     

    2.

    WebSecurityConfigurerAdapter를 상속하는 클래스 만들기

    package com.cloudlibrary.admin.ui.security;
    
    import com.cloudlibrary.admin.application.service.AdminService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.core.env.Environment;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        AdminService adminService;
        @Autowired
        Environment env;
    
        @Bean
        static public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .cors();
            http
                    .authorizeRequests()
                    .antMatchers("/v1/admin/health-check").permitAll()
                    .antMatchers("/v1/admin/signup").permitAll()
                    .antMatchers("/v1/admin/findid").permitAll()
                    .antMatchers("/v1/admin/findpw").permitAll();
    
            http
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .addFilter(getAuthenticationFilter());
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(adminService).passwordEncoder(passwordEncoder());
        }
    
        private AuthenticationFilter getAuthenticationFilter() throws Exception {
            AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(), adminService, env);
            return authenticationFilter;
        }
        
        @Bean
        public CorsConfigurationSource corsConfigurationSource(){
    
            CorsConfiguration configuration = new CorsConfiguration();
    
            configuration.addAllowedOriginPattern("*");
            configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","PATCH","OPTIONS","DELETE"));
            configuration.setAllowedHeaders(Arrays.asList("*"));
            configuration.setAllowCredentials(true);
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
    
            return source;
        }
    }

    - 패스워드를 암호화시키는 BCryptPasswordEncoder을 스프링에 등록

    - antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣는다

    - userDetailsService라는 애한테 loadUserByUserName 이라는 애가 있을 서비스와 암호화방식을 등록해준다

    - 필터 리턴

    =>복붙하고 클래스명과 변수명만 바꿔주면 잘돌아갑니다

     

    3.

    UsernamePasswordAuthenticationFilter를 상속하는 클래스 만들기

    package com.cloudlibrary.admin.ui.security;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Date;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import com.cloudlibrary.admin.application.service.AdminReadUseCase;
    import com.cloudlibrary.admin.application.service.AdminService;
    import com.cloudlibrary.admin.ui.requestBody.AdminLoginRequest;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.env.Environment;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
        private final AdminService adminService;
        Environment env;
    
        @Autowired
        public AuthenticationFilter(AuthenticationManager authenticationManager, AdminService adminService, Environment env) {
            super.setAuthenticationManager(authenticationManager);
            super.setFilterProcessesUrl("/v1/admin/signin");
            this.adminService = adminService;
            this.env = env;
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
            try {
                AdminLoginRequest creds = new ObjectMapper().readValue(request.getInputStream(), AdminLoginRequest.class);
    
                Authentication authentication = new UsernamePasswordAuthenticationToken(
                        creds.getId(), creds.getPw(), new ArrayList<>());
                return getAuthenticationManager().authenticate(authentication);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                                Authentication authResult) throws IOException, ServletException {
            String userName = ((User) authResult.getPrincipal()).getUsername();
            AdminReadUseCase.FindAdminResult findAdminResult = adminService.getAdminById(userName);
    
            String token = Jwts.builder()
                    .setSubject(findAdminResult.getId())
                    .setExpiration(new Date(System.currentTimeMillis()
                            + Long.parseLong(env.getProperty("token.expiration_time"))))
                    .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
                    .compact();
    
            Cookie setCookie = new Cookie("token", token);
            response.addCookie(setCookie);
            response.addHeader("token", token);
            response.addHeader("adminId", Long.toString(findAdminResult.getAdminId()));
            response.addHeader("userId", findAdminResult.getId());
            response.addHeader("libraryName", findAdminResult.getLibraryName());
            response.addHeader("email", findAdminResult.getEmail());
        }
    }

    ---아래는 안 읽어도 되고 변수명만 바꿔주면 되는 부분---

    값이 넘어오면,

    vo로 바꾸고, 

    Manager의 authentication메소드에 전달하고,

    매니저는 여러 Provider들을 모아놨다가 객체가 넘어오면 그에 맞는 Provider를 선택해서,

    Provider에 의해 인증과정을 거치고

    성공 실패 여부에 따라 successful / unsuccessful 메소드가 호출됨

    successful / unsuccessful 는 request, response, FilterChain, Authentication(Result)등을 전달받고, Response를 돌려줌

     

    - AuthenticationFilter는 UsernamePasswordAuthenticationFilter클래스를 상속받는데, 여기엔 PathMatcher가 있고, [POST] /login 으로 오는 로그인 Request를 해당 Filter에서 인터셉트할 수 있다

    - 이 클래스의 attemptAuthentication()은 토큰을 생성하는데, 

     

    id pwd기반이라서, UsernamePasswordAuthenticationFilter를 쓸 것

    (1)attemptAuthentication() 메서드와 (2)successfulAuthentication() 메서드를 구현해 줘야 함

     

    (1)attemptAuthentication() : HTTP Request가 처음으로 들어오는 메소드,

    - Request Body에 있는 데이터를 내 vo에 넣고,

    - Provider Manager의 authentication메소드에 전달함.

     

    *JSON을 Java로 deserialization 하기 위해 ObjectMapper의 readValue()메소드를 이용, request한테서 RequestLogin데이터를 추출해서 가져옴

     

    (2)successfulAuthentication()

    Provider에 의해서 인증과정을 거치고, 인증의 성공/실패 여부에 따라서 호출되는 메소드. authentication의 결과가 성공이면 인증된 객체가 successfulAuthentication 메소드로 돌아오고, 발행된 토큰과 같은 적절한 데이터를 포함한 HTTP Response를 돌려주면 된다

    *결과가 실패면 인자로 넘어온 예외를 활용해 사용자에게 적절한 데이터를 보여줘 이후 프로세스를 진행할 수 있도록

     

    Authentication 는 인터페이스고, 인증객체는 이 인터페이스를 구현한 모든 클래스의 객체를 말함

    Authentication 인터페이스에는 구현해야할 6개의 메소드가 있는데 이미 구현해놓은 클래스들이 있다. 예를 들어 UsernamePasswordAuthenticationToken 클래스이다.

    이걸로 List를 만들고, manager의 메소드에 넣는다

     

    authResult.getPrincipal() 에 담겨오는건 문자열, 문자열로 캐스팅(간단하게 해당 사용자를 고유하게 식별할 수 있는 객체가 들어가는 공간, String형 객체에 아이디를 저장해 사용할 수도 있다)

     

    맨 아래에는 토큰 생성 로직

     

    4.

    Service클래스로 가서 UserDetailsService를 상속받고 loadUserByUsername() 메소드 오버라이드

    @Slf4j
    @Service
    public class AdminService implements AdminOperationUseCase, AdminReadUseCase {
    
        private final AdminEntityRepository adminEntityRepository;
        private final AdminMapper adminMapper;
    
        @Autowired
        public AdminService(AdminEntityRepository adminEntityRepository
                , AdminMapper adminMapper){
            this.adminEntityRepository = adminEntityRepository;
            this.adminMapper = adminMapper;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Optional<AdminEntity> resultId = adminMapper.findAdminById(username);
            if (resultId == null) {
                throw new UsernameNotFoundException(username);
            }
            return new User(resultId.get().getId(), resultId.get().getEncryptedPw(),
                    true, true, true, true, new ArrayList<>());
        }
    }

    사용자 정보를 조회 -> 존재하는 경우 -> 스시 내부에서 사용하고 있는 User객체 형태로 변환

     

    5.

    token.expiration_time = 86400000
    token.secret = usertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkoreausertokensecretcloudlibraryhhjsisywsyjsdkyjfightingkorea

    application.properties에 이것 추가

     

    6.

    JWT토큰을 검증하는 인터셉터, HandlerInterceptorAdapter를 상속하는 클래스 만들기

    package com.cloudlibrary.admin.ui.security;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.env.Environment;
    import org.springframework.http.HttpHeaders;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import io.jsonwebtoken.Jwts;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    @SuppressWarnings("deprecation")
    public class TokenValidationInterceptor extends HandlerInterceptorAdapter {
    
        Environment env;
    
        public TokenValidationInterceptor(Environment env) {
            this.env = env;
        }
    
        private boolean isJwtValid(String jwt) {
            boolean returnValue = true;
    
            String subject = null;
            try {
                subject = Jwts.parser().setSigningKey(env.getProperty("token.secret")).parseClaimsJws(jwt).getBody()
                        .getSubject();
            } catch (Exception e) {
                returnValue = false;
            }
            if (subject == null || subject.isEmpty())
                returnValue = false;
    
            return returnValue;
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
    
            String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
            if (authorizationHeader == null) {
                throw new AccessDeniedException("인증 정보 누락");
    
            }
    
            String jwt = authorizationHeader.replace("Bearer", "").trim();
            if (!isJwtValid(jwt)) {
                throw new AccessDeniedException("인증 오류");
    
            }
            return super.preHandle(request, response, handler);
        }
    }
    

    7.

    WebMvcConfigurer를 implements하는 애 만들고 인터셉터 등록

    - 아까 antMatchers뒤에 로그인 안해도 허용할수있는 API를 넣은것처럼 여기에서도 exclude에 넣기

    package com.cloudlibrary.admin.ui.security;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.env.Environment;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebMvcConfiguration implements WebMvcConfigurer {
    
        @Autowired
        Environment env;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new TokenValidationInterceptor(env))
                    .addPathPatterns("/**")
                    .excludePathPatterns("/v1/admin/signup")
                    .excludePathPatterns("/v1/admin/findid")
                    .excludePathPatterns("/v1/admin/findpw");
        }
        
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**").allowedMethods("*").allowedOriginPatterns("*");
        }
    }

     

    ---끝---

    'Spring > SpringSecurity' 카테고리의 다른 글

    Spring Boot JWT Tutorial (4)권한 다른 API  (0) 2022.04.13
    Spring Boot JWT Tutorial (3)  (0) 2022.04.12
    Spring Boot JWT Tutorial (2)JWT  (0) 2022.04.12
    Spring Boot JWT Tutorial (1)설정, DB  (0) 2022.04.12
Designed by Tistory.