一、SpringSecurity 动态授权
上篇文章我们介绍了SpringSecurity的动态认证,上篇文章就说了SpringSecurity 的两大主要功能就是认证和授权,既然认证以及学习了,那本篇文章一起学习了SpringSecurity 的动态授权。
上篇文章地址:https://blog.csdn.net/qq_43692950/article/details/122393435
二、SpringSecurity 授权
我们接着上篇文章的项目继续修改,上篇文章中有说到我们WebSecurityConfig
配制类中的configure(HttpSecurity http)
这个方法就是用来做授权的,现在就可以来体验一下了,比如我们修改以admin
为开头的接口,权限或角色中需要有admin
:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("admin")
.antMatchers("/**").fullyAuthenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
}
下面使用admin
用户访问admin/test
接口:
报了403无权限的错误,因为我们设置了admin/**
接口必须要有admin
这个权限,可以看下上篇文章中写的UserService
类:
这边直接给用户设定死了一个admin角色,这里就有个问题了权限和角色有什么区别,其实在SpringSecurity 中权限和角色都放在了一起,可以说概念上是一样的,但角色是以ROLE_
开头的。
其中还需注意的是如果授权角色可以使用hasRole()
和hasAnyRole()
,如果是授权权限则使用hasAuthority()
和 hasAnyAuthority()
角色授权:授权代码需要加ROLE_前缀,controller上使用时不要加前缀。
权限授权:设置和使用时,名称保持一至即可。
所以可以修改UserService
类:
在此请求接口:
现在就有权限访问了,但是写死肯定不是我们要的效果,所以此时可以将角色放在数据库中,通过查询数据库动态获取用户的角色。
下面就需要在数据库中创建role
角色表:
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role` varchar(255) NOT NULL,
`role_describe` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
角色肯定是和人有关系的,而且有时多对多的关系,所以根据关系模型我们要抽取出一个角色用户关系表:
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userid` int(11) NOT NULL,
`roleid` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
对于角色的新增和关联用户,无非就是数据库的增删改,这里不做演示了,直接在创建好表可以在表中添加几条角色,并关联用户:
添加RoleEntity
实体
@Data
@TableName("/role")
public class RoleEntity {
private Long id;
private String role;
@TableField("role_describe")
private String roleDescribe;
}
RoleMapper
类,并写根据用户id查询全部角色的接口:
@Mapper
@Repository
public interface RoleMapper extends BaseMapper<RoleEntity> {
@Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #{userId}")
List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId);
}
修改UserService
类:
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<UserEntity>()
.eq(UserEntity::getUsername, username);
UserEntity userEntity = userMapper.selectOne(wrapper);
if (userEntity == null) {
throw new UsernameNotFoundException("用户不存在!");
}
List<GrantedAuthority> auths = roleMapper.getAllRoleByUserId(userEntity.getId())
.stream()
.map(r -> new SimpleGrantedAuthority(r.getRole()))
.collect(Collectors.toList());
userEntity.setRoles(auths);
return userEntity;
}
public boolean register(String userName, String password) {
UserEntity entity = new UserEntity();
entity.setUsername(userName);
entity.setPassword(new BCryptPasswordEncoder().encode(password));
entity.setEnabled(true);
entity.setLocked(false);
return userMapper.insert(entity) > 0;
}
}
下面就可以测试了,在浏览器再次访问上面的接口:
但是发现是403,原因是我们给admin设置的是权限admin
,不是角色,数据库中存的是ROLE_admin
,这里是想让大家对两者的区别更加深刻下,修改数据库为admin
重新启动再次访问:
已经可以访问了。上面大家应该对权限和角色有了一定的了解,下面对授权和授予角色的方法做下说明:
-
hasRole :
如果用户具备给定角色就允许访问,否则出现 403。给接口授权时无需写ROLE_
开头,因为底层代码会自动添加与之进行匹配,用户添加角色时必须写ROLE_
。 -
hasAnyRole
表示用户具备任何一个条件都可以访问。 -
hasAuthority :
如果当前的主体具有指定的权限,则返回 true,否则返回 false -
hasAnyAuthority
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返true
现在我们已经了解怎么样给用户授权了,也知道怎么给接口赋予权限了,但是还是有个问题:
这个都在代码里面写死也不合适呀,其实这里有两种方案,一种是地址和角色的固定变化不大的场景下,可以在这里从数据库中读取出来通过HttpSecurity
对象映射角色,但这种方案不太好在项目运行期间动态添加角色。还有一种方案就是实现FilterInvocationSecurityMetadataSource
接口,在这里面根据当前访问的url
返回该url
所具有的全部角色。显然后者更为灵活,但每次访问一次接口都取获取全部的角色肯定性能有所损失。
下面分别实现下这两种情况:
三、数据库读取通过HttpSecurity授权
上面已经创建了role
角色表,现在要做url
和role
的关联,所以添加一个menu
表用来存放url
:
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
menu
和role
也都是多对多的关系,所以也需要建一个menu_role
关系表:
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`menu_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
还是在表中添加一些数据:
创建MeunEntity
实体类:
@Data
@TableName("menu")
public class MeunEntity {
@TableId(type = IdType.AUTO)
private Integer id;
private String pattern;
}
MeunMapper
继承BaseMapper
@Mapper
@Repository
public interface MeunMapper extends BaseMapper<MeunEntity> {
}
修改RoleMapper
:
@Mapper
@Repository
public interface RoleMapper extends BaseMapper<RoleEntity> {
@Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #{userId}")
List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId);
@Select("SELECT r.id,r.role,r.role_describe FROM menu_role m,role r where m.role_id = r.id AND m.menu_id = #{menuId}")
List<RoleEntity> getAllRoleByMenuId(@Param("menuId") Integer menuId);
}
修改WebSecurityConfig
:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
MeunMapper meunMapper;
@Autowired
RoleMapper roleMapper;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(password());
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http
.authorizeRequests();
List<MeunEntity> meunEntities = meunMapper.selectList(null);
meunEntities.forEach(m -> {
authorizeRequests.antMatchers(m.getPattern()).hasAnyAuthority(roleMapper.getAllRoleByMenuId(m.getId())
.stream()
.map(RoleEntity::getRole).toArray(String[]::new));
});
authorizeRequests.antMatchers("/**").fullyAuthenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/register/**");
}
}
重启项目,然后再次访问测试接口,已经实现和上面相同的效果:
四、通过FilterInvocationSecurityMetadataSource 动态角色
上面已经实现了第一种方案,下面继续实现第二中方案,下面创建一个类实现FilterInvocationSecurityMetadataSource
接口:
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MeunMapper meunMapper;
@Autowired
RoleMapper roleMapper;
//用来实现ant风格的Url匹配
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//获取当前请求的Url
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<MeunEntity> list = meunMapper.selectList(null);
List<ConfigAttribute> roles = new ArrayList<>();
list.forEach(m -> {
if (antPathMatcher.match(m.getPattern(), requestUrl)) {
List<ConfigAttribute> allRoleByMenuId = roleMapper.getAllRoleByMenuId(m.getId())
.stream()
.map(r -> new SecurityConfig(r.getRole()))
.collect(Collectors.toList());
roles.addAll(allRoleByMenuId);
}
});
if (!roles.isEmpty()) {
return roles;
}
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
还需创建一个CustomAccessDecisionManager
用来实现AccessDecisionManager
:
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ca) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : ca) {
//如果请求Url需要的角色是ROLE_LOGIN,说明当前的Url用户登录后即可访问
if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken){
return;
}
Collection<? extends GrantedAuthority> auths = auth.getAuthorities(); //获取登录用户具有的角色
for (GrantedAuthority grantedAuthority : auths) {
if (configAttribute.getAttribute().equals(grantedAuthority.getAuthority())){
return;
}
}
}
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
修改WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
CustomAccessDecisionManager customAccessDecisionManager;
@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(password());
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
o.setAccessDecisionManager(customAccessDecisionManager);
return o;
}
})
.antMatchers("/**").fullyAuthenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/register/**");
}
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_admin > ROLE_user > ROLE_common";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
再次测试上面的测试接口,可以发现也达到了相同的效果:
但是此时是动态角色的,我们可以创建一个新用户,给新用户一个新的角色,再给该角色赋予admin/**
的权限。
创建用户adc
添加角色:
角色绑定用户:
角色绑定menu:
下面清楚浏览器的缓存,使用abc用户登录:
成功访问接口,说明动态角色权限已经生效了。
喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!