spring security(2)https://developer.aliyun.com/article/1531011
三, 授权
主要是为了不同用户可以使用不同的功能
3.1 授权的基本流程
默认使用FilterSecurityInterceptor
来进行权限校验,在FilterSecurityInterceptor
中会从SecurityContextHolder
中获取其中的AUthentication 然后获取其中的权限信息,当前用户是否拥有访问当前资源所需的权限.
所以我们需要在项目中把登录用户的权限信息也存入Authentication
然后设置我们的资源所需的权限即可
3.2 授权访问
3.2.1 限制访问资源所需的权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限
第一步: 在SecurityConfig配置类添加如下,作用是开启相关配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
第二步: 开启了相关配置之后,就能使用@PreAuthorize等注解了。在HelloController类的hello方法,添加如下注解,其中test表示自定义权限的名字
@PreAuthorize("hasAuthority('test')")
3.2.3 封装权限信息
登录的时候从数据库获取权限放入UserDetails然后验证后存到redis
@Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查询用户信息 LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(lambdaQueryWrapper); //查询权限信息 if (user == null){ throw new UsernameNotFoundException("用户名不存在"); } LoginUser loginUser = new LoginUser(); loginUser.setUser(user); List<String> roles = Arrays.asList("test"); return loginUser; } }
package com.sucurity.domain; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Data public class LoginUser implements UserDetails { private User user; private List<String> permissions; @JSONField(serialize = false) //这个注解的作用是不让下面那行的成员变量序列化存入redis,避免redis不支持而报异常 private List<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (permissions != null) { return permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); } return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
访问其他的接口的时候经过的过滤器会从redis获取权限
//TODO 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = //第一个参数是LoginUser用户信息,第二个参数是凭证(null),第三个参数是权限信息(null) new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
完整版本
package com.sucurity.filter; import com.sucurity.domain.LoginUser; import com.sucurity.utils.JwtUtil; import com.sucurity.utils.RedisCache; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collection; import java.util.Objects; @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取token,指定你要获取的请求头叫什么 String xxtoken = request.getHeader("token"); //判空,不一定所有的请求都有请求头,所以上面那行的xxtoken可能为空 //!StringUtils.hasText()方法用于检查给定的字符串是否为空或仅包含空格字符 if (!StringUtils.hasText(xxtoken)) { //如果请求没有携带token,那么就不需要解析token,不需要获取用户信息,直接放行就可以 filterChain.doFilter(request, response); //return之后,就不会走下面那些代码 return; } //解析token String userid; //把userid定义在外面,才能同时用于下面的46行和52行 try { Claims claims = JwtUtil.parseJWT(xxtoken); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //从redis中获取用户信息 String redisKey = "login:" + userid; //LoginUser是我们在domain目录写的实体类 LoginUser loginUser = redisCache.getCacheObject(redisKey); //判断获取到的用户信息是否为空,因为redis里面可能并不存在这个用户信息,例如缓存过期了 if(Objects.isNull(loginUser)){ //抛出一个异常 throw new RuntimeException("用户未登录"); } //把最终的LoginUser用户信息,通过setAuthentication方法,存入SecurityContextHolder //TODO 获取权限信息封装到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = //第一个参数是LoginUser用户信息,第二个参数是凭证(null),第三个参数是权限信息(null) new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //全部做完之后,就放行 filterChain.doFilter(request, response); } }
3.3.3 从数据库查询权限信息
3.3.3.1 RABC 权限模型
这个是基于角色的权限模型
用户 ,角色,权限
用户 1:多 角色
角色 1: 多 权限
(assets/Pasted%20image%2020240229174251.png)
sql
use security; CREATE TABLE `sys_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名', `path` varchar(200) DEFAULT NULL COMMENT '路由地址', `component` varchar(255) DEFAULT NULL COMMENT '组件路径', `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标', `create_by` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)', `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='权限表'; CREATE TABLE `sys_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL, `role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串', `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)', `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag', `create_by` bigint(200) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(200) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `remark` varchar(500) DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; CREATE TABLE `sys_role_menu` ( `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id', PRIMARY KEY (`role_id`,`menu_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; CREATE TABLE `sys_user_role` ( `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id', `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id', PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; insert into sys_user_role values (2,1); insert into sys_role values (1,'经理','ceo',0,0,default,default,default,default,default), (2,'程序员','coder',0,0,default,default,default,default,default); insert into sys_role_menu values (1,1),(1,2); insert into sys_menu values (1,'部门管理','dept','system/dept/index',0,0,'system:dept:list','#',default,default,default,default,default,default), (2,'测试','test','system/test/index',0,0,'system:test:list','#',default,default,default,default,default,default)
第二步: 测试SQL语句,也就是确认一下你的建表、插入数据是否达到要求
# 通过用户id去查询这个用户具有的权限列表。也就是根据userid查询perms,并且限制条件为role和menu都必须正常状态么也就是等于0 SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = 2 AND r.`status` = 0 AND m.`status` = 0
对应的实体类
package com.sucurity.domain; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; //权限表(也叫菜单表)的实体类 @TableName(value="sys_menu") //指定表名,避免等下mybatisplus的影响 @Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) //Serializable是官方提供的,作用是将对象转化为字节序列 public class Menu implements Serializable { private static final long serialVersionUID = -54979041104113736L; @TableId private Long id; /** * 菜单名 */ private String menuName; /** * 路由地址 */ private String path; /** * 组件路径 */ private String component; /** * 菜单状态(0显示 1隐藏) */ private String visible; /** * 菜单状态(0正常 1停用) */ private String status; /** * 权限标识 */ private String perms; /** * 菜单图标 */ private String icon; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; /** * 是否删除(0未删除 1已删除) */ private Integer delFlag; /** * 备注 */ private String remark; }
spring security(4)https://developer.aliyun.com/article/1531015