在前面的文章中,我们通过在 SecurityConfig 配置文件 中配置对应路径所需要的角色,然后在设置用户拥有的角色,以此来判断用户是否能访问路径。
在我们实际的项目开发中,随着系统升级和迭代,我们开发出的接口越来越多,我们就不得不在配置文件中追加很多类似的代码,这不仅是费时费力,而且还对系统原有的代码造成一定的破坏,这明显是有大问题的。
如果我们可以像加载动态中账号那样,也动态加载权限,那就好了。
下面我们就来讨论如何动态加载权限吧
在 Security中,我们可以在配置认证和授权的策略中配置
对象后处理器 ObjectPostProcessor
,通过它我们可以自定义的判断每次请求url应该如何处理。
一、对象后处理器ObjectPostProcessor
ObjectPostProcessor是配置在HttpSecurity中的,ObjectPostProcessor主要配置2个参数,他们分别是 SecurityMetadataSource
授权的元数据和 AccessDecisionManager
权限决策管理。
1. SecurityMetadataSource
SecurityMetadataSource的参数为 FilterInvocationSecurityMetadataSource
,这个类主要是用来获取当前访问的地址需要哪些权限的,这是一个接口,我们可以实现它,动态的到数据源中获取。
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.ArrayUtil;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
/**
* 自定义 获取当前访问的地址需要哪些权限规则
*
* @author lixingwu
*/
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
/*** ant 路径匹配规则 */
private final static AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
/**
* 这个是主要的方法,该方法会需要返回url需要的权限列表
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation filterInvocation = (FilterInvocation) object;
String requestUrl = filterInvocation.getRequestUrl();
Console.log("请求地址为[{}]", requestUrl);
// 忽略指定的url
Set<String> permitSet = permitAll();
if (CollUtil.isNotEmpty(permitSet)) {
for (String matcher : permitSet) {
// 如果当前url和需要忽略的url匹配上就直接返回null,直接放行
if (ANT_PATH_MATCHER.match(matcher, requestUrl)) {
Console.log("请求地址为[{}]和忽略规则[{}]匹配,直接放行。", requestUrl, matcher);
return null;
}
}
}
// 根据路径查询路径需要什么权限才能访问
String[] permissions = findByPath(requestUrl);
if (ArrayUtil.isNotEmpty(permissions)) {
Console.log("请求地址为[{}]需要权限[{}]", requestUrl, permissions);
return createList(permissions);
}
// 没有匹配上的资源,都是登录访问
// 这里直接给一个权限login,用于标识登录后才能访问,此处做不处理
// 是否应该放行是决策管理器AccessDecisionManager需要做的事情
return createList("login");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
private List<ConfigAttribute> createList(String... attributeNames) {
return org.springframework.security.access.SecurityConfig.createList(attributeNames);
}
/*-----------------------------------------------*/
/*--------------- 以下是模拟的数据 ----------------*/
/*-----------------------------------------------*/
/***
* 该方法用于模拟那些路径不需要任何权限就可以访问的
* 真实情况下要去数据库中查询,这样便于修改
*/
private Set<String> permitAll() {
return CollUtil.newHashSet("/doLogin", "/code", "/open/**");
}
/***
* 该方法用于模拟获取访问指定值地址需要的权限
* 真实情况下要去数据库中查询,这样便于修改
* @param requestUrl 请求的地址
*/
private String[] findByPath(String requestUrl) {
HashMap<String, String[]> map = new HashMap<>(5);
map.put("/admin/**", new String[]{"admin"});
map.put("/guest/**", new String[]{"admin", "guest"});
map.put("/loginUser", new String[]{"login"});
for (String key : map.keySet()) {
if (ANT_PATH_MATCHER.match(key, requestUrl)) {
return map.get(key);
}
}
return new String[0];
}
}
2.AccessDecisionManager
权限决策管理 AccessDecisionManager
主要用于判断当前用户和 SecurityMetadataSource 提供的权限数据进行决策是通过还是阻断。我们可以实现 AccessDecisionManager 接口来自定义这些决策的策略。
import cn.hutool.core.lang.Console;
import cn.hutool.json.JSONUtil;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
/**
* 决策管理器,判断请求的url是通过还是阻断.
*
* @author lixin
*/
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Console.log("当前路径需要的权限有{}", configAttributes);
Console.log("当前登录用户{}", JSONUtil.toJsonStr(authentication));
// 当前访问的地址需要哪些用户角色
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
// 需要登录权限,但是已经登录的,直接通过
if ("login".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("未登录或者登录失效");
}else {
return;
}
}
//当前用户所具有的权限,如果用户有路径需要的权限,就通过
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("没有权限访问");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
二、配置ObjectPostProcessor到HttpSecurity
先把 SecurityMetadataSource 和 AccessDecisionManager 配置到 ObjectPostProcessor ,然后再 ObjectPostProcessor 配置 HttpSecurity 中,详情看代码:
import com.miaopasi.securitydemo.config.security.handler.*;
import com.miaopasi.securitydemo.config.security.impl.UrlAccessDecisionManager;
import com.miaopasi.securitydemo.config.security.impl.UrlFilterInvocationSecurityMetadataSource;
import com.miaopasi.securitydemo.config.security.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* Security配置类,会覆盖yml配置文件的内容
*
* @author lixin
*/
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JsonSuccessHandler successHandler;
private final JsonFailureHandler failureHandler;
private final JsonAccessDeniedHandler accessDeniedHandler;
private final JsonAuthenticationEntryPoint authenticationEntryPoint;
private final JsonLogoutSuccessHandler logoutSuccessHandler;
private final UserDetailsServiceImpl userDetailsService;
private final UrlFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
private final UrlAccessDecisionManager accessDecisionManager;
@Autowired
public SecurityConfig(JsonSuccessHandler successHandler, JsonFailureHandler failureHandler, JsonAccessDeniedHandler accessDeniedHandler, JsonAuthenticationEntryPoint authenticationEntryPoint, JsonLogoutSuccessHandler logoutSuccessHandler, UserDetailsServiceImpl userDetailsService, UrlFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource, UrlAccessDecisionManager accessDecisionManager) {
this.successHandler = successHandler;
this.failureHandler = failureHandler;
this.accessDeniedHandler = accessDeniedHandler;
this.authenticationEntryPoint = authenticationEntryPoint;
this.logoutSuccessHandler = logoutSuccessHandler;
this.userDetailsService = userDetailsService;
this.filterInvocationSecurityMetadataSource = filterInvocationSecurityMetadataSource;
this.accessDecisionManager = accessDecisionManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 配置请求对象的处理器
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 配置url元数据
object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
// 配置url权限的决策器
object.setAccessDecisionManager(accessDecisionManager);
return object;
}
})
.anyRequest().authenticated()
.and().formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().logout().logoutUrl("/doLogout")
.logoutSuccessHandler(logoutSuccessHandler)
.and().exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.and().cors()
.and().csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
三、给登录用户设置权限
在上面的步骤中,我们给url资源设置了权限,只有指定权限的用户才能访问。现在我们需要给用户分配指定的权限,不然用户没有权限将不能访问。现在我们只需要在登录时,把用户的权限信息设置到用户对象里面去,自定义决策管理器 UrlAccessDecisionManager 就能获取到用户有权限,然后执行决策管理器里面的逻辑。
用户在登录时会调用我们的 UserDetailsServiceImpl 类里面的 loadUserByUsername 方法,我们可以在这里把用户拥有的权限查询出来,然后设置给登录的用户。完整代码如下:
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.lang.Console;
import com.miaopasi.securitydemo.config.security.SysUser;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
/**
* 自定查询UserDetails
*
* @author lixin
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Console.log("根据用户名查询UserDetails,{}", username);
Optional<UserDetails> first = userDetailsList().stream()
.filter(userDetails -> Objects.equals(userDetails.getUsername(), username))
.findFirst();
if (first.isPresent()) {
return first.get();
}
throw new BadCredentialsException("[" + username + "]用户不存在");
}
/**
* 正常情况下,我们应该是去数据库中查询数据,但是为了方便显示,这里就使用模拟的查询出来的数据
* 当然,除了数据库查询,这里我们也可以去文件中、内存中、甚至网络上爬去的方式来获取到用户信息,只要能提供数据来源就行了。
* 模拟从数据库查询出来的用户信息列表,
*/
private List<UserDetails> userDetailsList() {
List<UserDetails> userDetails = new ArrayList<>(10);
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
SysUser user;
for (int i = 0; i < 10; i++) {
user = new SysUser();
user.setId(Convert.toLong(i));
user.setGmtCreate(DateTime.now());
user.setOperator("管理员");
user.setIsDelete(false);
user.setSort(BigDecimal.valueOf(0));
user.setStatus(0);
user.setRemarks("测试用户" + i);
user.setUsername("user" + i);
user.setPassword(passwordEncoder.encode("pwd_" + i));
// 设置用户的权限信息
user.setAuthorities(listByUserId(user.getId()));
userDetails.add(user);
}
userDetails.forEach(Console::log);
return userDetails;
}
/**
* 【数据模拟】根据用户id查询出用户需要的权限.
* 这里我们模拟 user0 -> [admin,guest]
* 其他用户 user[0|9] -> [guest]
*
* @param userId 用户id
* @return the list
*/
private List<GrantedAuthority> listByUserId(Long userId) {
HashMap<Long, List<GrantedAuthority>> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
List<GrantedAuthority> list = new ArrayList<>();
// 只要0这个用户才有admin的权限
if (userId == 0) {
list.add(new SimpleGrantedAuthority("admin"));
}
list.add(new SimpleGrantedAuthority("guest"));
map.put(userId, list);
}
return map.get(userId);
}
}
四、测试
(1)根据我们模拟的数据,我们设置了一些地址不需要任何权限就能访问:
/doLogin, /code, /open/**
,我们现在不登录依次访问这些接口,发现正常返回数据。
# /doLogin
{
"msg": "登录成功",
"code": 0,
"data": {
"authenticated": true,
"authorities": [
{}
],
"principal": {
"isDelete": false,
"sort": 0,
"gmtCreate": 1594827663999,
"operator": "管理员",
"authorities": [
{}
],
"id": 1,
"remarks": "测试用户1",
"username": "user1",
"status": 0
},
"details": {
"remoteAddress": "127.0.0.1"
}
}
}
# /code
QXAN8A
# /open/get
open get
如果我们访问一下没有忽略的接口,比如:/admin/get,返回JSON字符串:
{
"msg": "未登录或者登录失效",
"code": 1001,
"data": "Full authentication is required to access this resource"
}
(2)我们在模拟数据时给路径分配的权限策略为:
表达式 | 权限 |
---|---|
/admin/** | admin |
/guest/** | admin, guest |
[其他或者没有明确指定的] | login |
用户分配的权限策略为:
账号 | 权限 |
---|---|
user0 | admin, guest |
user[1|9] | guest |
根据分配的策略,我们可以得出:
user0用户登录后可以访问全部的接口,user1到user9 除了 /admin/** 接口不能访问,其他都可以访问。
我们现在登录user0后访问/admin/get 、/guest/get 、/open/get、test/get 都正常返回JSON字符串。
然后我们再登录user1后访问/guest/get 、/open/get、test/get 都正常返回JSON字符串,访问 /admin/get ,返回JSON字符:
{
"msg": "没有权限访问",
"code": 1002,
"data": "没有权限访问"
}
五、简单说一下
动态权限在本篇文章中使用的是模拟的数据,项目中要根据自己的业务来加载数据。其实主要就是要查询url的权限列表和用户拥有的权限列表,这样我们就可以自行对权限的列表和用户的权限列表进行动态的操作。
spring security系列文章请 点击这里 查看。
这是代码 码云地址 。
注意注意!!!项目是使用分支的方式来提交每次测试的代码的,请根据章节来我切换分支。