Spring/SpringSecurity

인증, 인가를 프로젝트에 끼워넣기

finepiz 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("*");
    }
}

 

---끝---