1、需求
在spring security双token机制实现一文中已经实现了token的校验,在实际的项目中还需要根据用户的角色或用户Id对数据资源进行校验。
例如,有两个项目A和B,张三和李四都是项目实施人员这一角色,张三是项目A的项目组成员,李四是项目B的项目组成员,他们只能访问自己所属项目的资源。
2、实现
这里对spring security双token机制实现中的代码进行一部分改造。
2.1 改造TokenAuthenticationFilter
部分公共资源是可以不需要项目的权限就可以访问的,在前面的例子中,比如一些公共的模块(公司组织架构查询,帮助等)。token校验成功后可以将我们请求的资源需要的校验写入上下文中,以便过滤器链处理(代码中的NOTE注释)。
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private UserMapper userMapper;
private TokenMapper tokenMapper;
private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (userMapper == null) {
userMapper = SpringUtils.getBean(UserMapper.class);
}
if (tokenMapper == null) {
tokenMapper = SpringUtils.getBean(TokenMapper.class);
}
String tokenHeaderStr = request.getHeader("authorization");
String token = tokenHeaderStr.substring(7);
String userId = authenticateToken(token);
if (userId == null || userId.isEmpty()) {
logger.error("无效的token");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setCharacterEncoding("UTF-8");
response.getWriter().write("无效的token");
return;
}
User user = userMapper.findByUserIdWithRole(userId);
String requestUri = request.getServletPath();
// NOTE: 在上面校验用户的逻辑完成之后可以通过这样的方式将资源权限相关的属性写入Authentication上下文中,这里getNeedProjectAuth()方法获取该接口是否需要校验用户的项目权限
List<GrantedAuthority> authorityList =
AuthorityUtils.createAuthorityList(user.getRole().getRoleCode(), getNeedProjectAuth(requestUri).toString());
// NOTE: 这里将用户的信息写入Authentication上下文
Authentication authentication =
new UsernamePasswordAuthenticationToken(user, token, authorityList);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
// 实现shouldNotFilter方法,设置无需token校验的url
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String fullUri = request.getRequestURI();
String context_path = request.getContextPath();
String uri = fullUri.substring(fullUri.indexOf(context_path) + context_path.length());
return Arrays.asList(acceptUrls).contains(uri);
}
private String authenticateToken(String tokenStr) {
Token token = tokenMapper.getTokenByAccessToken(tokenStr);
if (token == null) {
return null;
}
LocalDateTime now = LocalDateTime.now();
if (token.getAccessExpireTime().isAfter(now)) {
return token.getUserId();
}
return null;
}
}
2.2 实现ProjectAuthenticationFilter
校验token成功之后通过ProjectAuthenticationFilter 来校验用户的项目权限,从上下文中读取Authentication,获得项目权限校验信息(根据url判断是否需要校验项目权限,用户是否具有该项目的权限)
@Component
public class ProjectAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private ProjectUserMapper projectUserMapper;
private static final Logger logger = LoggerFactory.getLogger(ProjectAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// ADMIN用户可以访问所有的项目资源
if (auth.getAuthorities().toArray()[0].toString().toUpperCase().equals("ROLE_ADMIN")) {
filterChain.doFilter(request, response);
} else {
/**
* 获取用户是否有请求项目资源的权限,方法略
*/
filterChain.doFilter(request, response);
}
}
// 在这里判断是否需要校验权限
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// NOTE:从上下文中获取Authentication ,根据接口所需资源情况判断是否需要校验用户的项目权限
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
boolean needProjectAuth = Boolean.parseBoolean(auth.getAuthorities().toArray()[1].toString());
return !needProjectAuth;
}
}
2.3 按校验顺序注册过滤器
这里需要改造下spring security配置类,按照恰当的顺序注册过滤器链(代码中的NOTE注释)。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenAuthenticationFilter tokenAuthenticationFilter;
@Autowired
private ServiceAuthenticationFilter serviceAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// NOTE:先经过token校验过滤器,再经过项目校验过滤器
http.csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and().addFilterBefore(tokenAuthenticationFilter, BasicAuthenticationFilter.class)
.addFilterAfter(projectAuthenticationFilter, TokenAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}