REST APIとSpring Securityをセキュリティで保護する方法を示すサンプルコードがたくさんありますが、それらのほとんどはWebクライアントを想定し、ログインページ、リダイレクト、Cookieの使用などについて話します。 HTTPヘッダーのカスタムトークンをチェックする単純なフィルターでも十分かもしれません。以下の要件のセキュリティを実装するにはどうすればよいですか?Gist/githubプロジェクトで同じことを行いますか?春のセキュリティに関する私の知識は限られているため、 Spring Securityでこれを実装する簡単な方法を教えてください。
Springboot、Spring Securityなどを使用します。Java config(no XML)
私の サンプルアプリ はまさにこれを行います-RESTステートレスシナリオでSpring Securityを使用してエンドポイントを保護します。個々のREST呼び出しはHTTPヘッダー:認証情報はサーバー側のメモリ内キャッシュに保存され、一般的なWebアプリケーションのHTTPセッションで提供されるものと同じセマンティクスを提供しますアプリは、最小限のカスタムコードで完全なSpring Securityインフラストラクチャを使用します。裸のフィルター、Spring Securityインフラストラクチャの外部にコードはありません。
基本的な考え方は、次の4つのSpring Securityコンポーネントを実装することです。
org.springframework.security.web.AuthenticationEntryPoint
to trap REST認証を要求しているが、必要な認証トークンが欠落しているため、要求を拒否する呼び出し。org.springframework.security.core.Authentication
REST APIに必要な認証情報を保持します。org.springframework.security.authentication.AuthenticationProvider
(データベース、LDAPサーバー、Webサービスなどに対する)実際の認証を実行します。org.springframework.security.web.context.SecurityContextRepository
は、HTTP要求の間に認証トークンを保持します。サンプルでは、実装はトークンをEHCACHEインスタンスに保存します。サンプルではXML構成を使用していますが、同等のJava config。
あなたは正しい、それは簡単ではなく、そこに多くの良い例はありません。私が見た例は、他の春のセキュリティのものを並べて使用できないように作られました。私は最近似たようなことをしました、ここに私がしたことです。
ヘッダー値を保持するにはカスタムトークンが必要です
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);
}
}
ヘッダーを抽出し、マネージャーに認証を依頼するには、次のようなスプリングセキュリティフィルターが必要です。emphasized text
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);
}
}
Spring Securityチェーンにカスタムフィルターを登録する
@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);
}
}
最後に、プロバイダーをBeanとして登録し、認証マネージャーが@Configurationクラスでそれを見つけるようにします。あなたはおそらくそれを@Componentにすることもできます、私はこの方法を好む
@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies) {
return new CustomAuthenticationProvider(injectedDependencies);
}
コードはすべてのエンドポイントを保護します-しかし、私はあなたがそれで遊ぶことができると確信しています:)。トークンは、Spring Boot Starter Securityを使用してRedisに保存され、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になります。
Java
これらの依存関係を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
を見つけますが、さらに多くの選択肢があります(LDAPなど)、クラス定義を調べてください。 userDetailsService
オブジェクトを必要とするUserDetailsService
を使用しています。
最後に、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を使用してマイクロサービスアプリケーションを生成してみてください。 Spring SecurityとJWTをすぐに統合できるテンプレートを生成します。