混合身份验证 - 基于 Spring MVC 会话 + 基于 JWT 令牌

Posted

技术标签:

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

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

仅对于这些请求,我需要使用基于令牌(例如 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 中作为响应吗? 我也面临这个问题,请问这个解决方案是否有效

以上是关于混合身份验证 - 基于 Spring MVC 会话 + 基于 JWT 令牌的主要内容,如果未能解决你的问题,请参考以下文章

使用 Spring 3.1 的混合模式 X509 身份验证

Web 应用程序会话与令牌的安全性

重启后 Spring Security 仍然经过身份验证(但没有会话)

MVC 身份验证超时/会话 cookie 删除后的 Ajax 请求

为网站和移动应用程序混合 MVC 5 + WEB API 2 身份验证

使用 ASP.NET MVC 上传(会话和身份验证)