在 Spring 个启动 API 上实施 JWT 身份验证

Implementing JWT Authentication on Spring Boot APIs

我有一个 SpringBoot 2.0.2.RELEASE Web 应用程序,配置文件如下:

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

    final List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
    if (activeProfiles.contains("dev")) {
        http.csrf().disable();
        http.headers().frameOptions().disable();
    }

    http
        .authorizeRequests()
        .antMatchers(publicMatchers()).permitAll()
        .anyRequest().authenticated()
        .and()
        .formLogin().loginPage("/login").defaultSuccessUrl("/bonanza/list")
        .failureUrl("/login?error").permitAll()
        .and()
        .logout().permitAll();
    }

我只想为匹配 /rest/** 下的 Rest 控制器添加一个基于 JWT 的自定义安全过滤器,所以我修改了这个文件的配置,但现在我无法登录应用程序,因为我有一个 HTTP 状态 401 – 未经授权

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

    final List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
    if (activeProfiles.contains("dev")) {
        http.csrf().disable();
        http.headers().frameOptions().disable();
    }

    http
       .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
       // don't create session
       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
       .authorizeRequests()
       .antMatchers(publicMatchers()).permitAll()
       .anyRequest().authenticated()
       .and()
       .formLogin().loginPage("/login").defaultSuccessUrl("/bonanza/list")
                   .failureUrl("/login?error").permitAll()
       .and()
       .logout().permitAll();


       // Custom JWT based security filter
       JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
       http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

和过滤器(从 OncePerRequestFilter 延伸)

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

    logger.info("processing authentication for '{}'", request.getRequestURL());


    if (request.getRequestURI().indexOf("/rest/")==-1) {
        chain.doFilter(request, response);
        return;
    }


    final String requestHeader = request.getHeader(this.tokenHeader);

    String username = null;
    String authToken = null;
    if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
        authToken = requestHeader.substring(7);
        try {
            username = jwtTokenUtil.getUsernameFromToken(authToken);
        } catch (IllegalArgumentException e) {
            logger.info("an error occured during getting username from token", e);
        } catch (ExpiredJwtException e) {
            logger.info("the token is expired and not valid anymore", e);
        }
    } else {
        logger.info("couldn't find bearer string, will ignore the header");
    }

    logger.info("checking authentication for user '{}'", username);
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
       logger.info("security context was null, so authorizating user");

        // It is not compelling necessary to load the use details from the database. You could also store the information
        // in the token and read it from it. It's up to you ;)
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

        // For simple validation it is completely sufficient to just check the token integrity. You don't have to call
        // the database compellingly. Again it's up to you ;)
        if (jwtTokenUtil.validateToken(authToken, userDetails)) {
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            logger.info("authorizated user '{}', setting security context", username);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        chain.doFilter(request, response);
    }

....

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return request.getRequestURI().indexOf("/rest/")==-1;
}

在我看到的记录器中

("couldn't find bearer string, will ignore the header"

因为我只想在 RestContoller 中应用 JWT 过滤器,而不是在所有 RestContoller 中应用,比如 LoginController

使用此配置 class 我可以访问 /rest/ URL 仅在应用程序中登录。

@Profile("web")
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final Logger LOG = LoggerFactory.getLogger(WebSecurityConfig.class);

    @Autowired
    private UserSecurityService userSecurityService;


    @Value("${server.servlet.context-path}")
    private String serverContextPath;

    /** The encryption SALT. */
    private static final String SALT = "fd&lkj§sfs23#*(_)nof";

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Configuration
    @Order(1)
    public static class ApiSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Autowired
        private JwtAuthenticationEntryPoint unauthorizedHandler;

        @Autowired
        private JwtTokenUtil jwtTokenUtil;

        @Value("${jwt.header}")
        private String tokenHeader;

        @Value("${jwt.route.authentication.path}")
        private String authenticationPath;

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

            http
                    // we don't need CSRF because our token is invulnerable
                    .csrf().disable().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

                    // don't create session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    .authorizeRequests()
                    .antMatchers(“/rest/**”).permitAll().anyRequest().authenticated()
                    .antMatchers(“**/rest/**”).permitAll().anyRequest().authenticated();

            // Custom JWT based security filter
            JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
            http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

            // disable page caching
            http.headers().frameOptions().sameOrigin() // required to set for H2 else H2 Console will be blank.
                    .cacheControl();

        }

    }

    @Configuration
    @Order(0)
    public static class OtherSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Value("${server.servlet.context-path}")
        private String serverContextPath;

        @Autowired
        private Environment env;

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

            final List<String> activeProfiles = Arrays.asList(env.getActiveProfiles());
            if (activeProfiles.contains("dev")) {
                http.csrf().disable();
                http.headers().frameOptions().disable();
            }

            http.authorizeRequests()
                .antMatchers(publicMatchers())
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/bonanza/list")
                .failureUrl("/login?error").permitAll()
                .and()
                .logout()
                .permitAll();
        }

         private String[] publicMatchers() {

             /** Public URLs. */
            final String[] PUBLIC_MATCHERS = {
                    "/webjars/**",
                    serverContextPath + "/css/**",
                    serverContextPath + "/js/**",
                    serverContextPath + "/fonts/**",
                    serverContextPath + "/images/**",                
                    serverContextPath ,
                    "/",
                    "/error/**/*",
                    "/console/**",
                    ForgotMyPasswordController.FORGOT_PASSWORD_URL_MAPPING,
                    ForgotMyPasswordController.CHANGE_PASSWORD_PATH,
                    SignupController.SIGNUP_URL_MAPPING
            };

            return PUBLIC_MATCHERS;

        }

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
    }

}

简而言之,您在同一个应用程序中有两个子路径(即 /rest/** 和其他子路径),并且您希望为每个子路径应用不同的登录方案。 Spring-security allows you to have multiple configurations,允许这样的场景。

我会这样做:

@EnableWebSecurity
public class SecurityConfig {

    @Configuration
    @Order(1)
    public static class ApiSecurityConfiguration 
                  extends WebSecurityConfigurerAdapter {

        private final JwtAuthorizationTokenFilter jwtFilter = new ...
        private final AuthenticationEntryPoint unauthorizedHandler = new ...

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/rest/**").authorizeRequests()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .and()
                .addFilter(jwtFilter);
        }
    }

    @Configuration
    public static class OtherSecurityConfiguration 
                  extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                        .loginPage("/login").defaultSuccessUrl("/bonanza/list")
                        .failureUrl("/login?error").permitAll()
                    .and()
                    .logout().permitAll();
        }
    }
}

使用这样的配置,JwtAuthorizationTokenFilter 应该只为匹配的路径激活。因此我认为您不需要检查 JwtAuthorizationTokenFilter 中的路径。