SpringBoot +Vue前后端项目 微人事(第七天)

后端接口权限设计

要利用Spring Security做动态权限控制,首先看一下数据库的权限控制的表
在这里插入图片描述
首先用户登录成功之后,会有用户id,根据用户id我可以查询出来他有哪些角色,根据他的角色我可以查询出来他可以操作哪些菜单,再到menu表中查看操作了哪些菜单
在这里插入图片描述
在进行接口设计的时候必须要和数据库中的menu表中的url属性是对应的

思路:
简单来说分为两步: 第一步,用户先从前端发起一个http请求,拿到http请求地址之后,我先去分析地址和数据库中的menu表中的哪一个url是相匹配的。就先看一下用户的请求地址跟这里边的哪一个是吻合的。
第一步的核心目的是根据用户的请求地址分析出来它所需要的角色,就是当前的请求需要哪些角色才能访问
第二步是去判断当前用户是否具备它需要的角色

注意:角色不分配给一级菜单,只分配给二级菜单,因为一级并没有一些实质性的接口
CustomFilterInvocationSecurityMetadataSource类
在config包中创建一个CustomFilterInvocationSecurityMetadataSource类,该类的作用是根据用户传来的请求地址,分析出请求需要的角色,该类需要实现FilterInvocationSecurityMetadataSource类并重写三个方法,第一个方法是最重要的。

第一个方法的Collection:当前请求需要的角色 Object:实际上是一个filterInvocation对象

从filterInvocation里面可以获取当前请求的地址,拿到地址后,我就要拿这个地址去数据库里面跟这里的每一个菜单项去匹配,看是符合哪一个模式,然后再去看这个模式需要哪些角色
String requestUrl = ((FilterInvocation) object).getRequestUrl();

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
 
    @Autowired
    MenuService menuService;
 
    AntPathMatcher antPathMatcher = new AntPathMatcher();
//    collenction:当前请求需要的角色  Object:实际上是一个filterInvocation对象
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //从filterInvocation里面可以获取当前请求的地址,拿到地址后,我就要拿这个地址去数据库里面跟这里的每一个菜单项去匹配,看是符合哪一个模式,然后再去看这个模式需要哪些角色
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        return null;
    }
 
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
 
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

修改model中的menu实体类

新加了private List roles; 这个菜单项需要哪些角色才能访问 一个菜单项对多个角色

public class Menu implements Serializable {
    private Integer id;
 
    private String url;
 
    private String path;
 
    private String component;
 
    private String name;
 
    private String iconCls;
 
    private Integer parentId;
 
    private Boolean enabled;
 
    private Meta meta;
 
    private List<Menu> children;  //children里面放的是List集合的Menu
 
    //这个菜单项需要哪些角色才能访问
    private List<Role> roles;
 
    //省略getter和setter

# 修改service包中的MenuService类

在service包的MenuService类中添加一个根据角色获取所有菜单的方法,返回在menuMapper接口中查询到的数据

@Service
public class MenuService {

    @Autowired
    MenuMapper menuMapper;
    public RespBean getMenusByHrId() {
      return RespBean.ok("操作成功!",menuMapper.getMenusByHrId( ((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId()));//Id从哪里来呢,前端传过来的信息是不可信,我们登录的用户信息保存在security,可以从Security里获取登录用户信息
    }



    /**
     * 获取所有的菜单角色   一对多 一个菜单项有多个角色
     * @return
     */
//    @Cacheable
    public List<Menu> getAllMenusWithRole(){
        return menuMapper.getAllMenusWithRole();
    }
}

修改mapper中的MenuMapper接口

List<Menu> getAllMenusWithRole();

这个方法先不写,现在sql数据库里面把sql语句先写好,写对了,再复制过去

在这里插入图片描述

定义MenuMapper.xml

<resultMap id="MenuWithRole" type="com.lqg.vhr.model.Menu" extends="BaseResultMap">
    <collection property="roles" ofType="com.lqg.vhr.model.Role">
      <id column="rid" property="id"/>
      <result column="rname" property="name"/>
      <result column="rnameZh" property="namezh"/>
    </collection>
  </resultMap>
  <select id="getAllMenusWithRole" resultMap="MenuWithRole">
    SELECT m.*,r.id as rid,r.`name` as rname,r.nameZh as rnamezh
    from menu m,menu_role mr,role r
    where m.id=mr.mid and mr.rid=r.id
    ORDER BY m.id
  </select>

在CustomFilterInvocationSecurityMetadataSource配置类里面注入MenuService,然后通过menuService.getAllMenusWithRole()

获取到所有的菜单数据了,这个方法大多数情况下都不会变,可以在service层的该方法上加上@Cacheable缓存

CustomUrlMyDecisionManager配置类

public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    AntPathMatcher pathMatcher = new AntPathMatcher();//创建AntPathMatcher,主要用来实现ant风格的URL匹配
    @Autowired
    MenuService menuService;
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //从filterInvocation里面可以获取当前请求的地址,拿到地址后,我就要拿这个地址去数据库里面跟这里的每一个菜单项去匹配,看是符合哪一个模式,然后再去看这个模式需要哪些角色
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        List<Menu> allMenus = menuService.getAllMenusWithRole();//从数据库中获取所有的资源信息
        for (Menu menu : allMenus) {//遍历资源信息
            if (pathMatcher.match(menu.getUrl(), requestUrl)) {//每个遍历的资源进行匹配当前请求的URL
                List<Role> roles = menu.getRoles();//获取相匹配资源的一个或多个角色Roles对象存在List集合里
                String[] rolesStr = new String[roles.size()];//创建一个字符串数组
                for (int i = 0; i < roles.size(); i++) {
                    rolesStr[i] = roles.get(i).getName();//把角色对象的角色名放在字符串数组里面
                }
                //因为这个方法的需要返回collection<ConfigAttribute>所以需要把rolesStr字串数组转成List数组
                return org.springframework.security.access.SecurityConfig.createList(rolesStr);
            }
        }
        //如果都匹配不上就返回ROLE_login这个角色名
        return  org.springframework.security.access.SecurityConfig.createList("ROLE_login");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}

这样我们的第一步就完成了,第一步的核心目的:根据用户的请求地址分析出它所需要的角色

第二步自定义AccessDecisionManager并重写decide方法,在该方法中判断当前登录的用户是否具备当前请求的URL所需要的信息,如果不具备,就抛出AccessDeniedException异常。

@Configuration
public class CustomUrlMyDecisionManager implements AccessDecisionManager {

    /*
   decide方法有三个参数:
   第一个参数Authentication authentication包含当前登录用户的信息;在User实体实现UserDetails接口的实现方法          getAuthorities方法处理了用户登录的时候存储了当前用户拥有的Role角色名。

   第二个参数则是一个FilterInvocation对象,可以获取当前请求对象等。
   第三个参数就是FilterInvocation中的getAttributes方法的返回值。
*/
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        //遍历需要的角色
        for (ConfigAttribute configAttribute : collection) {
            //它需要的角色
            String needRole = configAttribute.getAttribute();
            //如果它需要的角色是"ROLE_LOGIN"
            if ("ROLE_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 configAttribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}

CustomUrlMyDecisionManager配置类的作用是分析用户需要的角色你是否具备,如果具备,让请求继续往下走,如果不具备,则抛异常

两个关键类定义好了,接口来在SecurityConfig配置类里面把这两个定义好的配置类引入进来

在这里插入图片描述

在这里插入图片描述

   http.authorizeRequests()
                //剩下的其他请求都是登录之后就能访问的
//                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    //这边是将原对象传进去,执行对象的postProcess做一些修改之后在把对象返回回来
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {FilterSecurityInterceptor获取当前 request 对应的权限配置,调用访问控制器进行鉴权操作等都是核心功能。
                        object.setAccessDecisionManager(myDecisionManager);
                        object.setSecurityMetadataSource(metadataSource);
                        return object;
                    }
                })

接下来在HelloController控制类里面写两个方法测试一下

@Controller
public class HelloController {
 
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
 
    @GetMapping("/employee/basic/hello")
    public String hello2(){
        return "/emp/basic/hello";
    }
 
    @GetMapping("/employee/advanced/hello")
    public String hello3(){
        return "/emp/adv/hello";
    }
}

打开postman准备测试
修改完之后,登录成功再访问新添加的两个接口都是403,forbidden,这是不对的

在这里插入图片描述
再返回看一下登录时的数据

在这里插入图片描述
这里为null是因为我们从头到尾都没有去处理用户角色

查看用户Hr类的返回用户的所有角色的方法的返回值为null,我要给用户搞角色,就可以在hr类里面放一个role集合属性

还要给roles赋值,因为默认登录成功之后,用户是没有角色的

在这里插入图片描述
在这里插入图片描述

package com.xyg.pojo;

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.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
public class Hr implements UserDetails {
    private Integer id;

    private String name;

    private String phone;

    private String telephone;

    private String address;

    private Boolean enabled;

    private String username;

    private String password;

    private String userface;

    private String remark;

    private List<Role> roles;


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name == null ? null : name.trim();
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone == null ? null : phone.trim();
    }

    public String getTelephone() {
        return telephone;
    }

    public void setTelephone(String telephone) {
        this.telephone = telephone == null ? null : telephone.trim();
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address == null ? null : address.trim();
    }


    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setUsername(String username) {
        this.username = username == null ? null : username.trim();
    }



    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password == null ? null : password.trim();
    }

    public String getUserface() {
        return userface;
    }

    public void setUserface(String userface) {
        this.userface = userface == null ? null : userface.trim();
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark == null ? null : remark.trim();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
}

在HrService类里面用户登录成功之后,给用户设置角色

在这里插入图片描述

@Service
public class HrService implements UserDetailsService {

    @Autowired
    HrMapper hrMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Hr hr =hrMapper.loginByName(username);//根据名字查询用户
        if(hr==null){//判断查到的用户是否为空
            throw new UsernameNotFoundException("用户不存在!");
        }
        //登录成功之后,给用户设置角色
        hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
        return hr;
    }
}

在HrMapper接口里边加上getHrRolesById的方法

    List<Role> getHrRolesById(Integer id);

在HrMapper.xml文件里面加上如下代码

  <select id="getHrRolesById" resultType="com.xyg.pojo.Role">
    select r.* from role r,hr_role hrr where hrr.rid=r.id and hrid=#{id}
  </select>

现在再重启项目,登录成功之后访问localhost:8080/employee/basic/hello,显示如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
解决方法:

可以在SecurityConfig配置类里面加个方法即可,代码如下:
在这里插入图片描述

  @Override
    public void configure(WebSecurity web) throws Exception {//如果访问/login页不用经过SpringSecurity
        web.ignoring().antMatchers("/login");
    }

出现bug没有加载菜单
在这里插入图片描述
出现问题在
在这里插入图片描述
在这里插入图片描述
把它们改成一样就行,就不会拦截home一级菜单了
在这里插入图片描述
在这里插入图片描述
我没有登录直接访问菜单url会出现
在这里插入图片描述
出现这种情况
在这里插入图片描述

为了用户更好的体验效果
在main里添加一个判断
在这里插入图片描述
如果session里没有用户信息就会跳转到登录页面,有信息说明用户登录了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值