質問

We(in my company) use to save the JWT token in the cookie. The web application is on Spring boot + JSP application. So the flow is, in a successful login service send a JWT token, that token has been saved in the cookie and all the subsequent request to the service the token has been retrieved from the cookie. The current code that we use to write is more like as follow.

In Spring Controller

@GetMapping("/")
@ResponseBody
public List<Node> test(HttpServletRequest request) {
    var nodeList = service.testService(request);
    return nodeList;
}

In Service Layer

public List<Node> testService(HttpServletRequest request) {
  // business logic
  // some other service call
  someService.get(request)
}

In Rest Service Layer

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.
}

My concern with the servletRequest parameter. I have to carry this request everywhere where we can possible make rest call. What can I improve in this design? I am also seeking for advises how others are handling this.

==UPDATE==

Suppose A(controller) calls B, B calls C. Now C has to call D which has a rest call. This time the code has to be refactored to pass the Token parameter from A to all the way down to D.

役に立ちましたか?

解決

Spring allows you to specify certain parameters to your method with annotations so you don't have to do anything directly with the HttpServletRequest itself. The most common place to put your JWT token is as a bearer token in the Authorization header. The HTTP request would look something like this:

GET http://example.org/myservice/123 HTTP/1.1
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

This allows you to set up your method to retrieve the token and pass the actual claims to your service, like this (using Okta to parse and verify 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);
}

You want to minimize the amount of boundary code you expose to your internal services. Your internal services need to worry about the user and their privileges, so they should know nothing about the HttpServiceRequest at all. You could simply clean up your existing code by extracting and verifying the claims in the JWT in your controller, and only passing that in to your internal service.

That would look like this:

@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);
}

Granted, in Java you will likely need to expand the value behind ACCESS_TOKEN in the annotation since the compiler requires that to be a constant expression.

Making a Long Answer Short

You'll need to keep track of just the token, not the whole request object. Whether you use Feign, or you use RestTemplate directly, you can simply provide the needed information and make your next request.

他のヒント

You have indicated that "the token is needed by the service" and you are concerned with passing the token all the way from the web layer into the service layer. In comments on Berin's answer, though, you explain that the purpose of this is role based authorization. There is a simpler implementation possible, in which you create a filter that reads the JWT upon every request and updates the "authentication" object that contains the user's roles. Then you simply open or close different pathways in your application based on the roles.

In a similar Spring Boot application I have used a configuration like this:

@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
    }
}

My authentication and authorization filters are pretty similar to some of the tutorials out there. I'll skip the authentication filter, since you apparently already have your own. The authorization filter looks like this:

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;
    }
}

One issue I found is that the authentication becomes attached to the session (the JSESSION_ID cookie), which made it impossible for my users to sign out -- I would delete the JWT cookie but the other cookie would keep them logged in. So I added a custom SecurityContextDeletingFilter which clears the security context on every request. Here is its code:

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);
    }
}

To sum up, by requiring the "ADMIN" authority for URLs under /admin/, I can give access to any service-tier logic at those endpoints and know that they are secured by the roles encoded in the JWT token. I do not need to pass the token to the service tier to accomplish this.

ライセンス: CC-BY-SA帰属
所属していません softwareengineering.stackexchange
scroll top