programing

커스텀 토큰을 사용한 REST API 보안 보호(스테이트리스, UI, 쿠키, 기본 인증, OAuth, 로그인 페이지 없음)

newstyles 2023. 3. 15. 19:28

커스텀 토큰을 사용한 REST API 보안 보호(스테이트리스, UI, 쿠키, 기본 인증, OAuth, 로그인 페이지 없음)

Spring Security에서 REST API를 보호하는 방법을 보여주는 가이드라인이나 샘플코드가 많이 있지만 대부분은 웹 클라이언트를 상정하고 로그인 페이지, 리다이렉션, 쿠키 사용 등에 대해 이야기합니다.HTTP 헤더에서 커스텀토큰을 체크하는 단순한 필터로도 충분할 수 있습니다.다음 요건을 충족하기 위해 보안을 구현하려면 어떻게 해야 합니까?같은 작업을 하고 있는 GIST/GITHUB 프로젝트가 있습니까?스프링 보안에 대한 저의 지식은 한정되어 있기 때문에, 스프링 보안에 대해 보다 간단하게 실장할 수 있는 방법이 있으면 가르쳐 주세요.

  • HTTPS를 통한 스테이트리스 백엔드에서 제공하는 REST API
  • 클라이언트는 웹 앱, 모바일 앱, 임의의 SPA 스타일 앱, 서드파티 API일 수 있습니다.
  • 기본 인증 없음, 쿠키 없음, UI 없음(JSP/HTML/static-resources 없음), 리다이렉션 없음, OAuth 공급자 없음.
  • HTTPS 헤더에 설정된 사용자 정의 토큰
  • 외부 스토어에 대한 토큰 검증(MemCached/Redis/ 또는 RDBMS 등)
  • 선택한 경로(/login, /signup, /public 등)를 제외한 모든 API를 인증해야 합니다.

Springboot, Spring Security 등을 사용합니다.Java 구성 솔루션 선호(XML 없음)

샘플 앱은 바로 이 작업을 수행합니다. 상태 비저장 시나리오에서 Spring Security를 사용하여 REST 엔드포인트를 보호합니다.개개의 REST 콜은 HTTP 헤더를 사용하여 인증됩니다.인증 정보는 서버 측에 인메모리 캐시에 격납되어 일반적인 웹 어플리케이션의 HTTP 세션에 의해 제공되는 것과 동일한 의미를 제공합니다.이 앱은 최소한의 커스텀 코드로 완전한 Spring Security 인프라스트럭처를 사용합니다.스프링 보안 인프라스트럭처 외부에서는 필터도 코드도 사용할 수 없습니다.

기본 개념은 다음 4개의 스프링보안 컴포넌트를 구현하는 것입니다.

  1. org.springframework.security.web.AuthenticationEntryPointREST 콜을 트랩하는데 필요한 인증 토큰이 없어 요구를 거부합니다.
  2. org.springframework.security.core.AuthenticationREST API에 필요한 인증 정보를 유지합니다.
  3. org.springframework.security.authentication.AuthenticationProvider(데이터베이스, LDAP 서버, 웹 서비스 등에 대해) 실제 인증을 수행합니다.
  4. org.springframework.security.web.context.SecurityContextRepositoryHTTP를 사용합니다.EHCACHE의 EHCACHE의 EHCACHE의 EHCACHE의 EHCACHE가 있습니다.

이 예에서는 XML 설정을 사용하고 있습니다만, 동등한 Java 설정을 간단하게 작성할 수 있습니다.

맞아, 쉽지 않고 좋은 예가 많지 않아.제가 본 예시는 다른 스프링 보안 용품을 나란히 사용할 수 없도록 만들었습니다.저도 최근에 비슷한 일을 했어요. 제가 한 일은 이렇습니다.

헤더 값을 보유하려면 사용자 지정 토큰이 필요합니다.

public class CustomToken extends AbstractAuthenticationToken {
  private final String value;

  //Getters and Constructor.  Make sure getAutheticated returns false at first.
  //I made mine "immutable" via:

      @Override
public void setAuthenticated(boolean isAuthenticated) {
    //It doesn't make sense to let just anyone set this token to authenticated, so we block it
    //Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
    if (isAuthenticated) {

        throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
    }

    super.setAuthenticated(false);
}
}

헤더를 추출하여 관리자에게 인증하도록 요청하려면 스프링 보안 필터가 필요합니다.이렇게 강조된 텍스트는

public class CustomFilter extends AbstractAuthenticationProcessingFilter {


    public CustomFilter(RequestMatcher requestMatcher) {
        super(requestMatcher);

        this.setAuthenticationSuccessHandler((request, response, authentication) -> {
        /*
         * On success the desired action is to chain through the remaining filters.
         * Chaining is not possible through the success handlers, because the chain is not accessible in this method.
         * As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
         * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
         * "Subclasses can override this method to continue the FilterChain after successful authentication."
         */
        });

    }



    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {


        String tokenValue = request.getHeader("SOMEHEADER");

        if(StringUtils.isEmpty(tokenValue)) {
            //Doing this check is kinda dumb because we check for it up above in doFilter
            //..but this is a public method and we can't do much if we don't have the header
            //also we can't do the check only here because we don't have the chain available
           return null;
        }


        CustomToken token = new CustomToken(tokenValue);
        token.setDetails(authenticationDetailsSource.buildDetails(request));

        return this.getAuthenticationManager().authenticate(token);
    }



    /*
     * Overriding this method to maintain the chaining on authentication success.
     * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
     * "Subclasses can override this method to continue the FilterChain after successful authentication."
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {


        //if this isn't called, then no auth is set in the security context holder
        //and subsequent security filters can still execute.  
        //so in SOME cases you might want to conditionally call this
        super.successfulAuthentication(request, response, chain, authResult);

        //Continue the chain
        chain.doFilter(request, response);

    }


}

스프링 보안 체인에 사용자 지정 필터 등록

 @Configuration
 public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {        

      //Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
      protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
        CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
        filter.setAuthenticationManager(this.authenticationManagerBean());
        return filter;
      }

       @Override
       protected void configure(HttpSecurity http) throws Exception {                  

            http
            //fyi: This adds it to the spring security proxy filter chain
            .addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
       }
}

필터로 추출된 토큰을 검증하는 커스텀 인증 프로바이더.

public class CustomAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {

        CustomToken token = (CustomToken)auth;

        try{
           //Authenticate token against redis or whatever you want

            //This i found weird, you need a Principal in your Token...I use User
            //I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
            org.springframework.security.core.userdetails.User principal = new User(...); 

            //Our token resolved to a username so i went with this token...you could make your CustomToken take the principal.  getCredentials returns "NO_PASSWORD"..it gets cleared out anyways.  also the getAuthenticated for the thing you return should return true now
            return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
        } catch(Expection e){
            //TODO throw appropriate AuthenticationException types
            throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
        }


    }

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


}

마지막으로 인증 매니저가 일부 @Configuration 클래스에서 프로바이더를 찾을 수 있도록 프로바이더를 bean으로 등록합니다.@Component it도 이 방법을 선호합니다.

@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies)  {
    return new CustomAuthenticationProvider(injectedDependencies);
}

이 코드는 모든 엔드포인트를 보호합니다. Boot 를 사용하여 됩니다.또한 Spring Boot Starter Security를 사용하여 정의해야 .UserDetailsService당신이 지나쳐가는 곳AuthenticationManagerBuilder.

쇼트 페이스트EmbeddedRedisConfiguration ★★★★★★★★★★★★★★★★★」SecurityConfig를 치환합니다.AuthenticationManagerBuilder츠키노

HTTP:

요청 토큰 - 요청 헤더에 기본 HTTP 인증 내용을 보냅니다.토큰은 응답 헤더로 반환됩니다.

http --print=hH -a user:password localhost:8080/v1/users

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af

같은 요청이지만 토큰 사용:

http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token:  cacf4a97-75fe-464d-b499-fcfacb31c8af

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

잘못된 사용자 이름/비밀번호 또는 토큰을 전달하면 401이 반환됩니다.

자바

는 그 를 나는에 했다.build.gradle

compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")

다음으로 Redis 설정

@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {

    private static RedisServer redisServer;

    @Bean
    public JedisConnectionFactory connectionFactory() throws IOException {
        redisServer = new RedisServer(Protocol.DEFAULT_PORT);
        redisServer.start();
        return new JedisConnectionFactory();
    }

    @PreDestroy
    public void destroy() {
        redisServer.stop();
    }

}

보안 구성:

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .requestCache()
                .requestCache(new NullRequestCache())
                .and()
                .httpBasic();
    }

    @Bean
    public HttpSessionStrategy httpSessionStrategy() {
        return new HeaderHttpSessionStrategy();
    }
}

.AuthenticationManagerBuilder를 사용합니다.inMemoryAuthentication자민당클래스의 정의를 확인해 주세요.하고 userDetailsService, 「」가 필요합니다.UserDetailsService★★★★★★ 。

마지막으로 리리지 service service 를 사용한 사용자 서비스CrudRepository.

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userRepository.findByEmail(username);
        if (userAccount == null) {
            return null;
        }
        return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

JWT - Jhipster를 사용한 또 다른 프로젝트 예시

JHipster를 사용하여 Microservice 응용 프로그램을 생성해 보십시오.Spring Security와 JWT가 즉시 통합되어 템플릿을 생성합니다.

https://jhipster.github.io/security/

JSON Web Tokens http://jwt.io/을 추천합니다.이것은 스테이트리스로 확장 가능합니다.

다음은 프로젝트 예시입니다.https://github.com/brahalla/Cerberus

언급URL : https://stackoverflow.com/questions/25317405/securing-rest-api-using-custom-tokens-stateless-no-ui-no-cookies-no-basic-au