Spring Boot 安全性 + JWT
Posted
技术标签:
【中文标题】Spring Boot 安全性 + JWT【英文标题】:Spring Boot Security + JWT 【发布时间】:2021-07-14 01:56:32 【问题描述】:我有一个 SpringBoot 2.4.2 应用程序,它使用 JSON Web Tokens(JWT,有时发音为 /dʒɒt/,与英语单词“jot”[1] 相同)是 Internet 提议的标准,用于创建具有可选签名和/ 或可选加密,其有效负载包含断言一定数量的声明的 JSON。令牌使用私有密钥或公钥/私钥进行签名。例如,服务器可以生成具有“以管理员身份登录”声明的令牌并将其提供给客户端。然后客户端可以使用该令牌来证明它是以管理员身份登录的。
这是我的 WebSecurityConfig:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
private static final String SALT = "fd23451*(_)nof";
private final JwtAuthenticationEntryPoint unauthorizedHandler;
private final JwtTokenUtil jwtTokenUtil;
private final UserSecurityService userSecurityService;
@Value("$jwt.header")
private String tokenHeader;
public ApiWebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, JwtTokenUtil jwtTokenUtil,
UserSecurityService userSecurityService)
this.unauthorizedHandler = unauthorizedHandler;
this.jwtTokenUtil = jwtTokenUtil;
this.userSecurityService = userSecurityService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
auth
.userDetailsService(userSecurityService)
.passwordEncoder(passwordEncoder());
@Bean
public BCryptPasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
httpSecurity
// 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()
// Un-secure H2 Database
.antMatchers("/h2-console/**/**").permitAll()
.antMatchers("/api/v1/users").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
httpSecurity
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity
.headers()
.frameOptions()
.sameOrigin() // required to set for H2 else H2 Console will be blank.
.cacheControl();
@Override
public void configure(WebSecurity web)
// AuthenticationTokenFilter will ignore the below paths
web
.ignoring()
.antMatchers(
HttpMethod.POST,
"/api/v1/users"
);
这是我的过滤器:
@Provider
@Slf4j
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private String tokenHeader;
public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader)
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.tokenHeader = tokenHeader;
@Override
protected boolean shouldNotFilter(HttpServletRequest request)
return new AntPathMatcher().match("/api/v1/users", request.getServletPath());
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException,
IOException
log.info("processing authentication for ''", request.getRequestURL());
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");
log.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));
log.info("authorizated user '', setting security context", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
和
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException
log.info("user tries to access a secured REST resource without supplying any credentials");
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
这是我启动应用程序时的控制台:
18:02:51.974 [restartedMain] DEBUG com.agrumh.Application - Running with Spring Boot v2.4.2, Spring v5.3.3
18:02:51.974 [restartedMain] INFO com.agrumh.Application - No active profile set, falling back to default profiles: default
18:02:57.383 [restartedMain] INFO o.s.s.web.DefaultSecurityFilterChain - Will secure Ant [pattern='/api/v1/users', POST] with []
18:02:57.414 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/h2-console/**/**']
18:02:57.415 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/api/v1/users']
18:02:57.416 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [authenticated] for any request
18:02:57.422 [restartedMain] INFO o.s.s.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@24c68fed, org.springframework.security.web.context.SecurityContextPersistenceFilter@1537eb0a, org.springframework.security.web.header.HeaderWriterFilter@95de45c, org.springframework.security.web.authentication.logout.LogoutFilter@733cf550, com.dispacks.config.JwtAuthorizationTokenFilter@538a96c8, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@8d585b2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@784cf061, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@64915f19, org.springframework.security.web.session.SessionManagementFilter@21f180d0, org.springframework.security.web.access.ExceptionTranslationFilter@2b153a28, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4942d157]
18:02:58.619 [restartedMain] INFO com.dispacks.DispacksApplication - Started DispacksApplication in 6.974 seconds (JVM running for 7.697)
18:04:03.685 [http-nio-1133-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing POST /error
18:04:03.687 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
18:04:03.689 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext
18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]
18:04:03.698 [http-nio-1133-exec-1] INFO c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials
18:04:03.699 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request
但是当我使用 Postman 访问时,我遇到了这个错误:
22:58:33.562 [http-nio-1133-exec-2] WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain' not supported]
22:58:33.579 [http-nio-1133-exec-2] INFO c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials
【问题讨论】:
问题解决了吗?如果您对异常设置断点,异常消息说明了什么? 【参考方案1】:您从 Postman 那里调用的路径是什么?如果是/api/v1/users
,我可以看到您在过滤器的shouldNotFilter
方法中设置了此路径。这是否意味着您忽略了此路径的 JWT 过滤器?
顺便说一句,如果您不需要任何额外的功能,您可以使用 Spring Security 对验证 JWT 的支持。看看this tutorial 看看它是如何配置的。这样您就不需要自己的过滤器了。
【讨论】:
确实是api/v1/users,所以我忽略了这个请求 您在调用此端点时提供了什么凭据? JWT / Basic / Cookie? 无身份验证 啊,现在我看到您实际上希望此路径没有身份验证。只是为了绝对确定 - 您正在从 Postman 发出 POST 请求?配置对我来说看起来不错,奇怪的是它不起作用。也许尝试打开调试甚至跟踪日志并查看服务器中发生了什么(甚至在某处放置断点)并尝试了解 Spring 重定向您的原因。【参考方案2】:授权和认证是不同的
POST /api/v1/users
被允许,因为资源 POST 不需要被授权访问。
在您的代码中,
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException // AuthenticationException means authentication failed, not "without supplying any credentials".
) throws IOException
// Break point here, or print authException.
log.info("user tries to access a secured REST resource without supplying any credentials"); // Wrong message. You can say "Authentication failed.", or log.info(authException.getMessage()).
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
访问/error
资源时确实发生了身份验证错误。
18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]
我假设发生了一些错误,您的应用程序将您重定向到 /error
,但 /error
受到保护。所以 authenticationException 发生在/error
。
-
在
.permitAll()
之前添加/error
。
断点 authenticationException 以便我可以更新此答案。
【讨论】:
【参考方案3】:如果我理解你是正确的,你希望 JWT 过滤器只为某些端点运行吗?我遇到了同样的问题,无论我尝试了多少不同的安全配置,我都无法让 SpringSecurity 只为指定的入口点运行我的 JWT 过滤器。
我像你一样通过覆盖 shouldNotFilter 解决了这个问题,但我的看起来像这样:
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException
return new AntPathRequestMatcher("/api/v1/users").matches(request);
也许这可以解决你的问题。
【讨论】:
为除此之外的所有端点运行:/api/v1/users 试试我的 shouldNotFilter() 版本。它应该有效地停止对端点“/api/v1/users”的过滤。至少在我的项目中是这样。以上是关于Spring Boot 安全性 + JWT的主要内容,如果未能解决你的问题,请参考以下文章
在 Spring Boot 安全性中使用公钥/私钥创建和验证 JWT 签名
Spring Boot 安全性 - 允许使用过期 JWT 令牌的用户请求
启用具有 JWT 安全性的 Spring Boot 2.0 执行器端点
来自文件的 Spring Boot http 安全 jwt 密钥
在 Spring Boot 中使用 jwt 令牌的具有 http 安全性的 CrossOrigin
我已经在 Spring Boot 代码中实现了 JWT 令牌安全性。如何在我的代码中的任何地方获取 jwt 令牌?需要保存审核