[Spring Security] AuthToken, RefreshToken을 통한 로그인 예시

WOOK JONG KIM·2022년 12월 9일
0

패캠_java&Spring

목록 보기
95/103
post-thumbnail

Authentication Token

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AdvancedSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SpUserService spUserService;

    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // LoginFilter의 경우 UserAuthenticationManager가 있기에 매니저에게 검증을 위임하면 되지만
        // CheckFilter는 사용자를 직접 가져올 상황이 생김
        JWTLoginFilter loginFilter = new JWTLoginFilter(authenticationManager());
        JWTCheckFilter checkFilter = new JWTCheckFilter(authenticationManager(), spUserService);
        http
                .csrf().disable() // 토큰을 쓸려면 csrf disable 하는 것이 좋음(비용이 많이듬)
                .sessionManagement(session ->{
                    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })
                .addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAt(checkFilter, BasicAuthenticationFilter.class);
    }
}
public class JWTCheckFilter extends BasicAuthenticationFilter {

    private SpUserService spUserService;

    // 토큰을 검사해서 security context holder에 user principal 정보를 채워 주는 역할
    public JWTCheckFilter(AuthenticationManager authenticationManager, SpUserService spUserService) {
        super(authenticationManager);
        this.spUserService = spUserService;
    }

    // 이를 통해 토큰을 통한 검사
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String bearer = request.getHeader(HttpHeaders.AUTHORIZATION);
        if(bearer == null || !bearer.startsWith("Bearer ")){
            chain.doFilter(request, response); // 그냥 흘려보내줘야 함(ex 인증이 필요하지 않은 경우)
            return;
        }

        String token = bearer.substring("Bearer ". length());
        VerifyResult result = JWTUtil.verifyResult(token);
        if(result.isSuccess()){
            SpUser user = (SpUser)spUserService.loadUserByUsername(result.getUsername());
            UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(
                    user.getUsername(), null, user.getAuthorities()
            );
            SecurityContextHolder.getContext().setAuthentication(userToken);
            chain.doFilter(request, response);
        } else{
            throw new AuthenticationException("Token is not valid");
        }
    }
}
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
    // 유효한 사용자에게 인증 필터를 내려주는 필터

    private ObjectMapper objectMapper = new ObjectMapper();

    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
        setFilterProcessesUrl("/login"); // POST LOGIN 처리
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException
    {
        UserLoginForm userLogin = objectMapper.readValue(request.getInputStream(), UserLoginForm.class);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                userLogin.getUsername(), userLogin.getPassword(), null
        );
        // user details 처리 부분 ...
        return getAuthenticationManager().authenticate(token);
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain,
            Authentication authResult) throws IOException, ServletException
    {
        SpUser user = (SpUser) authResult.getPrincipal();

        // login이 끝날려면 토큰을 발행해 줘야 함
        response.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + JWTUtil.makeAuthToken(user));
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getOutputStream().write(objectMapper.writeValueAsBytes(user));
    }
}
public class JWTUtil {

    private static final Algorithm ALGORITHM = Algorithm.HMAC256("wookjong");
    private static final long AUTH_TIME = 20 * 60; // 20분
    private static final long REFRESH_TIME = 60 * 60 * 24 * 7; // 1주일


    public static String makeAuthToken(SpUser user){
        return JWT.create()
                .withSubject(user.getUsername())
                // ExpiresAt으로도 가능
                .withClaim("exp", Instant.now().getEpochSecond() + AUTH_TIME)
                .sign(ALGORITHM);
    }

    public static String makeRefreshToken(SpUser user){
        return JWT.create()
                .withSubject(user.getUsername())
                .withClaim("exp", Instant.now().getEpochSecond() + REFRESH_TIME)
                .sign(ALGORITHM);
    }

    public static VerifyResult verifyResult(String token){
        try{
            DecodedJWT verify = JWT.require(ALGORITHM).build().verify(token);
            return VerifyResult.builder().success(true)
                    .username(verify.getSubject()).build();
        }catch(Exception e){
            DecodedJWT decode = JWT.decode(token);
            return VerifyResult.builder().success(false)
                    .username(decode.getSubject()).build();
        }

    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserLoginForm {

    private String username;
    private String password;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class VerifyResult {

    private boolean success; // verify success
    private String username;

}
@RestController
public class HomeController {

    @PreAuthorize("isAuthenticated()") // 로그인을 했을 때 리퀘스트 보낼수 있음
    @GetMapping("/greeting")
    public String greeting(){
        return "hello";
    }
}
public class JWTRequestTest extends WebIntegrationTest {

    @Autowired
    private SpUserRepository userRepository;

    @Autowired
    private SpUserService userService;

    @BeforeEach
    void before(){
        userRepository.deleteAll();

        SpUser user = userService.save(SpUser.builder()
                .email("user1")
                .password("1111")
                .enabled(true)
                .build());

        userService.addAuthority(user.getUserId(), "ROLE_USER");

    }

    @DisplayName("1. hello 메세지 받아오기 ")
    @Test
    void test_1(){

        RestTemplate client = new RestTemplate();

        HttpEntity<UserLoginForm> body = new HttpEntity<>(
            UserLoginForm.builder().username("user1").password("1111").build()
        );
        ResponseEntity<SpUser> resp1 = client.exchange(uri("/login"), HttpMethod.POST, body, SpUser.class);
        System.out.println(resp1.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0));
        System.out.println(resp1.getBody());

        // greeting 메세지 받아오기
        HttpHeaders header = new HttpHeaders();
        // Bearer + Authentication Token이 와있음
        header.add(HttpHeaders.AUTHORIZATION, resp1.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0));
        body = new HttpEntity<>(null, header);
        ResponseEntity<String> resp2 = client.exchange(uri("/greeting"), HttpMethod.GET, body, String.class);

        assertEquals("hello", resp2.getBody());
    }
}

Refresh Token

public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
    // 유효한 사용자에게 인증 필터를 내려주는 필터

    private ObjectMapper objectMapper = new ObjectMapper();

    private SpUserService userService;

    public JWTLoginFilter(AuthenticationManager authenticationManager, SpUserService userService) {
        super(authenticationManager);
        this.userService = userService;
        setFilterProcessesUrl("/login"); // POST LOGIN 처리
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException
    {
        UserLoginForm userLogin = objectMapper.readValue(request.getInputStream(), UserLoginForm.class);
        if(userLogin.getRefreshToken() == null) {
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                    userLogin.getUsername(), userLogin.getPassword(), null
            );
            // user details 처리 부분 ...
            return getAuthenticationManager().authenticate(token);
        } else{
            // refreshtoken이 내려온 경우
            VerifyResult verify = JWTUtil.verifyResult(userLogin.getRefreshToken());
            if(verify.isSuccess()){
                SpUser user = (SpUser) userService.loadUserByUsername(verify.getUsername());
                return new UsernamePasswordAuthenticationToken(
                        user, user.getAuthorities() // user를 principal로
                );
            }else{
                throw new TokenExpiredException("refresh token expired");
            }
        }
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain,
            Authentication authResult) throws IOException, ServletException
    {
        SpUser user = (SpUser) authResult.getPrincipal();

        // Bearer 토큰은 사실 서버에서 필요한 것이고, response에 꼭 담아서 갈 필요는 없다
//        response.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + JWTUtil.makeAuthToken(user));
        response.setHeader("auth_token", JWTUtil.makeAuthToken(user));
        // 다시 들어오면 refresh token 이 들어온 시점부터 다시 일주일이 늘어나는 설정..
        response.setHeader("refresh_token", JWTUtil.makeRefreshToken(user));
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getOutputStream().write(objectMapper.writeValueAsBytes(user));
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TokenBox {

    private String authToken;
    private String refreshToken;
}
public class JWTRequestTest extends WebIntegrationTest {

    @Autowired
    private SpUserRepository userRepository;

    @Autowired
    private SpUserService userService;

    @BeforeEach
    void before(){
        userRepository.deleteAll();

        SpUser user = userService.save(SpUser.builder()
                .email("user1")
                .password("1111")
                .enabled(true)
                .build());

        userService.addAuthority(user.getUserId(), "ROLE_USER");

    }

    private TokenBox getToken(){

        RestTemplate client = new RestTemplate();

        HttpEntity<UserLoginForm> body = new HttpEntity<>(
                UserLoginForm.builder().username("user1").password("1111").build()
        );

        ResponseEntity<SpUser> resp1 = client.exchange(uri("/login"), HttpMethod.POST, body, SpUser.class);
        return TokenBox.builder().authToken(resp1.getHeaders().get("auth_token").get(0))
                .refreshToken(resp1.getHeaders().get("refresh_token").get(0))
                .build();
    }

    private TokenBox refreshToken(String refreshToken){

        RestTemplate client = new RestTemplate();

        HttpEntity<UserLoginForm> body = new HttpEntity<>(
                UserLoginForm.builder().refreshToken(refreshToken).build()
        );

        ResponseEntity<SpUser> resp1 = client.exchange(uri("/login"), HttpMethod.POST, body, SpUser.class);
        return TokenBox.builder().authToken(resp1.getHeaders().get("auth_token").get(0))
                .refreshToken(resp1.getHeaders().get("refresh_token").get(0))
                .build();
    }

    @DisplayName("1. hello 메세지 받아오기 ")
    @Test
    void test_1(){

        TokenBox token = getToken();
        RestTemplate client = new RestTemplate();


        // greeting 메세지 받아오기
        HttpHeaders header = new HttpHeaders();
        // Bearer + Authentication Token이 와있음
        header.add(HttpHeaders.AUTHORIZATION, "Bearer" + token.getAuthToken());
        HttpEntity body = new HttpEntity<>(null, header);
        ResponseEntity<String> resp2 = client.exchange(uri("/greeting"), HttpMethod.GET, body, String.class);

        assertEquals("hello", resp2.getBody());
    }

    @DisplayName("2. 토큰 만료 테스트")
    @Test
    void test_2() throws InterruptedException {
        // Auth_time을 2초로 지정하였음

        TokenBox token = getToken();

        Thread.sleep(3000); // 이렇게 만료된다면 갱신 받아서 사용해야 함
        HttpHeaders header = new HttpHeaders();
        header.add(HttpHeaders.AUTHORIZATION, "Bearer" + token.getAuthToken());
        RestTemplate client = new RestTemplate();
        assertThrows(Exception.class, () -> {
            HttpEntity body = new HttpEntity<>(null, header);
            ResponseEntity<String> resp2 = client.exchange(uri("/greeting"), HttpMethod.GET, body, String.class);
        });

        token = refreshToken(token.getRefreshToken());
        HttpHeaders header2 = new HttpHeaders();
        header2.add(HttpHeaders.AUTHORIZATION, "Bearer " + token.getAuthToken());
        HttpEntity body = new HttpEntity<>(null, header2);
        ResponseEntity<String> resp3 = client.exchange(uri("/greeting"), HttpMethod.GET, body, String.class);

        assertEquals("hello", resp3.getBody());
    }

refresh token을 브라우저 환경에 내려주면 사용자는 자동 로그인이 되기떄문에 접속시 편리
-> 그러나 보안에 취약

위 코드의 경우는 refresh_token이 탈취된다면 이 토큰으로 접근하려는 사용자를 막을 수 없다
-> 서버에서 보안이 문제가 된다면 갱신된 토큰에 대한것을 추적하고, 만약 예외적인 경우에 사용된다고 판단된다면 refresh_token을 무력화 할수 있는 방법을 구현해야 함
-> ex) Db에 시리즈 형식으로 저장

profile
Journey for Backend Developer

0개의 댓글