在 Spring Boot API 上实现 JWT 身份验证

Posted

技术标签:

【中文标题】在 Spring Boot API 上实现 JWT 身份验证【英文标题】:Implementing JWT Authentication on Spring Boot APIs 【发布时间】:2018-10-27 09:54:54 【问题描述】:

我有一个 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 Controller 添加一个基于自定义 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"

因为我只想在 RestContollers 中应用 JWT 过滤器,而不是在所有这些中应用,例如 LoginController

通过这个配置类,我可以访问仅在应用程序中登录的 /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#$1*(_)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());
    


【问题讨论】:

为什么不用antMatcher(“rest/**”).authenticated() 我必须在所有情况下都经过身份验证,rest/ 使用 JWT,其他使用典型的 springsecurity 假设你的过滤器是一个OncePerRequestFilter,它有一个方法shouldNotFilter(),你能用它来忽略你想要的请求之外的所有东西吗? 【参考方案1】:

简而言之,您在同一个应用程序中有两个子路径(即/rest/** 和其他子路径),并且您希望为每个子路径应用不同的登录方案。 Spring-security 允许您拥有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 中的路径。

【讨论】:

以上是关于在 Spring Boot API 上实现 JWT 身份验证的主要内容,如果未能解决你的问题,请参考以下文章

使用Spring Security进行auth api调用时出现Cros错误

如何在 Spring Boot 中实现 oAuth2 和 JWT 身份验证? [关闭]

将 Angular 与 JWT 的 Spring Boot 连接时出现 CORS 错误

使用 Spring Boot 和 JWT 保护 REST Api

我已经在 Spring Boot 代码中实现了 JWT 令牌安全性。如何在我的代码中的任何地方获取 jwt 令牌?需要保存审核

spring boot 2 集成JWT实现api接口认证