所谓授权,就是对访问资源管理,看你是否有权限进行访问。访问资源通常是我们后端定义的url。我们对访问资源定义好权限后,用户进行登录后,访问其它url,这个时候,需要判断用户携带的权限和url定义的权限是否匹配,如果匹配,就允许访问,否则提示权限不足。在SpringSecurity中授权主要通过最后一个过滤器FilterSecurityIntercepter
来处理。
省略了其它不重要的过滤器
FilterSecurityInterceptor
:授权过滤器,即对访问资源进行拦截,然后调用其它接口进行授权处理
//该类定义的源码信息
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
private FilterInvocationSecurityMetadataSource securityMetadataSource;
private boolean observeOncePerRequest = true;
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
if (fi.getRequest() != null && this.observeOncePerRequest) {
fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
//调用了父类的方法
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
}
SecurityMetadataSource
: 安全数据源的接口可以理解为是一种安全对象的概念模型。即我们访问的资源(url),可以抽象成一个带有权限的对象,而这个接口就是用来设置安全的对象的权限信息。该类的继承关系如下:
Spring Security
对SecurityMetadataSource
提供了两个子接口 :
MethodSecurityMetadataSource
:
由Spring Security Core定义,用于表示安全对象是方法调用(MethodInvocation
)的安全元数据源。
FilterInvocationSecurityMetadataSource
:
由Spring Security Web定义,用于表示安全对象是Web请求(FilterInvocation
)的安全元数据源。通常使用此对象获取请求资源的。通常通过实现此接口,来保存数据库中查询的角色信息,来表示权限
//该接口方法的定义
public interface SecurityMetadataSource extends AopInfrastructureBean {
// ~ Methods
// =====================================================================
/**
*获取某个受保护的安全对象object的所需要的权限信息,是一组ConfigAttribute对象的集合,如果该安全对象object不被当前 SecurityMetadataSource对象支持,则抛出异常IllegalArgumentException。该方法通常配合
boolean supports(Class> clazz)一起使用,先使用boolean supports(Class> clazz)确保安全对象能被当前 SecurityMetadataSource支持,然后再调用该方法。
*/
Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException;
/**
获取该SecurityMetadataSource对象中保存的针对所有安全对象的权限信息的集合。该方法的主要目的是被AbstractSecurityInterceptor用于启动时校验每个ConfigAttribute对象。
*/
Collection<ConfigAttribute> getAllConfigAttributes();
/**
* 这里clazz表示安全对象的类型,该方法用于告知调用者当前SecurityMetadataSource是否支持此类安全对象,只有支持的时候,才能对这类安全对象调用getAttributes方法。
*/
boolean supports(Class<?> clazz);
}
Object表示安全对象 :
//获取访问的资源url FilterInvocation filterInvocation = (FilterInvocation) o; //获取请求url String requestUrl = filterInvocation.getRequestUrl();
Collection 保存安全对象的权限信息
//将访问资源的权限保存 for (Menu menu : menusWithRole) { if (antPathMatcher.match(menu.getUrl(),requestUrl)){ //讲角色的英文名 ROLE_* 保存到集合里面 String[] roleArr = menu.getRoles().stream().map(Role::getName).toArray(String[]::new); return SecurityConfig.createList(roleArr); } }
AccessDisisionManager
: 决策管理器,决定当前对象对访问资源到底有没有权限。在其实习类中,Spring Security引入了投票器的概念,真正的授权功能是通过一组AccessDecisionVoter
来实现的。
AccessDecisionManager
维护着一个AccessDecisionVoter
列表参与授权的投票。根据处理投票的策略不同
Spring Security
中AccessDecisionManager
有3个不同的实现。
此接口的继承关系如下:
子类的说明:
AffirmativeBased
: 只要有一个投票器投票通过,就允许访问资源。ConsensusBased
: 超过一半的投票器通过才允许访问资源。UnanimousBased
: 所有投票器都通过才允许访问资源。投票者:
RoleVoter
是Spring Security
内置的一个AccessDecisionVoter
,其会将ConfigAttribute
简单的看作是一个角色名称,在投票的时如果拥有该角色即投赞成票。如果ConfigAttribute
是以“ROLE_”开头的,则将使用RoleVoter
进行投票。当用户拥有的权限中有一个或多个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute
时其将投赞成票;如果用户拥有的权限中没有一个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute
,则RoleVoter
将投反对票;如果受保护对象配置的ConfigAttribute
中没有以ROLE_
开头的,则RoleVoter
将弃权。
思路:每次请求一个url,将所有的菜单权限信息查询出来。判断菜单里面的url和请求的url是否匹配,如果匹配,将菜单的角色保存ConfigAttribute
。
菜单表:
菜单类:
public class Menu implements Serializable {
/**
* id
*/
@ApiModelProperty("id")
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 访问的url
*/
@ApiModelProperty(" url")
private String url;
*/
/*
* 当前url的权限
*/
@ApiModelProperty("角色列表")
@TableField(exist = false) //声明表字段不存在,否则可能会报错
private List<Role> roles;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
自己定义一个类实现FilterInvocationSecurityMetadataSource
接口。
/**
* 根据url获取对应的角色,并保存
* FilterInvocationSecurityMetadataSource
* 该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色。
*/
@Component
public class CustomeFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
private MenuService menuService;
// 声明一个ant路径匹配器
private AntPathMatcher antPathMatcher=new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
FilterInvocation filterInvocation = (FilterInvocation) o;
//获取请求url
String requestUrl = filterInvocation.getRequestUrl();
//获取所有的菜单
List<Menu> menusWithRole = menuService.getAllMenusWithRole();
//获取匹配路径对应角色
for (Menu menu : menusWithRole) {
if (antPathMatcher.match(menu.getUrl(),requestUrl)){
//将角色的英文名 ROLE_* 保存到集合里面
String[] roleArr = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
return SecurityConfig.createList(roleArr);
}
}
//没有匹配的默认登录即可
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true; //支持当前对象
}
}
用户对象:
@TableName(value ="t_admin")
public class Admin implements Serializable, UserDetails {
/**
* id
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 用户的权限
* @return
*/
@TableField(exist = false)
private List<Role> roles;
//==========================当前对象所具备的权限===================================
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//将角色名转成SimpleGrantedAuthority对象放入集合中
List<SimpleGrantedAuthority> roleList = roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return roleList;
}
//===============================================================
//省略其它不必要属性
// ...
}
用户认证处理:
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
LambdaQueryWrapper<Admin> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(Admin::getUsername,s);
queryWrapper.eq(Admin::getEnabled,1);
Admin admin = adminMapper.selectOne(queryWrapper);
//设置权限
if (admin!=null){
admin.setRoles(roleMapper.getRoles(admin.getId()));
}
return admin;
}
}
思路:访问资源的权限有了,当前用户的权限有了,然后进行匹配,就可以知道当前用户到底有没有权限访问这个资源了。
自己定义一个类实现AccessDecisionManager
接口,重写其方法,按照自己的业务需求进行权限判断。
/**
* 判断url的角色和用户的角色是否匹配\
* 对访问的资源进行授权,决策。要求资源必须配置
* 对一次访问授权,需要传入三个信息。
* (1)认证过的Authentication,确定了谁正在访问资源。
* (2)被访问的资源object。
* (3)访问资源要求的权限配置ConfigAttribute。
*/
@Component
public class CusteomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//获取当前url的角色
for (ConfigAttribute configAttribute : collection) {
// roleConfig为SecurityConfig配置的角色
String roleConfig = configAttribute.getAttribute();
//判断是否登录角色
if ("ROLE_LOGIN".equals(roleConfig)) {
//判断是否登录
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录");
} else {
return;
}
} else {
//获取用户角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(roleConfig)) {
return;
}
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
自定义的资源权限配置,决策管理器需要注册到SpringSecurity中,才可以生效。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//密码加密工具
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//jwt过滤器
@Autowired
public JwtTokenFilter jwtTokenFilter;
//失败处理
@Autowired
private RestAccessDenyHandler restAccessDenyHandler;
@Autowired
private RestAuthenticationEntryPoint authenticationEntryPoint;
//注入自定义实现类
@Autowired
private CustomeFilter customeFilter;
@Autowired
private CusteomUrlDecisionManager custeomUrlDecisionManager;
/*
*访问资源放行配置
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring() //swagger+登录 允许
.antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"/index.html",
"/favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/kaptcha"
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//授权管理
http.authorizeRequests()
.anyRequest().authenticated()
//===========设置动态权限决策 FilterSecurityInterceptor是授权管理器==========
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
//=======================设置自定义实习类=======================
object.setAccessDecisionManager(custeomUrlDecisionManager);
object.setSecurityMetadataSource(customeFilter);
return object;
}
});
//关闭csrf 使用jwt,不需要csrf_token进行安全保护
http.csrf()
.disable()
//基于token,不需要session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//添加jwt 登录授权过滤器
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restAccessDenyHandler)
.authenticationEntryPoint(authenticationEntryPoint);
}
}