欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

利用spring security实现动态权限控制

程序员文章站 2022-05-05 22:55:39
...

前言:利用security实现权限控制

一,准备数据库MySql

1,创建五张表,user表、role表、uer_role表、menu表、menu_role表
user表:用来存放用户信息,字段id、name、username、password、enabled等
说明:必要的字段username和password,这两个字段表示登陆时需要验证的内容。
在登陆页面输入的用户名和密码即为这两个字段,name字段表示该用户的角色,
是管理员还是其他角色等,可自定义。enabled字段表示该用户是否启用。
role表:字段id、name和nameZh
说明:name表示角色英文名称,nameZh表示中文名称,记住,name字段必须得以
ROLE_开头;如下图:
利用spring security实现动态权限控制
user_role表:用户角色关联表,字段id、uid和rid,uid和rid分别设置两个外键对应user表和role表的id。
说明:该表的意义在于在用户登陆时可以通过该表查找到此用户的角色。
menu表:菜单表,其实是将后端的接口地址保存在数据库,如下图:利用spring security实现动态权限控制
其中url为后端接口地址,接口地址采取ant风格,和spring security的要求统一。
说明:该表非必需,但是为了好动态的控制权限,设置该表为角色动态添加功能。
menu_role表:菜单角色关联表,字段id、mid和rid
说明:该表用于通过角色查找对应的菜单,表示该角色能够访问哪些菜单项。实现权限控制,mid和rid分别设置外键和menu表id、role表id关联。

二,编写代码

1,创建实体类User、Role、Menu;根据字段创建,生成get和set方法。
其中Role和Menu正常创建即可。重点在User类
User类:创建完User类后,需要实现spring 中UserDetails类,并且实现里面的七个方法
如下源码图
利用spring security实现动态权限控制
实现里面的方法后,这里需要注意的是,这里面已经有了getPassword、getUsername和isEnabled,你在自动生成get和set时会和实现UserDetails时重复,需要把自动生成的删掉,不然就有两个对应的get方法。利用spring security实现动态权限控制
接下来就是UserService类了,该类需要实现UserDetailsService类,该类的源码:利用spring security实现动态权限控制
其中只有一个方法需要实现,loadUserByUsername(String usernam),见名知意,通过用户名查找用户信息。
利用spring security实现动态权限控制
在UserService中实现该方法后,需要注入UserMapper类
我这里的Hr其实就是User,loadUserByUsername该方法返回的是UserDetails对象,因为,你的User类实现了UserDetails类,所以这里需要返回User,接下来就是配置securityConfig配置类
利用spring security实现动态权限控制
标号1,注入你的UserService,我这里的Hr其实就是User,
标号2,返回spring security的密码加密实例,注意@bean注解,告诉spring这里使用的是
BCryptPasswordEncoder加密方式,这个加密方式生成的密文每次都不一样,比喻123加密后
生成的密文每次不一致,可能你会问那登陆怎么匹配呢?其实,它在生成密文时会加上一个
随机数,并且把随机数保存,登陆匹配时,再次提取即可。源码如下:

// Seed Generator
private static volatile SecureRandom seedGenerator = null;

/**
 * Constructs a secure random number generator (RNG) implementing the
 * default random number algorithm.
 *
 * <p> This constructor traverses the list of registered security Providers,
 * starting with the most preferred Provider.
 * A new SecureRandom object encapsulating the
 * SecureRandomSpi implementation from the first
 * Provider that supports a SecureRandom (RNG) algorithm is returned.
 * If none of the Providers support a RNG algorithm,
 * then an implementation-specific default is returned.
 *
 * <p> Note that the list of registered providers may be retrieved via
 * the {@link Security#getProviders() Security.getProviders()} method.
 *
 * <p> See the SecureRandom section in the <a href=
 * "{@docRoot}/../technotes/guides/security/StandardNames.html#SecureRandom">
 * Java Cryptography Architecture Standard Algorithm Name Documentation</a>
 * for information about standard RNG algorithm names.
 *
 * <p> The returned SecureRandom object has not been seeded.  To seed the
 * returned object, call the {@code setSeed} method.
 * If {@code setSeed} is not called, the first call to
 * {@code nextBytes} will force the SecureRandom object to seed itself.
 * This self-seeding will not occur if {@code setSeed} was
 * previously called.
 */

标3:该方法进行用户信息验证,进行密码匹配,将我们注入的UserService注入即可。
标4:此方法为重点,进行权限控制。主要是对请求进行控制验证和反馈。
利用spring security实现动态权限控制

http.authorizeRequests() //对请求授权,表示接下来需要配置授权规则
.anyRequest().authenticated()//表示,任何请求都需要通过我配置的授权规则才能访问,才能被系统认证

ok,路径规则配置完后,接下来需要配置登陆页面

.formLogin()//配置表单登陆
.usernameParameter("username")//登陆时的参数,springsecurity必须是post请求登陆
.passwordParameter("password")//这两个参数定义表示你传递的key
//springsecurity 默认就是username来接收用户输入的用户名,password接收用户输入密码
//你也可以设置成其他的
.loginProcessingUrl("/doLogin")
//发送用户输入的信息的路径,用户发送的信息,springsecurity会自动处理,
//不需要我们去写controller进行密码匹配的操作了
.loginPage("/login")
//配置登陆页面,如果前端需要自定义登陆页面这个必须配置
//你只需要在前端访问该路径时,controller里面返回login.html或者login.ftl即可

如果将usernameParameter和passwordParameter设置为自定义的,你在前端通过表单或者ajax发送的key就得跟自定义的保持一致。
那么登陆成功后该如何处理呢?这里有两种方式:
1,一般表单登陆的话,就可以登陆成功后直接跳转到首页,只需要配置如下方法即可

.successForwardUrl("/index")

2,如果是ajax登陆,那么返回的就是json数据了

.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
        res.setContentType("application/json;charset=utf-8");
        PrintWriter o = res.getWriter();
        Hr hr = (Hr) authentication.getPrincipal();//拿到用户对象包含了该用户的所有信息
        hr.setPassword(null);//将密码设置为null,密码不返回给前端
        RespBean ok = RespBean.ok("登陆成功", hr);//处理返回信息
        String s = new ObjectMapper().writeValueAsString(ok);//将RespBean 实例转换成字符串,因为前端接收的是json,其本质为字符串
        o.write(s);
        o.flush();
        o.close();
    }
})

RespBean 类,该类处理所有返回信息

/***
 * 单例模式
 */
public class RespBean {
    private Integer status;
    private String msg;
    private Object obj;

    public static RespBean ok(String msg){
        return new RespBean(200,msg,null);
    }

    public static RespBean ok(String msg,Object obj){
        return new RespBean(200,msg,obj);
    }

    public static RespBean error(String msg){
        return new RespBean(500,msg,null);
    }

    public static RespBean error(String msg,Object obj){
        return new RespBean(200,msg,obj);
    }

    private RespBean() {

    }

    private RespBean(Integer status, String msg, Object obj) {
        this.status = status;
        this.msg = msg;
        this.obj = obj;
    }

//省略getter和setter ...
}

有成功自然有失败,道理和成功一样有两种方式,
1,跳转错误页面

.failureForwardUrl("/error")

2,通过返回json,适用于ajax

.failureHandler(new AuthenticationFailureHandler() {
    @Override
    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
        res.setContentType("application/json;charset=utf-8");
        PrintWriter o = res.getWriter();
        RespBean error = RespBean.error("登陆失败");
        if (e instanceof LockedException) { //判断e属于哪个异常的实例来确定是何错误。
            error.setMsg("账户被锁定,请联系管理员!");
        } else if (e instanceof CredentialsExpiredException) {
            error.setMsg("密码过期,请联系管理员");
        } else if (e instanceof AccountExpiredException) {
            error.setMsg("账户过期,请联系管理员");
        } else if (e instanceof UsernameNotFoundException) {
            error.setMsg("用户名未找到,请重新输入!");
        } else if (e instanceof DisabledException) {
            error.setMsg("账户被禁用,请联系管理员");
        } else if (e instanceof BadCredentialsException) {
            error.setMsg("用户名或密码输入错误,请重新输入!");
        }
        o.write(new ObjectMapper().writeValueAsString(error));
        o.flush();
        o.close();
    }
})
.permitAll()//表示登陆的请求和页面路径任何人都允许访问

配置用户登出:

.logout()
.logoutUrl("/logout")//登出路径,默认就是/logout 可以不配置
.logoutSuccessHandler(new LogoutSuccessHandler() {
    @Override
    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
        res.setContentType("application/json;charset=utf-8");
        PrintWriter o = res.getWriter();
        o.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
        o.flush();
        o.close();
    }
})
.csrf().disable()//关闭csrf(跨站点请求伪造)保护,

如果未登录,直接通过地址栏输入地址访问spring security默认重定向到登陆页/login
我这里不需要重定向所以,进行一下配置

//处理未登录 直接通过地址栏访问,返回提示信息,不重定向到登录页
.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
    @Override
    public void commence(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
        res.setContentType("application/json;charset=utf-8");
        res.setStatus(401);
        PrintWriter o = res.getWriter();
        RespBean error = RespBean.error("登陆失败");
        if (e instanceof InsufficientAuthenticationException) {
            error.setMsg("未登录,访问失败!");
        }
        o.write(new ObjectMapper().writeValueAsString(error));
        o.flush();
        o.close();
    }

标5:该方法对静态资源访问进行配置,一般在/resource/static/下面的静态资源允许用户访问,忽略请求认证。

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

ok,那么如何实现,管理员权限大于普通用户呢,比喻说,如果是管理员,应该能访问用户的资源,但是,如果我并没有配置如下代码

.antMatchers("/user/**").hasAnyRole("ROLE_admin","ROLE_user")

而只是配置了,

.antMatchers("/user/**").hasRole("ROLE_user")

那么管理员访问不了,user下面的资源了,那这怎么办呢?
springsecurity 当然也考虑到了,可以配置角色继承

@Bean
RoleHierarchy roleHierarchy(){
    RoleHierarchy roleHierarchy = new RoleHierarchyImpl();
    String hierarchy = "ROLE_admin > ROLE_user";
    //如果有多个的话
    // String hierarchy = "ROLE_admin > ROLE_user \n ROLE_user > ROLE_test1";
    //这种方式适合spring 5及以上
    ((RoleHierarchyImpl) roleHierarchy).setHierarchy(hierarchy);
    return roleHierarchy;
}

利用spring security实现动态权限控制
到此基本配置结束了,那么,你有没有想过这样一个问题:比喻说要做一个公司的人事管理系统,里面有部门经理、招聘主管、培训主管和系统管理员,这些角色访问的资源肯定不一样,而且又包含关系,也有交集对吧。前面我们说过利用如下配置进行权限设置,但是这样写死的每个用户登陆,他访问的路径都不一样,怎么处理?这就需要动态权限管理了,当然如果你的项目不是那种权限很细的,其实这种够用了。

.antMatchers("/admin/**").hasRole("ROLE_admin")//拥有ROLE_admin 角色才能访问/admin/**匹配的路径
.antMatchers("/user/**").hasAnyRole("ROLE_admin","ROLE_user")

如何配置动态权限呢?首先,创建MyFilter类,加上注解@component,并且implements FilterInvocationSecurityMetadataSource类 FilterInvocationSecurityMetadataSource类继承SecurityMetadataSource类,需要实现三个方法:如下源码
利用spring security实现动态权限控制

@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;//注入菜单表,菜单表里面为后端方法路径
    AntPathMatcher antPathMatcher = new AntPathMatcher();//路径匹配的工具类,spring security里面有,无需自己写 
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) o).getRequestUrl(); //获取用户的访问路径
        List<Menu> menus = menuService.getAllMenusWithRole();//查询所有菜单以及对应的角色
        //sql语句为:
      //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;
        for (Menu menu : 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();//将角色名赋值给str
                }
                return SecurityConfig.createList(str);//创建角色属性列表
            }
        }
        //1,如果所有请求地址都没有匹配上的话,可以return null
        //2,返回SecurityConfig.createList("ROLE_LOGIN");
        //ROLE_LOGIN 只是一个标记,后边判断返回值 如果是ROLE_LOGIN的话 则让用户登陆后访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null; //返回null即可
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;/返回true即可
    }
}

该类并没有做实质性的权限控制,只是将该请求地址所对应的角色添加到spring的安全元数据属性列表上,方便在spring security 上下文调用。接下来进行权限控制
创建MyDecisionManager类,加上@component 注解,实现AccessDecisionManager类,源码如下:利用spring security实现动态权限控制
需要实现该类的三个方法

@Component
public class MyDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> ConfigAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : ConfigAttributes) {
            String needRole = configAttribute.getAttribute(); //在安全框架上下文中,已经添加了角色列表,此处即可获取对应路径的角色
//            System.out.println(needRole);
            if ("ROLE_LOGIN".equals(needRole)){//如果角色列表返回的是ROLE_LOGIN则表示未登录
                if (authentication instanceof AnonymousAuthenticationToken){//判断是否未登录 AnonymousAuthenticationToken 表示匿名
                    //判断用户是否登陆  根据authentication 是否是匿名的实例 AnonymousAuthenticationToken 为匿名登陆 即未登录
                    throw new AccessDeniedException("尚未登陆,请登录!");
                }else {
//                    如果不是匿名 则停止该方法
                    return;
                }
            }
//            获取登陆用户的角色 通过authentication.getAuthorities()获取
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)){//记住 needRole 是通过数据库里面查找到访问该路径的角色(通过MyFilter类查询到的),这里与登陆用户所拥有的角色对比
                    return;//如果匹配上,即可访问该路径
                }
            }
        }
//如果权限不足
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

//如下两个方法返回false即可
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return false;
    }

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

到此两个类也配置完了,那么如何加入到权限控制链上呢?
删除原来配置的利用spring security实现动态权限控制
添加如下配置利用spring security实现动态权限控制
大功告成!