混合身份验证 - 基于 Spring MVC 会话 + 基于 JWT 令牌
我有一种情况,我正在使用 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 throughJwtAuthTokenFilter
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 byUsernamePasswordAuthenticationFilter
[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
相关文章