如何用Spring Security为不同端点配置专属Header Token验证
问题描述
我正在使用Spring Boot构建Web应用,在原型集成阶段不想花费过多时间在权限规则管理上,希望生成随机字符串Token,让知晓该Token的其他服务可调用我的服务。我清楚这种方式存在风险,但当前原型阶段确实需要如此。
我的问题是:如何配置Spring Security为特定端点验证专属Token(注意:我希望不同端点使用不同Token,如果是全局单Token,用简单过滤器即可实现)。
期望实现类似如下配置:
protected void configure(HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .authorizeRequests() .antMatchers(AUTH_WHITELIST).permitAll() .antMatchers(HttpMethod.POST, "/boards/").access("SOMETHING LIKE: httpRequest->headers->get('tokenHeader') == 'mytoken'") .anyRequest().authenticated() //...... }
我目前仅能通过如下方式实现,但这种写法不符合Spring模式,使用起来很繁琐:
@Value("${controller.plan.authorization}") private String AUTHORIZATION_TOKEN; @GetMapping public List<Plan> getPlans(@RequestHeader(HttpHeaders.AUTHORIZATION) String authToken) { if (AUTHORIZATION_TOKEN.equals(authToken)) return planService.findAll(); else throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid authorization_token"); }
请问是否有办法通过Spring Security实现该需求?
解决方案
可以通过Spring Security的自定义安全表达式或者端点匹配过滤器两种方式实现,完全贴合Spring的配置风格,避免在控制器中重复校验逻辑:
方式一:自定义安全表达式(推荐)
这种方式能直接在HttpSecurity配置中通过表达式指定每个端点的Token校验规则,和你期望的写法风格一致。
步骤1:实现自定义表达式处理器
创建一个类扩展默认的Web安全表达式处理器,添加自定义的Token校验方法:
import org.springframework.security.access.expression.SecurityExpressionRoot; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; import org.springframework.security.web.access.expression.WebSecurityExpressionRoot; public class CustomSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler { @Override protected SecurityExpressionRoot createSecurityExpressionRoot(FilterInvocation fi) { WebSecurityExpressionRoot root = new CustomWebSecurityExpressionRoot(fi); root.setAuthentication(fi.getAuthentication()); return root; } private static class CustomWebSecurityExpressionRoot extends WebSecurityExpressionRoot { public CustomWebSecurityExpressionRoot(FilterInvocation fi) { super(fi); } // 自定义表达式:校验请求头中的Token是否匹配指定值 public boolean hasMatchingToken(String headerName, String expectedToken) { String actualToken = getRequest().getHeader(headerName); return expectedToken.equals(actualToken); } } }
步骤2:在Security配置中注册并使用表达式
修改Spring Security配置类,注册自定义处理器,然后在access()方法中使用自定义表达式绑定不同端点的Token:
import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // 从配置文件读取不同端点的专属Token @Value("${controller.board.authorization}") private String BOARD_TOKEN; @Value("${controller.plan.authorization}") private String PLAN_TOKEN; private static final String[] AUTH_WHITELIST = {"/public/**", "/actuator/**"}; @Bean public CustomSecurityExpressionHandler customSecurityExpressionHandler() { return new CustomSecurityExpressionHandler(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .authorizeRequests() .expressionHandler(customSecurityExpressionHandler()) // 注册自定义表达式处理器 .antMatchers(AUTH_WHITELIST).permitAll() // 对/boards/的POST请求校验专属Token .antMatchers(HttpMethod.POST, "/boards/").access("hasMatchingToken('Authorization', '" + BOARD_TOKEN + "')") // 对/plans/的GET请求校验另一个Token .antMatchers(HttpMethod.GET, "/plans/").access("hasMatchingToken('Authorization', '" + PLAN_TOKEN + "')") .anyRequest().authenticated(); } }
步骤3:配置文件中添加Token
在application.properties或application.yml中配置各端点的Token:
# 示例配置 controller.board.authorization=board-prototype-token-xyz controller.plan.authorization=plan-prototype-token-abc
方式二:自定义端点匹配Filter
如果觉得表达式方式不够直观,也可以自定义一个Filter,根据请求路径和方法匹配对应Token进行校验:
import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class PrototypeTokenFilter extends OncePerRequestFilter { @Value("${controller.board.authorization}") private String BOARD_TOKEN; @Value("${controller.plan.authorization}") private String PLAN_TOKEN; private static final String AUTHORIZATION_HEADER = "Authorization"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String path = request.getRequestURI(); String method = request.getMethod(); String token = request.getHeader(AUTHORIZATION_HEADER); // 针对不同端点和HTTP方法校验Token if ("/boards/".equals(path) && "POST".equals(method)) { if (!BOARD_TOKEN.equals(token)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid board token"); return; } } else if ("/plans/".equals(path) && "GET".equals(method)) { if (!PLAN_TOKEN.equals(token)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid plan token"); return; } } filterChain.doFilter(request, response); } }
然后在Security配置中添加这个Filter:
@Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .addFilterBefore(prototypeTokenFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers(AUTH_WHITELIST).permitAll() .anyRequest().authenticated(); }
内容的提问来源于stack exchange,提问作者Rafael Lima
相关产品推荐
相关产品推荐

