13.动态权限配置

要利用spring security做动态权限控制,首先看一下数据库的权限控制的表

首先用户登录成功后,会有用户id,根据用户id我可以查询出来他有哪些角色,根据他的角色我可以查询出来他可以操作哪些菜单,再到menu表中查看操作了哪些菜单

在进行接口设计的时候必须要和数据库种的menu表中的url属性时对应的
思路:

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

注意:角色不分配给一级菜单,只分配给二级菜单,因为一级并没有一些实质性的接口

CustomFilterInvocationSecurityMetadataSource类

在config包中创建一个CustomFilterInvocationSecurityMetadataSource类,该类的作用是根据用户传来的请求地址,分析出请求需要的角色,该类需要实现FilterInvocationSecurityMetadataSource类并重写三个方法,第一个方法是最重要的。
第一个方法的Collection:当前请求需要的角色 Object:实际上是一个filterInvocation对象 (invocation调用)

从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;
 
    /**
     * 通过用户id获取菜单
     * @return
     */
    public List<Menu> getMenusByHrId() {
        //要传入id了,id从哪里来,我们登录的用户信息保存到security里面
        return menuMapper.getMenusByHrId(((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId());
        //SecurityContextHolder里面有一个getContext()方法.getAuthentication()它里面的getPrincipal(),Principal它是当前登录的用户对象,然后强转成Hr对象再获取它里面的id
    }
 
    /**
     * 获取所有的菜单角色   一对多 一个菜单项有多个角色
     * @return
     */
//    @Cacheable
    public List<Menu> getAllMenusWithRole(){
        return menuMapper.getAllMenusWithRole();
    }
}

修改mapper中的MenuMapper接口

@Repository
public interface MenuMapper {
    int deleteByPrimaryKey(Integer id);
 
    int insert(Menu record);
 
    int insertSelective(Menu record);
 
    Menu selectByPrimaryKey(Integer id);
 
    int updateByPrimaryKeySelective(Menu record);
 
    int updateByPrimaryKey(Menu record);
 
    List<Menu> getMenusByHrId(Integer hrid);
 
    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缓存

@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();
//      这个方法每次请求都会调用
        List<Menu> menus = menuService.getAllMenusWithRole();
        //比较request跟这menus里面的url是否一致 遍历menus 借助AntPathMatcher工具进行
        for (Menu menu : menus) {
//          String pattern:menus里面的规则
            if (antPathMatcher.match(menu.getUrl(),requestUrl)){
                List<Role> roles = menu.getRoles();
                String[] str = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(str);
            }
        }
//      没匹配上的统一登录之后就可以访问  "ROLE_LOGIN"只是一个标记
        return SecurityConfig.createList("ROLE_LOGIN");
    }
 
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
 
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

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

CustomUrlMyDecisionManager配置类

第二步:判断当前用户是否具备这些角色,我要在config配置包里面定义CustomUrlMyDecisionManager配置类,该类需要实现AccessDecisionManager并重写三个方法,第一个方法是最重要的

@Component
public class CustomUrlMyDecisionManager implements AccessDecisionManager {
    /**
     *
     * @param authentication 当前登录的用户
     * @param object 请求对象
     * @param configAttributes 是CustomFilterInvocationSecurityMetadataSource类中的getAttributes方法的返回值
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    //很好比对,用户的角色在authentication里面,需要的角色在configAttributes里面,再区比较他们俩集合里面有没有包含关系就行
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //遍历需要的角色
        for (ConfigAttribute configAttribute : configAttributes) {
            //它需要的角色
            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 true;
    }
 
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

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

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

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    HrService hrService;
 
    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
 
    @Autowired
    CustomUrlMyDecisionManager customUrlMyDecisionManager;
 
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
 
    //要有一个configure方法吧hrService整进来
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(hrService);
    }
 
    //配置登录成功或者登录失败向前端传送json数据
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //剩下的其他请求都是登录之后就能访问的
//                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(customUrlMyDecisionManager);
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return object;
                    }
                })
                .and()
                //表单登录
                .formLogin()
                //修改默认登录的username
                .usernameParameter("username")
                //修改默认登录的password
                .passwordParameter("password")
                //处理表单登录的url路径
                .loginProcessingUrl("/doLogin")
                //默认看到的登录页面,如果是前后端分离的话,就不用配置登录页面
                .loginPage("/login")
                //登录成功的处理
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        //如果登录成功就返回一段json
                        resp.setContentType("application/json;charset=utf-8");
                        //这是往出写的
                        PrintWriter out = resp.getWriter();
                        //登录成功的hr对象
                        Hr hr = (Hr)authentication.getPrincipal();
                        hr.setPassword(null);
                        RespBean ok = RespBean.ok("登录成功!", hr);
                        //把hr写成字符串
                        String s = new ObjectMapper().writeValueAsString(ok);
                        //把字符串写出去
                        out.write(s);
                        out.flush();
                        out.close();
 
 
                    }
                })
                //登录失败的处理
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
                        //如果登录成功就返回一段json
                        resp.setContentType("application/json;charset=utf-8");
                        //这是往出写的
                        PrintWriter out = resp.getWriter();
                        RespBean respBean = RespBean.error("登录失败!");
                        if(exception instanceof LockedException){
                            respBean.setMsg("账户被锁定,请联系管理员!");
                        }else if (exception instanceof CredentialsExpiredException){
                            respBean.setMsg("密码过期,请联系管理员!");
                        }else if (exception instanceof AccountExpiredException){
                            respBean.setMsg("账户过期,请联系管理员!");
                        }else if (exception instanceof DisabledException){
                            respBean.setMsg("账户被禁用,请联系管理员!");
                        }else if (exception instanceof BadCredentialsException){
                            respBean.setMsg("用户名或者密码输入错误,请重新输入!");
                        }
                        out.write(new ObjectMapper().writeValueAsString(respBean));
                        out.flush();
                        out.close();
                    }
                })
                //跟登录相关的接口就能直接访问
                .permitAll()
                .and()
                .logout()
                //注销成功后的回调
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
                        out.flush();
                        out.close();
                    }
                })
                .permitAll()
                .and()
                //关闭csrf攻击
                .csrf().disable();
 
    }
 
}

测试

接下来在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赋值,因为默认登录成功之后,用户是没有角色的

public class Hr implements UserDetails {
    /**
     * hrID
     */
    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;
 
 
    /**
     * 账户是否没有过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    /**
     * 账户是否被锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    /**
     * 密码是否没有过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    /**
     * 是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return enabled;
    }
 
    public void setUsername(String username) {
        this.username = username;
    }
 
    /**
     *返回用户的所有角色
     * @return
     */
    @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 s) throws UsernameNotFoundException {
        Hr hr = hrMapper.loadUserByUsername(s);
        if (hr == null) {
            throw new UsernameNotFoundException("用户名不对");
        }
        //登录成功之后,给用户设置角色
        hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
        return hr;
    }
}

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

@Repository
public interface HrMapper {
    int deleteByPrimaryKey(Integer id);
 
    int insert(Hr record);
 
    int insertSelective(Hr record);
 
    Hr selectByPrimaryKey(Integer id);
 
    int updateByPrimaryKeySelective(Hr record);
 
    int updateByPrimaryKey(Hr record);
 
    /**
     * 通过用户名查找用户
     * @param username
     * @return
     */
    Hr loadUserByUsername(String username);
 
    List<Role> getHrRolesById(Integer id);
}

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

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

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


还有个小bug就是没有登录之前,就访问接口,会出来如下页面:

解决方法:

可以在SecurityConfig配置类里面加个方法即可,代码如下:

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/login");
}

至此,后端接口权限设计已经完成了

posted @ 2020-08-07 16:59  柒丶月  阅读(177)  评论(0编辑  收藏  举报