私たちの会社では、JWTトークンをCookieに保存するために使用しています。 Webアプリケーションは、Springブート+ JSPアプリケーション上にあります。したがって、フローは、ログインサービスが成功した場合にJWTトークンを送信し、そのトークンはCookieに保存され、その後のサービスへのすべてのリクエストでは、トークンがCookieから取得されます。作成に使用している現在のコードは、次のようになります。
Spring Controller
_@GetMapping("/")
@ResponseBody
public List<Node> test(HttpServletRequest request) {
var nodeList = service.testService(request);
return nodeList;
}
_
サービス層
_public List<Node> testService(HttpServletRequest request) {
// business logic
// some other service call
someService.get(request)
}
_
残りのサービス層
_public List<Node> get(HttpServletRequest request){
// finally we retrieve the token from the sevletRequest
token = WebUtils.getCookie(request, ACCESS_TOKEN);
// rest call with this token.
}
_
servletRequest
パラメータに関する私の懸念。休憩できる場所ならどこでもこの依頼をしなければなりません。このデザインで何を改善できますか?他の人がこれをどのように扱っているかについてのアドバイスも求めています。
==更新==
A(controller) calls B, B calls C
とします。今、CはDを呼び出さなければなりません。今回は、AからDに至るまでTokenパラメーターを渡すために、コードをリファクタリングする必要があります。
Springでは、アノテーションを使用してメソッドに特定のパラメーターを指定できるため、HttpServletRequest
自体を直接操作する必要はありません。 JWTトークンを配置する最も一般的な場所は、Authorization
ヘッダーのベアラートークンです。 HTTPリクエストは次のようになります。
GET http://example.org/myservice/123 HTTP/1.1
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
これにより、次のように、トークンを取得して実際のクレームをサービスに渡すようにメソッドを設定できます( Oktaを使用 でJWTを解析および検証します)。
@GetMapping("/")
@ResponseBody
public List<Node> get(@RequestHeader("authorization") String token) {
// Remove the "bearer" prefix so you just have the token...
// This line will throw an exception if it is not a signed JWS (as expected)
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY))
.parseClaimsJws(token).getBody();
return service.get(claims);
}
内部サービスに公開する境界コードの量を最小限に抑える必要があります。内部サービスはユーザーとその権限について心配する必要があるため、HttpServiceRequest
については何も知りません。コントローラーのJWTでクレームを抽出して検証し、それを内部サービスに渡すだけで、既存のコードをクリーンアップできます。
これは次のようになります。
@GetMapping("/")
@ResponseBody
public List<Node> get(@CookieValue(ACCESS_TOKEN) String token) {
// This line will throw an exception if it is not a signed JWS (as expected)
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY))
.parseClaimsJws(token).getBody();
return service.get(claims);
}
確かに、Java=では、コンパイラは定数式である必要があるため、注釈のACCESS_TOKENの後ろの値を拡張する必要があるでしょう。
長い回答を短くする
リクエストオブジェクト全体ではなく、トークンをだけ追跡する必要があります。 Feign を使用する場合も、 RestTemplate を直接使用する場合も、必要な情報を提供して次のリクエストを行うだけです。
「サービスにはトークンが必要である」とのことですが、Webレイヤーからサービスレイヤーにトークンをすべて渡すことに懸念があります。ただし、ベリンの回答に対するコメントでは、この目的は役割ベースの承認であると説明しています。より簡単な実装が可能です。この場合、リクエストごとにJWTを読み取り、ユーザーのロールを含む「認証」オブジェクトを更新するフィルターを作成します。次に、ロールに基づいて、アプリケーション内の異なる経路を開くか閉じるだけです。
同様のSpring Bootアプリケーションで、次のような構成を使用しました。
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
private final MyUserDetailService myUserDetailService;
public MySecurityConfiguration(MyUserDetailService myUserDetailService) {
this.myUserDetailService= myUserDetailService;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers( // Spring will ignore security controls for the specified endpoints, making them publicly-accessible.
"/favicon.ico", // Spring boot looks for a favicon in src/main/resources and points this URL at it. -- Only when running standalone. Not when running in Tomcat.
"/webjars/**", // This directory is where Maven downloads bootstrap, jquery, etc., at build time
"/public/**" // A directory for static images, CSS, and JS that can be accessed without authentication. Do not use for data that should be private.
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilter(new JWTAuthenticationFilter(authenticationManager(),myUserDetailService)) // This filter intercepts the login, authenticates the user, and creates a JWT token in a cookie to authorize subsequent requests..
.addFilterBefore(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class) // This filter reads the JWT token from the cookie on subsequent requests, and authorizes the user (or not).
.addFilterBefore(new SecurityContextDeletingFilter(), SecurityContextPersistenceFilter.class) // Prevent the HttpSession from keeping the user logged in after we've deleted the cookie.
.formLogin()
.loginPage("/login") // Our custom Login form. Users will be redirected to it if they are not authenticated.
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout") // This should be the default, but it doesn't hurt to make it explicit.
.logoutSuccessHandler(new LogoutSuccessHandler()) // deletes the JWT authentication cookie and redirects user to the login page
.and()
.authorizeRequests()
.mvcMatchers("/login").permitAll() // The login form must be accessible to anyone, whether or not they're authenticated.
.mvcMatchers("/").authenticated() // The main menu is accessible to any authenticated user.
.mvcMatchers("/admin/**").hasAuthority("ADMIN") // The security pages require the ADMIN authority.
.anyRequest().denyAll(); // deny any other request by default
}
}
私の認証および承認フィルターは、そこにあるチュートリアルのいくつかにかなり似ています。認証フィルターはスキップします。あなたはすでに自分のものを持っているようです。承認フィルターは次のようになります。
public class JWTAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
Cookie cookie = WebUtils.getCookie(httpServletRequest, "JWT-TOKEN");
if (cookie == null) {
filterChain.doFilter(httpServletRequest, httpServletResponse); // Proceed normally, ignoring the rest of this filter
return;
}
try {
// Our custom JWT-based authorization logic is in getAuthentication(). See below.
UsernamePasswordAuthenticationToken authentication = getAuthentication(cookie.getValue());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (ExpiredJwtException e) {
// User presented an expired JWT
} catch (JwtException e) {
// If you get here, the user has a cookie-based JWT but it's invalid for some other reason than expiration.
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
if( token != null ) {
// parse the token
Claims claims = Jwts.parser()
.setSigningKey(System.getenv("SECRET_KEY"))
.parseClaimsJws(token)
.getBody();
String user = claims.getSubject();
// authorities are transmitted as a comma-delimited string like "USER,ADMIN,SUPERUSER"
String authorityString = (String)claims.get("authorities");
Collection<? extends GrantedAuthority> authorities;
if(!authorityString.isEmpty()) {
authorities = Arrays.asList(authorityString.split(",")).stream()
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
} else {
authorities = null;
}
if( user != null ) {
return new UsernamePasswordAuthenticationToken(user, null, authorities);
}
return null;
}
return null;
}
}
私が見つけた1つの問題は、認証がセッション(JSESSION_ID Cookie)にアタッチされ、ユーザーがサインアウトできないことです-JWT Cookieを削除しましたが、他のCookieはそれらをログインしたままにしていました。すべてのリクエストでセキュリティコンテキストをクリアするカスタムSecurityContextDeletingFilter
ここにそのコードがあります:
public class SecurityContextDeletingFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpSession session = request.getSession();
if( session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY) != null ) {
session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
}
filterChain.doFilter(servletRequest,servletResponse);
}
}
要約すると、/admin/
の下のURLに「ADMIN」権限を要求することにより、それらのエンドポイントでのサービス層ロジックへのアクセスを許可し、それらがJWTトークンにエンコードされたロールによって保護されていることを確認できます。 I しないでくださいこれを行うには、トークンをサービス層に渡す必要があります。