꼬물꼬물

Spring Security 설정하기 본문

코딩/환경설정

Spring Security 설정하기

멩주 2022. 4. 24. 19:59

User에게 권한을 부여해 사용자를 관리하자

build.gradle 파일에서 spring security설정

dependencies { 
    implementation 'org.springframework.boot:spring-boot-starter-security' 
    testImplementation 'org.springframework.security:spring-security-test' 
    implementation 'com.auth0:java-jwt:3.18.2' 
    implementation 'io.jsonwebtoken:jjwt:0.9.1' 
}

PrincipalDetails.java

  • User 객체를 사용한다.
public class PrincipalDetails implements UserDetails {
    private User user; // User 객체를 사용한다.
    public PrincipalDetails(User user) { this.user = user; } // 해당 유저의 권한 리턴

    @Override public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(() -> user.getUser_auth().toString());
        return authorities;
    }

    @Override public String getPassword() { return user.getUser_pwd(); }

    @Override public String getUsername() { return user.getUser_email(); }

    @Override public boolean isAccountNonExpired() { return true; }

    @Override public boolean isAccountNonLocked() { return true; }

    @Override public boolean isCredentialsNonExpired() { return true; }

    @Override public boolean isEnabled() { // 멈춰뒀다면 7일 정지 return true; }

}

PrincipalDetailsService.java

// http://localhost:{포트번호}/login이 요청될 때 실행된다.

@Service  
@RequiredArgsConstructor  
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    // user_email이 user_name으로 설정했다.
    public UserDetails loadUserByUsername(String user_email) throws UsernameNotFoundException {

    // System.out.println("PrincipalDetailsService의 loadUserByUsername()");  
    User userEntity = userRepository.findByEmail(user\_email);  
    return new PrincipalDetails(userEntity);  
    }  
}

Spring Security 설정하기

CorsConfig.java

@Configuration  
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter(){
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*"); // 모든 ip에 응답 허용
        config.addAllowedHeader("*"); // 모든 header에 응답 허용
        config.addAllowedMethod("*"); // 모든 post, get.. 응답 허용
        config.addExposedHeader("Authorization");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

SecurityConfig.java

@Configuration  
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록된다. 활성화  
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // 특정 주소로 접근하면 권한 및 인증을 미리 체크하겠다는 뜻(Secured 어노테이션 활성화), preAuthorize라는 어노테이션 활설화  
@RequiredArgsConstructor  
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CorsFilter corsFilter;

    private final UserRepository userRepository;

    @Bean // 해당 메서드의 리턴되는 오브젝트를 IoC로 등록
    public BCryptPasswordEncoder encoderPwd(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and();
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session 사용하지 않겠다.
                .and()
                .addFilter(corsFilter) // Filter 추가
                .formLogin().disable() // form tag 만들어 로그인하지 않겠다.
                .httpBasic().disable() // 기본인증 방식 -> 우리는 barear 사용
                .addFilter(new JwtAuthenticationFilter(authenticationManager())) // AuthenticationManager 파라미터를 넘겨야한다.
                .addFilter(new JwtAuthorizationFilter(authenticationManager(), userRepository))
                .authorizeRequests()
                .antMatchers("/user/**") // /user로 들어오는 사용자는 user과 admin만 사용 가능하다.
                .access("hasAnyRole('USER', 'ADMIN')")
                .antMatchers("/admin/**") // /admin으로 들어오는 사용자는 admin만 사용 가능하다.
                .access("hasAnyRole('ADMIN')")
                .anyRequest().permitAll();
    }
}

jwt token 사용

jwtAutheriticationFilter.java

// 스프링 시큐리티에서 UsernamePasswordAuthenticationFilter가 있음.
// /login 요청해서 username, password 전송하면(post)
// UsernamePasswordAuthenticationFilter가 동작
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager; // SecurityConfig에서 받아옴


    // /login 요청을 하면 로그인 시도를 위해서 실행되는 함수
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        System.out.println("JwtAuthenticationFilter: 로그인 시도중");

        // 1. username과 password를 받아서
        Authentication authentication = null;
        try {
            ObjectMapper om = new ObjectMapper();
            User user = om.readValue(request.getInputStream(), User.class);

            // 로그인 시도시 토큰 생성
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(user.getUser_email(), user.getUser_pwd());

            // 토큰으로 로그인 시도
            // PrincipalDetailsService의 loadUerByUsername() 실행
            // 로그인이 정상적으로 실행될 경우 authentication에 로그인한 정보가 담긴다.
            authentication = authenticationManager.authenticate(authenticationToken);

            // authentication이 session 영역에 저장된다. => 로그인 성공(db와 일치한다,)
            PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();

            // authentication 객체가 session영역에 저장해야 하고 그 방법이 return
            // 리턴의 이유는 권한 관리를 security가 대신 해주기 때문에 편하기 위해서
            // JWT 토큰을 사용하면서 세션을 만들 이유는 없지만 단지 권한 처리 때문에
            // attemptAuthentication 함수 실행 후 successfulAuthentication 함수가 실행된다.
            return authentication;
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 2. 로그인 시도 authenticationManager로 로그인 시도하면 PrincipalDetailsService가 호출된다
        // loadUserByUsername 자동 실행

        // 3. 리턴되면 PrincipalDetails를 세션에 담고(권한 관리를 위해서)

        // 4. JWT 토큰을 만들어 응답
        return null;
    }

    // attemptAuthentication 실행 후 인증이 정상적으로 되었으면 해당 함수 호출
    // JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해야 한다.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        System.out.println("로그인 성공!");
        PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();

        // RSA 방식이 아닌 Hash 암호방식
        String jwtToken = JWT.create()
                .withSubject(principalDetails.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPORATION_TIME))
                .withClaim("user_no", principalDetails.getUser().getUser_no()) // 넣고 싶은 key value 값
                .withClaim("user_name", principalDetails.getUser().getUser_email())
                .sign(Algorithm.HMAC512(JwtProperties.SECRET)); // secrete 값

        response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX +jwtToken); // 헤더에 담겨 사용자에게 응답
//        System.out.println(principalDetails.getUser().getUser_email());
//        System.out.println(principalDetails.getUser().getUser_pwd());
        Result result = new Result(true, HttpStatus.OK.value(), new UserDto(principalDetails.getUser().getUser_no(), principalDetails.getUser().getUser_email()));
        String json = new ObjectMapper().writeValueAsString(result);
//        System.out.println(json);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(json);
        response.setHeader("Access-Control-Allow-Headers", "Authorization");
        // 보통
        // 서버에서 세션 ID를 생성하고 클라이언트에게 응답해준다.
        // 요청할 때마다 쿠키값 세션 ID를 항상 들고 서버에게 요청하기 때문에
        // 서버는 세션 ID가 유효한지 판단해 유효할 경우에만 인증이 필요한 페이지에 접근 가능하게 한다.

        // 여기서
        // email, pwd가 정상이라면 JWT 토큰을 생성해 유저에게 header에 담아 응답한다.
        // 클라이언트는 JWT 토큰을 가지고 요청하며 서버에서 JWT 토큰이 유효한지 판단 -> 필터
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        System.out.println("로그인 실패!");
        Result result = new Result(false, HttpStatus.BAD_REQUEST.value());
        String json = new ObjectMapper().writeValueAsString(result);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(json);
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class UserDto{
        private int user_no;
        private String user_email;

        public UserDto(User user) {
            this.user_no = user.getUser_no();
            this.user_email = user.getUser_email();
        }
    }
}

jwtAuthorizationFilter.java

// 시큐리티가 filter를 가지고 있는데 그 필터 중에 BasicAuthenticationFilter이 있다.
// 권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어있다.
// 만약 권한이 인증이 필요한 주소가 아니라면 필터 안탄다.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private UserRepository userRepository;

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
        super(authenticationManager);
        this.userRepository = userRepository;
    }

    // 인증이나 권한이 필요한 주소 요청이 있을 경우 => /user
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String jwtHeader = request.getHeader("Authorization");

        // header가 있는지 확인
        if(jwtHeader == null || !jwtHeader.startsWith("Bearer")){
            chain.doFilter(request, response);
            return ;
        }

        System.out.println("인증이나 권한이 필요한 주소 요청이 있을 경우");
        // JWT 토큰을 검증해서 정상적인 사용자인지 확인
        String jwtToken = request.getHeader("Authorization").replace("Bearer ","");
        int user_no = JWT.require(Algorithm.HMAC512("challympic")).build().verify(jwtToken).getClaim("user_no").asInt();

        // 서명이 정상적으로 됨
        if(user_no > 0){
            System.out.println("서명 정상이야?");
            User user = userRepository.findOne(user_no);

            PrincipalDetails principalDetails = new PrincipalDetails(user);
            // jwt 토큰 서명을 통해서 서명이 정상이면 Authentication 객체를 만들어준다.
            Authentication authentication =
                    new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());

            // 강제로 security의 세션에 접근하여 Authentication 객체를 저장
           SecurityContextHolder.getContext().setAuthentication(authentication);


           chain.doFilter(request, response);
        }else{
            System.out.println("서명 정상아냐ㅠㅠ");
        }

    }
}

jwtProperties.java

public interface JwtProperties {  
    String SECRET = "{우리 서버만 아는 비밀 값}";  
    int EXPORATION\_TIME = 60000 \* 1000; // 10일  
    String TOKEN\_PREFIX = "Bearer ";  
    String HEADER\_STRING = "Authorization";  
}

'코딩 > 환경설정' 카테고리의 다른 글

Git 연동하기  (0) 2022.08.26
DBeaver MariaDB 연결하기  (0) 2022.06.13
QueryDSL  (0) 2022.03.24
MariaDB/Intellij 연결  (0) 2022.01.28
Lombok(롬복) 설정  (0) 2022.01.19