Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

끄적끄적

[spring] security와 JWT 사용하기 본문

개발

[spring] security와 JWT 사용하기

으아아아앜 2019. 12. 23. 19:49

학습한 내용을 정리한 것입니다.

생략된 내용들과 부정확할 수 있는 내용들에 주의해주세요.

 

- 구조도

- security 설정

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

    @Autowired
    SignInProvider signInProvider;
    @Autowired
    SignInFilterSuccessHandler signInFilterSuccessHandler;
    @Autowired
    SignInFilterFailureHandler signInFilterFailureHandler;

    @Autowired
    JwtProvider jwtProvider;
    @Autowired
    JwtFilterSuccessHandler jwtFilterSuccessHandler;
    @Autowired
    JwtFilterFailureHandler jwtFilterFailureHandler;


    //filter 생성 및 Manager에 등록
    @Bean
    public JwtFilter jwtFilter() throws Exception {
        FilterSkipMatcher filterSkipMatcher = new FilterSkipMatcher(
                Arrays.asList(
                        "/api/account/signIn",
                        "/api/account/signOut",
                        "/api/account/authenticate"), "/api/**");
        JwtFilter jwtFilter = new JwtFilter(filterSkipMatcher, jwtFilterSuccessHandler, jwtFilterFailureHandler);
        jwtFilter.setAuthenticationManager(super.authenticationManagerBean());
        return jwtFilter;
    }
    @Bean
    public SignInFilter signInFilter() throws Exception{
        SignInFilter signInFilter = new SignInFilter("/api/account/signIn",signInFilterSuccessHandler, signInFilterFailureHandler);
        signInFilter.setAuthenticationManager(super.authenticationManagerBean());
        return signInFilter;
    }
    @Bean
    public SignOutFilter signOutFilter() throws Exception{
        SignOutFilter signOutFilter = new SignOutFilter("/api/account/signOut");
        signOutFilter.setAuthenticationManager(super.authenticationManagerBean());
        return signOutFilter;
    }

    //Manager 등록
    @Bean
    public AuthenticationManager getAuthenticationManager() throws Exception{
        return super.authenticationManagerBean();
    }

    //Provider 등록
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .authenticationProvider(this.jwtProvider)
                .authenticationProvider(this.signInProvider);
    }

    //Filter 등록
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http
                .csrf().disable();

        http
                .headers().frameOptions().disable();

        http
                .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(signInFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(signOutFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

사용자의 request를 어떤 filter가 처리할 것인지, 어떤 provider를 manager에 등록할 것인지 등의 설정입니다.

- token

  • UsernamePasswordAuthenticationToken은 세큐리티가 디폴트로 제공하는 Authentication 인터페이스의 구현체
  • UsernamePasswordAuthenticationToken을 상속받는 커스텀 토큰을 생성하여 목적에 따라 구분
  • 이 token의 클래스타입을 보고 어떤 provider가 처리할 것인지 판단
  • UsernamePasswordAuthenticationToken를 생성자2개로 생성하면 인증받기 전 토큰을 뜻함
  • UsernamePasswordAuthenticationToken를 생성자3개로 생성하면 인증받은 후 토큰을 뜻함

 

여기서는 아래의 3가지 토큰을 정의

 

  • SignInPreToken(로그인 전)
public class SignInPreToken extends UsernamePasswordAuthenticationToken {

    public SignInPreToken(String email, String password) {
        super(email, password);
    }

    public SignInPreToken(AccountDTO.SignIn dto) {
        this(dto.getEmail(), dto.getPassword());
    }

    public String getEmail(){
        return (String)super.getPrincipal();
    }
    public String getPassword(){
        return (String)super.getCredentials();
    }
}

 

  • SignInPostToken(로그인 후) 
public class SignInPostToken extends UsernamePasswordAuthenticationToken {

    public SignInPostToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }

    public String getEmail(){
        return (String)super.getPrincipal();
    }
    public String getPassword(){
        return (String)super.getCredentials();
    }


    public AccountSecurityDTO getAccountSecurityDTO() {
        return (AccountSecurityDTO) super.getPrincipal();
    }

    public AccountDetails getAccountDetails(){
        return new AccountDetails(this.getEmail(), this.getPassword(), super.getAuthorities());
    }
}

 

  • JwtPreToken(서버 REST API 요청),
public class JwtPreToken extends UsernamePasswordAuthenticationToken {

    public JwtPreToken(String token) {
        super(token, token.length());
    }

    public String getToken(){
        return (String)super.getPrincipal();
    }
    public int getTokenLength(){
        return getToken().length();
    }
}

 

- filter

  • SignInFilter
public class SignInFilter extends AbstractAuthenticationProcessingFilter {

    private SignInFilterSuccessHandler successHandler;
    private SignInFilterFailureHandler failureHandler;

    public SignInFilter(String defaultUrl){
       super(defaultUrl);
    }
    public SignInFilter(String defaultUrl,
                        SignInFilterSuccessHandler successHandler,
                        SignInFilterFailureHandler failureHandler){
        this(defaultUrl);
        this.successHandler = successHandler;
        this.failureHandler = failureHandler;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {

        AccountDTO.SignIn dto = new ObjectMapper().readValue(req.getReader(),AccountDTO.SignIn.class);
        SignInPreToken token = new SignInPreToken(dto);

        return super.getAuthenticationManager().authenticate(token);
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        this.successHandler.onAuthenticationSuccess(req,res,authResult);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException {
        this.failureHandler.onAuthenticationFailure(req,res,failed);
    }

}

@Override한 attempAuthentication 메소드에서 req에서 로그인 정보를 얻고, 그에 따라 SignInPreToken을 생성하여 ProviderManager에게 넘깁니다. 

 

successfulAuthentication과 unsuccessfulAuthentication은 각각 필터가 요청을 성공,실패 했을때 동작하는 메소드들입니다.

 

 

 

아래 코드는 로그인이 성공했을때의 로직입니다. (this.successHandler.onAuthenticationSuccess 의 내용)

 

 

@Component
public class SignInFilterSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    JwtFactory jwtFactory;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) throws IOException, ServletException {
        String jwt = jwtFactory.generateToken(((SignInPostToken) auth).getAccountSecurityDTO());
        Cookie cookie = new Cookie("jwt-token", jwt);
        cookie.setPath("/");
        cookie.setMaxAge(60*60);
        res.addCookie(cookie);
    }
}

jwtFactory를 통해 토큰을 생성하고, 쿠키에 담아 클라이언트에게 응답해줍니다.

 

 

아래는 JwtFactory 입니다.

@Slf4j
@Component
public class JwtFactory {
    @Value("${jwt.secret}")
    private String secret;

    public String generateToken(AccountSecurityDTO dto) {

        String token = null;
        try {
            token = JWT.create()
                    .withIssuer("kong")
                    .withClaim("email", dto.getEmail())
                    .withClaim("nickname", dto.getNickname())
                    .withClaim("role", dto.getRole().toString())
                    .withClaim("gen-time", new Date())
                    .sign(generateAlgorithm());

        } catch (Exception e) {
            log.error(e.getMessage());
        }

        return token;
    }

    private Algorithm generateAlgorithm() throws UnsupportedEncodingException {
        return Algorithm.HMAC256(secret);
    }

}

 

  • SignOutFilter
public class SignOutFilter extends AbstractAuthenticationProcessingFilter {

    public SignOutFilter(String defaultUrl){
       super(defaultUrl);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
        res.addCookie(JwtCookieUtil.createSignOutCookie());
        SecurityContextHolder.clearContext();
        return null;
    }
}

@Override한 attempAuthentication 메소드에서 로그아웃처리를 하고있다. 여기서는 jwt토큰을 쿠키에 담아서 사용함으로, 해당 쿠키를 제거함으로써(위 코드에서는 max-age가 0인 쿠키를 넣어줌으로써) 로그아웃을 구현하고 있다.

 

  • JwtFilter
public class JwtFilter extends AbstractAuthenticationProcessingFilter {

    private JwtFilterSuccessHandler successHandler;
    private JwtFilterFailureHandler failureHandler;

    public JwtFilter(RequestMatcher requestMatcher){
       super(requestMatcher);
    }
    public JwtFilter(RequestMatcher requestMatcher,
                     JwtFilterSuccessHandler successHandler,
                     JwtFilterFailureHandler failureHandler){
        this(requestMatcher);
        this.successHandler = successHandler;
        this.failureHandler = failureHandler;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
        String jwtToken = JwtCookieUtil.getJwtCookieValue(req);
        return super.getAuthenticationManager().authenticate(new JwtPreToken(jwtToken));
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        this.successHandler.onAuthenticationSuccess(req,res,authResult);
        chain.doFilter(req,res);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException {
        this.failureHandler.onAuthenticationFailure(req,res,failed);
    }

}

@Override한 attempAuthentication 메소드에서 jwt토큰이 담겨있는 쿠키를 추출하고, jwt문자열을 얻어낸다.

그리고 그것을 JwtPreToken에 담아 JwtProvider에서 처리하도록 넘겨준다.

그리고 chain.doFilter(req,res)를 통해 요청이 디스패처 서블릿까지 도달하도록 해줍니다.

 

아래는 jwt토큰의 유효성 검사가 성공할때 작동하는, @Override successfulAuthentication 메소드의 로직입니다.

(this.successHandler.onAuthenticationSuccess의 로직)

 

@Component
public class JwtFilterSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) throws IOException, ServletException {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(auth);
        SecurityContextHolder.setContext(context);
    }
}

- provider 

  • SignInProvider
@Component
public class SignInProvider implements AuthenticationProvider {
    @Autowired
    AccountDetailsService detailsService;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SignInPreToken token = (SignInPreToken) authentication;
        AccountDetails details = (AccountDetails) detailsService.loadUserByUsername(token.getEmail());
        details.validatePreToken(token,passwordEncoder);
        return details.getPostToken(details);
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return SignInPreToken.class.isAssignableFrom(aClass) ;
    }
}

SignInFilter에게 넘겨받은 토큰을 통해 이메일을 알아냅니다.

해당 이메일을 통해 DB에서 PASSWORD를 알아내고, 로그인이 가능한지 판단합니다.

로그인이 가능하다면, SignInPostToken(details.getPostToken)을 생성하여 SignInFilter에 다시 넘겨줍니다.

 

  • JwtProvider
@Component
public class JwtProvider implements AuthenticationProvider {

    @Autowired
    JwtDecoder jwtDecoder;
    @Autowired
    AccountDetailsService detailsService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        JwtPreToken token = (JwtPreToken) authentication;
        if(isAnonymous(token)){
            return token;
        }
        DecodedJWT decodedJWT = jwtDecoder.decodeJwt(token.getToken());

        SignInPostToken signInPostToken = (SignInPostToken) SecurityContextHolder.getContext().getAuthentication();

        if(signInPostToken == null){
            String email = decodedJWT.getClaim("email").asString();
            AccountDetails details = (AccountDetails) detailsService.loadUserByUsername(email);
            return details.getPostToken(details);
        }
        return signInPostToken;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return JwtPreToken.class.isAssignableFrom(aClass);
    }

    private boolean isAnonymous(JwtPreToken token){
        return token.getToken().equals("");
    }
}

JwtFilter에게 넘겨받은 토큰을 통해 jwt-token 문자열을 알아냅니다.

해당 문자열을 jwtDecoder를 통해 유효성을 검증하고, 문제가 없다면 SignInPostToken을 생성하여 JwtFilter에게 다시 돌려줍니다.

 

* 현재 위 로직은 적절치 않습니다. (매번 db에서 정보를 쿼리해와야합니다)

JwtPreToken안에 있는 정보만으로 SignInPostToken을 생성할 수 있도록 로직을 변경하여야합니다.

(따라서, JwtFactory의 로직변경 필요)

 

아래는 JwtDecoder입니다.

@Slf4j
@Component
public class JwtDecoder {
    @Autowired
    AccountDetailsService detailsService;
    @Value("${jwt.secret}")
    private String secret;

    public DecodedJWT decodeJwt(String token) {
        return isValidToken(token).orElseThrow(() -> new InvalidJwtException("유효한 토큰아 아닙니다."));
    }

    private Optional<DecodedJWT> isValidToken(String token) {

        DecodedJWT jwt = null;

        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm).build();

            jwt = verifier.verify(token);
        } catch (Exception e) {
            log.error(e.getMessage());
        }

        return Optional.ofNullable(jwt);
    }

}

 

 

 

 

 

git : https://github.com/DingDongDeng/MyBoard

 

 

 

 

'개발' 카테고리의 다른 글

[spring] database 연결 테스트 코드  (0) 2019.12.24
[spring] properties 여러개 사용하기  (0) 2019.12.24
[database] 자주 쓰는 SQL  (0) 2019.12.24
[intelij] 인텔리제이 환경설정하기  (0) 2019.12.24
[spring] @Valid 활용하기  (0) 2019.12.23
Comments