Spring SecurityとJWTトークンを使用した認証

みなさんこんにちは! ハブルは生きている! この投稿が多くの意見やコメントを集めることはまずありませんが、Habrの健康に少し役立つと思います。



この記事では、比較的新しい認証メカニズムであるJSON Web Token(JWT)を使用して、Spring Webアプリケーションでの認証の原理を調べます 。 このメカニズムは、多くのプログラミング言語で既にテストおよび実装されています。







トークンを使用すると、サーバーは要求(HTTPセッション)間で状態を維持することを気にせず、データベース要求の数を減らすことができます。回復に必要なデータをトークンに保存できます。 JWTトークンについて:サーバーは、JSONペイロード(ヘッダーと本文)を秘密キーと混合し、ハッシュを生成して、ペイロードに署名として添付します。 ペイロードはbase64Urlアルゴリズムによってエンコードされるため、当然、秘密データはトークンで送信されるべきではありません。 JWT標準では、ペイロードの暗号化は提供されていません。 必要に応じて自分で個別に暗号化します。トークンのタスクは認証を提供することのみです。



読者は、Spring Secutityの基本に精通しているはずです。 ここで彼について読むことができます。



1)。 トークン生成


私の例で 、JWT仕様の実装の 1つを取り上げました。 トークンは次のように生成されます。



package com.example.security; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.crypto.MacProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import java.util.*; @Service public class GetTokenServiceImpl implements GetTokenService { @Autowired private UserDetailsService userDetailsService; @Override public TokenObject getToken(String username, String password) throws Exception { if (username == null || password == null) return null; User user = (User) userDetailsService.loadUserByUsername(username); Map<String, Object> tokenData = new HashMap<>(); if (password.equals(user.getPassword())) { tokenData.put("clientType", "user"); tokenData.put("userID", user.getUserId().toString()); tokenData.put("username", authorizedUser.getUsername()); tokenData.put("token_create_date", new Date().getTime()); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.YEAR, 100); tokenData.put("token_expiration_date", calendar.getTime()); JwtBuilder jwtBuilder = Jwts.builder(); jwtBuilder.setExpiration(calendar.getTime()); jwtBuilder.setClaims(tokenData); String key = "abc123"; String token = jwtBuilder.signWith(SignatureAlgorithm.HS512, key).compact(); return token; } else { throw new Exception("Authentication error"); } } }
      
      





その結果、 <Title>。<Body>。<Signature>という形式の行を取得し、クライアントに送信します



さて、Spring Securityへ。 独自の認証メカニズムを実装するには、独自のフィルター認証マネージャーを実装する必要があります。



2)。 フィルターの実装


フィルターは、 javax.servlet.Filterインターフェースを実装するクラスのオブジェクトであり、特定のURLへのリクエストをインターセプトし、いくつかのアクションを実行します。 複数のフィルターがある場合、それらはフィルターのチェーンを形成します。アプリケーションによって受信された後のHTTP要求は、このチェーンを通過します。 チェーン内の各フィルターは、要求を処理し、チェーン内の次のフィルターにスキップするかどうかを設定し、すぐにクライアントに応答を送信できます。



フィルターのタスクは、トークンを要求から認証マネージャーに転送し、認証が成功した場合、アプリケーションセキュリティコンテキストを確立することです。



 package com.example.security; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public TokenAuthenticationFilter() { super("/rest/**"); setAuthenticationSuccessHandler((request, response, authentication) -> { SecurityContextHolder.getContext().setAuthentication(authentication); request.getRequestDispatcher(request.getServletPath() + request.getPathInfo()).forward(request, response); }); setAuthenticationFailureHandler((request, response, authenticationException) -> { response.getOutputStream().print(authenticationException.getMessage()); }); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String token = request.getHeader("token"); if (token == null) token = request.getParameter("token"); if (token == null) { TokenAuthentication authentication = new TokenAuthentication(null, null); authentication.setAuthenticated(false); return authentication; } TokenAuthentication tokenAuthentication = new TokenAuthentication(token); Authentication authentication = getAuthenticationManager().authenticate(tokenAuthentication); return authentication; } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { super.doFilter(req, res, chain); } }
      
      







認証用に特別に設計された抽象クラスorg.springframework.security.web.authentication.AbstractAuthenticationProcessingFilterから継承しました。 リクエストURL マシンのパターン「/ rest / **」と一致する場合、 attemptAuthentication()関数が呼び出されます。

また、コンストラクターにAuthenticationSuccessHandlerAuthenticationFailureHandlerの 2つのハンドラーをインストールしました。 attemptAuthenticationAuthenticationオブジェクトを返す場合、最初のハンドラーが起動し、 attemptAuthenticationが AuthenticationExceptionを スローすると2番目のハンドラーが起動します。

ご覧のとおり、認証が成功すると、 SecurityContextHolder.getContext()。SetAuthentication(authentication)を介してアプリケーションのセキュリティコンテキストを設定します。 この方法で設定されたコンテキストは、 ThreadLocal変数です。 クライアントワークフローが動作しているときに使用できます。 コンテキストを設定した後、最初にリクエストされたURLでユーザーリクエストをサーブレットに送信します。



3)。 認証マネージャー。


認証マネージャーは、単一のauthenticate()メソッドでorg.springframework.security.authentication.AuthenticationManagerインターフェースを実装するクラスのオブジェクトです。 このメソッドは、 org.springframework.security.core.Authenticationインターフェース(アプリケーションセキュリティコンテキスト)を実装する部分的に満たされたオブジェクトを渡す必要があります。

認証マネージャーのタスクは、認証が成功した場合に認証オブジェクトを完了し、それを返すことです。 入力する場合、ユーザー( プリンシパル )、ユーザーの権限( 権限 )を設定し、 setAuthenticated(true)を実行する必要があります 。 失敗した場合、認証マネージャーはAuthenticationExceptionをスローする必要があります。



以下は、 org.springframework.security.core.Authenticationインターフェースの実装例です。



 package com.example.security; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.List; import java.util.Map; public class TokenAuthentication implements Authentication { private String token; private Collection<? extends GrantedAuthority> authorities; private boolean isAuthenticated; private UserDetails principal; public TokenAuthentication(String token) { this.token = token; this.details = request; } public TokenAuthentication(String token, Collection<SimpleGrantedAutority> authorities, boolean isAuthenticated, UserDetails principal) { this.token = token; this.authorities = authorities; this.isAuthenticated = isAuthenticated; this.principal = principal; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public Object getCredentials() { return null; } @Override public Object getDetails() { return details; } @Override public String getName() { if (principal != null) return ((UserDetails) principal).getUsername(); else return null; } @Override public Object getPrincipal() { return principal; } @Override public boolean isAuthenticated() { return isAuthenticated; } @Override public void setAuthenticated(boolean b) throws IllegalArgumentException { isAuthenticated = b; } public String getToken() { return token; } }
      
      







認証マネージャの実装は次のとおりです。



 package com.example.security; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.impl.DefaultClaims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.authentication.AuthenticationServiceException import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import org.springframework.security.core.GrantedAuthority; import javax.servlet.http.HttpServletRequest; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Service public class TokenAuthenticationManager implements AuthenticationManager { @Autowired private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { if (authentication instanceof TokenAuthentication) { TokenAuthentication readyTokenAuthentication = processAuthentication((TokenAuthentication) authentication); return readyTokenAuthentication; } else { authentication.setAuthenticated(false); return authentication; } } catch (Exception ex) { if(ex instanceof AuthenticationServiceException) throw ex; } } private TokenAuthentication processAuthentication(TokenAuthentication authentication) throws AuthenticationException { String token = authentication.getToken(); String key = "key123"; DefaultClaims claims; try { claims = (DefaultClaims) Jwts.parser().setSigningKey(key).parse(token).getBody(); } catch (Exception ex) { throw new AuthenticationServiceException("Token corrupted"); } if (claims.get("TOKEN_EXPIRATION_DATE", Long.class) == null) throw new AuthenticationServiceException("Invalid token"); Date expiredDate = new Date(claims.get("TOKEN_EXPIRATION_DATE", Long.class)); if (expiredDate.after(new Date())) return buildFullTokenAuthentication(authentication, claims); else throw new AuthenticationServiceException("Token expired date error"); } private TokenAuthentication buildFullTokenAuthentication(TokenAuthentication authentication, DefaultClaims claims) { User user = (User) userDetailsService.loadUserByUsername(claims.get("USERNAME", String.class)); if (user.isEnabled()) { Collection<GrantedAutority> authorities = user.getAuthorities(); TokenAuthentication fullTokenAuthentication = new TokenAuthentication(authentication.getToken(), authorities, true, user); return fullTokenAuthentication; } else { throw new AuthenticationServiceException("User disabled");; } } }
      
      







4)。 すべてをまとめる方法


最初に、フィルターをインストールする必要があります。 これを行うには2つの方法があります。



最初の方法は、アプリケーションのweb.xmlファイルでフィルターを定義することです



  <filter> <filter-name>springSecurityTokenFilter</filter-name> <filter-class>com.example.security.TokenAuthenticationFilter</filter-class> </filter> <filter-mapping> <filter-name>springSecurityTokenFilter</filter-name> <url-pattern>/rest/**</url-pattern> </filter-mapping>
      
      





このメソッドでは、Springアプリケーションのコンテキストではフィルターインスタンスを使用できないため、フィルターコンストラクターで認証マネージャーをすぐに設定する必要があります。 Spring Beanとしてフィルターまたは認証マネージャーが必要な場合は、2番目の方法を使用する必要があります。



2番目の方法は、Spring Security構成にフィルターをインストールすることです。



たとえば、Java Configを使用して構成を表示しましょう



 package com.example.security; import com.example.security.RestTokenAuthenticationFilter; import com.example.security.TokenAuthenticationManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsService") UserDetailsService userDetailsService; @Autowired TokenAuthenticationManager tokenAuthenticationManager; @Override protected void configure(HttpSecurity http) throws Exception { http .headers().frameOptions().sameOrigin() .and() .addFilterAfter(restTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/rest/*").authenticated() } @Bean(name = "restTokenAuthenticationFilter") public RestTokenAuthenticationFilter restTokenAuthenticationFilter() { RestTokenAuthenticationFilter restTokenAuthenticationFilter = new RestTokenAuthenticationFilter(); tokenAuthenticationManager.setUserDetailsService(userDetailsService); restTokenAuthenticationFilter.setAuthenticationManager(tokenAuthenticationManager); return restTokenAuthenticationFilter; } }
      
      





並んで

.addFilterAfter(restTokenAuthenticationFilter()、UsernamePasswordAuthenticationFilter.class)

標準フィルターUsernamePasswordAuthenticationFilterの後にフィルターチェーンにフィルターを追加しました。



これで、JSON Web Tokenを使用したSpring Securityの認証メカニズムの基本設定が完了しました。



皆さんの成功を祈っています!



ご清聴ありがとうございました!



All Articles