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

2022-01-10 00:00:00 jwt spring java spring-mvc spring-security

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

I have a situation, I am using Spring MVC (jsp, controllers, service, dao) and session based authentication. But now few urls I am using as a RESTful Web service for integration purpose.

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

For those requests only, I need to use token (for eg JWT) based authentication.

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

So, is there any possibility that I can use both type of authentication within same project.

推荐答案

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

is there any possibility that I can use both type of authentication within same project.

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

Yes you can. By having two authentication processing filters.

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

Filter - 1: for Rest API (JwtAuthTokenFilter) which should be stateless and identified by Authorization token sent in request each time.
Filter - 2: You need another filter (UsernamePasswordAuthenticationFilter) By default spring-security provides this if you configure it by http.formLogin(). Here each request is identified by the session(JSESSIONID cookie) associated. If request does not contain valid session then it will be redirected to authentication-entry-point (say: login-page).

api-url-pattern    = "/api/**"
webApp-url-pattern = "/**"

工作原理

  • 带有 /api/** 的 URL 将通过 JwtAuthTokenFilter 传递,它将读取令牌,如果它具有有效令牌,则设置身份验证对象和链继续.如果它没有有效的请求,那么链会被破坏并且响应将被发送 401(未授权)状态.

    How it works

    • URL's with /api/** will be passed through JwtAuthTokenFilter where it will read the token and if it has valid token, sets authentication object and chain continues. If it does not have the valid request then chain gets broken and response will be sent with 401(Unauthorized) status.

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

      URL's other than /api/** will be handled by UsernamePasswordAuthenticationFilter [which is default in spring security configured by .formLogin() configuration] It will check for valid session, if it does not contain the valid session it will redirects to logoutSuccessUrl configured.

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

      Note: Your Webapp can not access APIs by using existing session. What option you have is to use Jwt token to access API from Web application.

      如何配置

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

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

      以上解释是一种实现,我已经在 我的另一个答案,你可以在这里参考

      Above explanation is one type of implementation, i have explained other type of implementation(where Rest APIs can be accessed by auth token as well as session) in my another answer which you can refer here

相关文章