SpringSecurity 进行自定义Token校验

时间:2022-02-18 13:40:51

背景

Spring Security默认使用「用户名/密码」的方式进行登陆校验,并通过cookie的方式存留登陆信息。在一些定制化场景,比如希望单独使用token串进行部分页面的访问权限控制时,默认方案无法支持。在未能在网上搜索出相关实践的情况下,通过官方文档及个别Stack Overflow的零散案例,形成整体思路并实践测试通过,本文即关于该方案的一个分享。

参考

官方文档:https://docs.spring.io/spring-security/site/docs/5.0.5.BUILD-SNAPSHOT/reference/htmlsingle/

SpringSecurity校验流程

基本的SpringSecurity使用方式网上很多,不是本文关注的重点,如有需要可以自行搜索或参见https://blog.csdn.net/u012702547/article/details/54319508

关于校验的整个流程简单的说,整个链路有三个关键点,

  • 将需要鉴权的类/方法/url),定义为需要鉴权(本文代码示例为方法上注解@PreAuthorize("hasPermission('TARGET','PERMISSION')")
  • 根据访问的信息产生一个来访者的权限信息Authentication,并插入到上下文中
  • 在调用鉴权方法时,根据指定的鉴权方式,验证权限信息是否符合权限要求

完整的调用链建议在IDE中通过单步调试亲自体会,本文不做相关整理。

如何自定义

我的需求,是使用自定义的token,验证权限,涉及到:

  • 产生Authentication并插入到上下文中
  • 针对token的验证方式

需要做的事情如下:

  • 自定义TokenAuthentication类,实现org.springframework.security.core.Authenticaion,作为token权限信息
  • 自定义AuthenticationTokenFilter类,实现javax.servlet.Filter,在收到访问时,根据访问信息生成TokenAuthentication实例,并插入上下文
  • 自定义SecurityPermissionEvalutor类,实现org.springframework.security.access.PermissionEvaluator,完成权限的自定义验证逻辑
  • 在全局的配置中,定义使用SecurityPermissionEvalutor作为权限校验方式

TokenAuthentication.java

/**
* @author: Blaketairan
*/ import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import java.util.ArrayList;
import java.util.Collection; /**
* Description: spring-security的Authentication的自定义实现(用于校验token)
*/
public class TokenAuthentication implements Authentication{
private String token;
public TokenAuthentication(String token){
this.token = token;
} @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<GrantedAuthority>(0);
} @Override
public Object getCredentials(){
return token;
} @Override
public Object getDetails() {
return null;
} @Override
public Object getPrincipal() {
return null;
} @Override
public boolean isAuthenticated() {
return true;
} @Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { } @Override
public String getName() {
return null;
}
}

AuthenticationTokenFilter.java

/**
* @author: Blaketairan
*/ import com.google.common.base.Strings;
import com.blaketairan.spring.security.configuration.TokenAuthentication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException; /**
* Description: 用于处理收到的token并为spring-security上下文生成及注入Authenticaion实例
*/
@Configuration
public class AuthenticationTokenFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException{ } @Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterChain)
throws IOException, ServletException{
if (servletRequest instanceof HttpServletRequest){
String token = ((HttpServletRequest) servletRequest).getHeader("PRIVATE-TOKEN");
if (!Strings.isNullOrEmpty(token)){
Authentication authentication = new TokenAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("Set authentication with non-empty token");
} else {
/**
* 在未收到Token时,至少塞入空TokenAuthenticaion实例,避免进入SpringSecurity的用户名密码默认模式
*/
Authentication authentication = new TokenAuthentication("");
SecurityContextHolder.getContext().setAuthentication(authentication);
System.out.println("Set authentication with empty token");
}
}
filterChain.doFilter(servletRequest, servletResponse);
} @Override
public void destroy(){
}
}

SecurityPermissionEvalutor.java

/**
* @author: Blaketairan
*/ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication; import java.io.Serializable; /**
* Description: spring-security 自定义的权限处理模块(鉴权)
*/
public class SecurityPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication,Object targetDomainObject, Object permission){
String targetDomainObjectString = null;
String permissionString = null;
String token = null;
try {
targetDomainObjectString = (String)targetDomainObject;
permissionString = (String)permission;
token = (String)authentication.getCredentials();
} catch (ClassCastException e){
e.printStackTrace();
return false;
}
return hasPermission(token, targetDomainObjectString, permissionString);
} @Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission){
/**
* 使用@PreAuthorize("hasPermission('TARGET','PERMISSION')")方式,不使用该鉴权逻辑
*/
return false;
} private boolean hasPermission(String token,String targetDomain, String permission){
/**
* 验证权限
**/
return true;
}
}

SecurityConfig.java 全局配置

/**
* @author: Blaketairan
*/ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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; /**
* Description: spring-security配置,指定使用自定义的权限评估方法
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception{
return super.authenticationManager();
} @Bean
public PermissionEvaluator permissionEvaluator() {
/**
* 使用自定义的权限验证
**/
SecurityPermissionEvaluator securityPermissionEvaluator = new SecurityPermissionEvaluator();
return securityPermissionEvaluator;
} @Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
/**
* 关掉csrf方便本地ip调用调试
**/
httpSecurity
.csrf()
.disable()
.httpBasic()
.disable();
} }

BaseRepository.java 某个需要权限验证的方法

/**
* @author: Blaketairan
*/ import org.springframework.security.access.prepost.PreAuthorize; import java.util.List; /**
* Description:
*/
public interface BaseRepository{
@PreAuthorize("hasPermission('DOMAIN', 'PERMISSION')")
void deleteAll();
}

结语

希望对看到本文的人有所帮助。