【问题标题】:Hybrid authentication - Spring MVC session based + JWT token based混合身份验证 - 基于 Spring MVC 会话 + 基于 JWT 令牌
【发布时间】:2020-02-04 18:53:27
【问题描述】:

我有一个情况,我正在使用 Spring MVC(jsp、控制器、服务、dao)和基于会话的身份验证。但是现在我将几个 url 用作 RESTful Web 服务来进行集成。

仅对于这些请求,我需要使用基于令牌(例如 JWT)的身份验证。

那么,有没有可能我可以在同一个项目中同时使用这两种类型的身份验证。

【问题讨论】:

    标签: java spring spring-mvc spring-security jwt


    【解决方案1】:

    有没有可能我可以在同一个项目中使用这两种类型的身份验证。

    是的,你可以。通过具有两个身份验证处理过滤器。

    Filter - 1:用于 Rest API (JwtAuthTokenFilter),它应该是无状态的,并由每次在请求中发送的授权令牌标识。
    Filter - 2:您需要另一个过滤器 (UsernamePasswordAuthenticationFilter) 默认情况下,如果您通过 http.formLogin() 配置它,spring-security 会提供此过滤器。这里每个请求都由相关的会话(JSESSIONIDcookie)标识。如果请求不包含有效会话,那么它将被重定向到身份验证入口点(例如:登录页面)。

    推荐的网址格式
    api-url-pattern    = "/api/**"
    webApp-url-pattern = "/**"
    
    工作原理
    • 带有/api/** 的URL 将通过JwtAuthTokenFilter 传递,它将读取令牌,如果它具有有效令牌,则设置身份验证对象并继续链。如果它没有有效的请求,那么链会被破坏并且响应将被发送 401(未授权)状态。

    • /api/** 以外的 URL 将由 UsernamePasswordAuthenticationFilter 处理 [这是由 .formLogin() 配置配置的 Spring Security 中的默认设置] 它会检查有效会话,如果它不包含有效会话,它将重定向到配置的 logoutSuccessUrl。

    注意: 您的 Web 应用程序无法使用现有会话访问 API。您有什么选择是使用 Jwt 令牌从 Web 应用程序访问 API。

    如何配置

    要实现两种不同的认证处理过滤器, 您应该以不同的顺序配置多个 http 安全配置
    可以通过在安全配置类中声明静态类来配置多个 http 安全配置,如下所示。
    (尽管 OP 要求概念明智地呈现代码明智。它可能会帮助您参考)Spring 安全配置

    @Configuration
    @EnableWebSecurity
    @ComponentScan(basePackages = "com.gmail.nlpraveennl")
    public class SpringSecurityConfig
    {
        @Bean
        public PasswordEncoder passwordEncoder() 
        {
            return new BCryptPasswordEncoder();
        }
    
        @Configuration
        @Order(1)
        public static class RestApiSecurityConfig extends WebSecurityConfigurerAdapter
        {
            @Autowired
            private JwtAuthenticationTokenFilter jwtauthFilter;
    
            @Override
            protected void configure(HttpSecurity http) throws Exception
            {
                http
                    .csrf().disable()
                    .antMatcher("/api/**")
                    .authorizeRequests()
                    .antMatchers("/api/authenticate").permitAll()
                    .antMatchers("/api/**").hasAnyRole("APIUSER")
                .and()
                    .addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);
    
                http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            }
        }
    
        @Configuration
        @Order(2)
        public static class LoginFormSecurityConfig extends WebSecurityConfigurerAdapter
        {
            @Autowired
            private PasswordEncoder passwordEncoder;
    
            @Autowired
            public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) throws Exception
            {
                auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin@123#")).roles("ADMIN");
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception
            {
                http
                    .csrf().disable()
                    .antMatcher("/**").authorizeRequests()
                    .antMatchers("/resources/**").permitAll()
                    .antMatchers("/**").hasRole("ADMIN")
                .and().formLogin();
    
                http.sessionManagement().maximumSessions(1).expiredUrl("/customlogin?expired=true");
            }
        }
    }
    
    Jwt 身份验证令牌过滤器
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
    {
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
        {
            final String header = request.getHeader("Authorization");
    
            if (header != null && header.startsWith("Bearer ")) 
            {
                String authToken = header.substring(7);
                System.out.println(authToken);
    
                try
                {
                    String username = jwtTokenUtil.getUsernameFromToken(authToken);
                    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null)
                    {
                        if (jwtTokenUtil.validateToken(authToken, username))
                        {
                            List<GrantedAuthority> authList = new ArrayList<>();
                            authList.add(new SimpleGrantedAuthority("ROLE_APIUSER"));
    
                            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authList);
                            usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    
                            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                        }
                    }
                }
                catch (Exception e)
                {
                    System.out.println("Unable to get JWT Token, possibly expired");
                }
            }
    
            chain.doFilter(request, response);
        }
    }
    
    Jwt 令牌实用程序类
    @Component
    public class JwtTokenUtil implements Serializable
    {
        private static final long   serialVersionUID    = 8544329907338151549L;
        public static final long    JWT_TOKEN_VALIDITY  = 5 * 60 * 60;
        private String              secret              = "my-secret";
    
        public String getUsernameFromToken(String token)
        {
            return getClaimFromToken(token, Claims::getSubject);
        }
    
        public Date getExpirationDateFromToken(String token)
        {
            return getClaimFromToken(token, Claims::getExpiration);
        }
    
        public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver)
        {
            final Claims claims = getAllClaimsFromToken(token);
            return claimsResolver.apply(claims);
        }
    
        private Claims getAllClaimsFromToken(String token)
        {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        }
    
        private Boolean isTokenExpired(String token)
        {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        }
    
        public String generateToken(String username)
        {
            Map<String, Object> claims = new HashMap<>();
            return doGenerateToken(claims, username);
        }
    
        private String doGenerateToken(Map<String, Object> claims, String subject)
        {
            return "Bearer "+Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)).signWith(SignatureAlgorithm.HS512, secret).compact();
        }
    
        public Boolean validateToken(String token, String usernameFromToken)
        {
            final String username = getUsernameFromToken(token);
            return (username.equals(usernameFromToken) && !isTokenExpired(token));
        }
    }
    
    Dispatcher Servlet 配置
    @Configuration
    @EnableWebMvc
    @ComponentScan(basePackages = "com.gmail.nlpraveennl") //Do not skip componentscan
    public class ServletConfiguration implements WebMvcConfigurer
    {
         @Bean
         public ViewResolver configureViewResolver() 
         {
             InternalResourceViewResolver viewResolve = new InternalResourceViewResolver();
             viewResolve.setPrefix("/WEB-INF/jsp/");
             viewResolve.setSuffix(".jsp");
    
             return viewResolve;
         }
    
        @Bean
        public ResourceBundleMessageSource messageSource()
        {
            ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
            messageSource.setBasename("messages");
            messageSource.setDefaultEncoding("UTF-8");
            messageSource.setUseCodeAsDefaultMessage(true);
            return messageSource;
        }
    
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry)
        {
            registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
        }
    }
    

    以上解释是一种实现,我已经在my another answer which you can refer here解释了其他类型的实现(可以通过身份验证令牌和会话访问Rest API)

    【讨论】:

    • 在 API 中的 /api/authenticate 已打开,您可以使用该 API 获取接受用户名和密码的令牌,并在凭据有效时发送带有令牌的响应
    • @Rohit Tiwari 如果您在实现此目标时遇到任何问题,请告诉我。我会将代码上传到 GitHub 并分享下载链接。
    • 你会在哪里存储 jwt 令牌?曲奇饼? /api/authenticate/会将其添加到 cookie 中作为响应吗?
    • 我也面临这个问题,请问这个解决方案是否有效
    猜你喜欢
    • 2017-03-21
    • 2018-01-08
    • 2021-11-13
    • 2015-12-24
    • 2017-07-10
    • 2020-03-03
    • 2013-02-11
    • 2016-09-21
    • 2016-06-01
    相关资源
    最近更新 更多