在Spring Security实现动态权限设置(一)——基于数据库登录一文中已经介绍了Spring Security是如何实现基于数据库登录的,上文中提到要创建Role
和User
实例,为了实现动态权限我们需要一个Menu
实例,这个实例是用来查找数据库中路径与所需角色的,创建Menu
实例也需要成员变量与数据库中Menu表的字段相对应,除此之外,还需要一个Role
类型的List
用来存储路径所需角色,显然这里Menu
与Role
有一个一对多的关系,后面在做查询要注意这一点。
Menu实例
public class Menu {
private int id;
private String pattern;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
}
动态权限设置思想
动态权限设置是基于数据库验证的,所有在这个“动态”是由数据库中用户权限变更保证的,不同于将用户权限写死在代码中,这里动态权限设置的大致思想是:当用户登录后,由获取用户访问路径并对其进行解析,查看数据库中访问该路径所需要的用户角色,并对比当前用户所拥有的角色,如果相匹配则可以访问;还有一些路径,只需要用户登录即可访问,无关用户角色,则可以在解析路径时返回默认标识以表示该路径无需用户角色;
MenuService和MenuMapper
在实现基于数据库登录的基础上,除了UserService
还需要一个MenuService
,MenuServic
中的getAllMenus
方法用来获取访问menu表中路径所需的角色,那么对应的需要MenuMapper
接口和XML去操作数据库:
MenuService
@Service
public class MenuService {
@Autowired
MenuMapper menuMapper;
public List<Menu> getAllMenus(){
return menuMapper.getAllMenus();
}
}
MenuMapper.xml
<mapper namespace="org.yc.security_dynamic.mapper.MenuMapper">
<resultMap id="BaseMap" type="org.yc.security_dynamic.bean.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"/>
<collection property="roles" ofType="org.yc.security_dynamic.bean.Role">
<id column="rid" property="id"/>
<result column="rname" property="name"/>
<result column="rnameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="getAllMenus" resultMap="BaseMap">
select m.*,r.id as rid ,r.name as rname , r.nameZh as rnameZh from menu m left join menu_role mr on m.id = mr.mid left join role r on mr.rid = r.id
</select>
</mapper>
XML中的SELECT查询是一个三表联合查询,查询结果应当是每个路径所需角色,由于每个路径所需角色可能不止有一个(这里数据库表里是每一个路径只有一个所需角色,但实际项目可能不是这样),所以路径和角色应该是一对多的关系,那么回到Mybatis
那一套,select标签中就不能写resultType
,而应该是resultMap
。
完成MenuService
之后可以在Test中做Service测试SQL查询结果是否是我们所期待的;这里提供一个简单测试:
@SpringBootTest
class SecurityDynamicApplicationTests {
@Autowired
MenuService menuService;
@Test
void contextLoads() {
System.out.println(menuService.getAllMenus());
}
}
返回结果如图:
FilterInvocationSecurityMetadataSource的实现类
针对menu表中的路径所需角色,需要在访问路径时,做路径解析,并获取路径角色。编写MyFileter类实现FilterInvocationSecurityMetadataSource
接口的作用就是这个。
@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
//路径匹配符 直接用以匹配路径
AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
/*根据请求地址 分析请求该地址需要什么角色*/
String url = ((FilterInvocation) o).getRequestUrl(); //获取请求地址
List<Menu> allMenus = menuService.getAllMenus();
for (Menu menu:allMenus
) {
if(pathMatcher.match(menu.getPattern() , url)){
List<Role> roles = menu.getRoles();
String[] rolesStr = new String[roles.size()];
for(int i = 0;i<roles.size();i++){
rolesStr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(rolesStr); //返回请求地址所需的角色
}
}
//请求地址没有匹配数据库中的地址则返回默认值,以作标识
return SecurityConfig.createList("ROLE_login");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
//是否支持该方法,返回true即可
return true;
}
}
该Filter中的getAttributes
方法获取到的是访问每条路径所需要的角色,并将其存储在类型为ConfigAttribute
的Collection
中作为返回值;接下来就要判断当前用户角色是否与访问路径所需角色相匹配,实现AccessDecisionManager
接口的MyAccessDecisionManager
类就是完成这件事的。
AccessDecisionManager的实现
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
/*
* authentication: 当前登录用户信息
* o:当前请求对象(FilterInvocation对象)
*collection:FilterInvocationSecurityMetadataSource接口实现类中getAttributes方法的返回值
* */
for (ConfigAttribute attribute:collection
) {
if("ROLE_login".equals(attribute.getAttribute())){ //路径不在数据库配置范围内,返回标志ROLE_login
if(authentication instanceof AnonymousAuthenticationToken){ //用户未登录
throw new AccessDeniedException("非法请求!");
}else{
return; //用户已经登录 无需判断 方法到此结束
}
}
//获取当前登录用户的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority:authorities
) {
if(authority.getAuthority().equals(attribute.getAttribute())) {
return; //当前登录用户具备所需的角色 则无需判断
}
}
}
throw new AccessDeniedException("非法请求!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
decide
方法中的以前面filter中返回的类型为ConfigAttribute
的collection
做变量的for循环会首先判断collection
中的值是不是“ROLE_login”
默认标识,表示该路径只要认证(登录)就可访问,如果是,那么只要判断当前用户对象authentication
不是未登录状态就可以访问(放行,直接退出方法);如果collection
不是ROLE_login
,则会以用户角色做for
循环,只要用户角色中有能匹配当前路径的角色则退出方法。如果整个for
循环都做完了仍没有退出方法,表示该访问非法,抛出异常即可。注意后面两个support
方法表示是否支持该方法,返回true
即可。
Security的配置类
编写完两个重要类之后,如何让它们生效呢?这就需要回到Security的配置类中,做配置:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Autowired
MyFilter myFilter;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(myAccessDecisionManager);
o.setSecurityMetadataSource(myFilter);
return o;
}
})
.and()
.formLogin()
.permitAll()
.and()
.csrf()
.disable();
}
}
只需在实现登录配置的基础上注入上面编写的两个类,并将这两个类配置在继承自FilterSecurityInterceptor
对象中即可生效。
Controller接口
接下来就编写controller接口来测试权限访问是否成功:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello security!";
}
@GetMapping("/admin/hello")
public String admin(){
return "hello admin!";
}
@GetMapping("/db/hello")
public String db(){
return "hello db!";
}
@GetMapping("/user/hello")
public String user(){
return "hello user!";
}
}
总结
动态权限设置,关键在于“动态”,而这个动态的实现靠的是修改数据库,那么我们要如何在Spring Security中实现动态获取数据库数据并进行判断就成了实现“动态”的关键所在。