Springsecurity从数据库中动态加载登陆、授权、资源鉴权规则(重要)

五、加载动态数据进行登录与授权(重要)

实际的业务系统中,用户与权限的对应关系通常是存放在RBAC权限模型的数据库表中的

  • RBAC的权限模型可以从用户获取为用户分配的一个或多个角色,从用户的角色又可以获取该角色的多种权限。通过关联查询可以获取某个用户的角色信息和权限信息
  • 如果我们不希望用户、角色、权限信息写死在配置里面。我们应该实现UserDetails与UserDetailsService接口,从而从数据库或者其他的存储上动态的加载这些信息

5.1UserDetails与UserDetailsService接口

UserDetailsService接口有一个方法叫做loadUserByUsername,我们实现动态加载用户、角色、权限信息就是通过实现该方法。函数见名知义:通过用户名加载用户。该方法的返回值就是UserDetails(本质上是个实体类,Security会自动从里面取值进行对比)。

UserDetails就是用户信息,即:用户名、密码、该用户所具有的权限

源码中的UserDetails接口都有哪些方法:

public interface UserDetails extends Serializable {
    //获取用户的权限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    //获取密码
    String getPassword();

    //获取用户名
    String getUsername();

    //账号是否没过期
    boolean isAccountNonExpired();

    //账号是否没被锁定
    boolean isAccountNonLocked();

    //密码是否没过期
    boolean isCredentialsNonExpired();

    //账户是否可用
    boolean isEnabled();
}

我们把这些信息提供给Spring Security,Spring Security就知道怎么做登录验证了,

这也体现了Springboot的整体理念,配置大于编码,根本不需要我们自己写Controller实现登录验证逻辑

5.2、实现UserDetails 接口

一个适应于UserDetails的java POJO类,所谓的 UserDetails接口实现就是一些get方法。get方法由Spring Security调用,我们通过set方法或构造函数为 Spring Security提供UserDetails数据(从数据库查询)。

package com.springSecurityDemo.basicserver.config.auth;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * 实现用户信息接口,相当于Security又套了一层的用户实体类
 */
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetails implements UserDetails {

    //**********************************编写UserDetails相关属性
    public String password; //密码
    public String username;//用户名
    public boolean accountNonExpired; //当前账户是否过期
    public boolean accountNonLocked; //是否没被锁定
    public boolean credentialsNonExpired; //是否没过期
    public boolean enabled; // 账户是否可用
    Collection<? extends GrantedAuthority> authorities; // 用户权限集合

    //*****************通过下面的方法SpringSecuirty获取用户的的相关数据
    /**************这几个参数一定要传递好,否则会导致无法登陆,原本重写过来是null,我们应该重写成我们定义的属性传递回去**/
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        return this.authorities;
    }

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

    @Override
    public String getUsername() {

        return this.username;
    }


    //账号是否没过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //是否没被锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //密码是否没过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //账号是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }

    //******************************自定义set方法对黑盒子进行赋值让springsecurity进行调用
    public void setPassword(String password)
    {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

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

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

}

5.3实现UserDetailsService接口

5.3.0Dao层需要实现三个接口给Security查询出其想要的数据才能进行这部操作:

实现三个接口:一是通过userId(用户名)查询用户信息;二是根据用户名查询用户角色列表;三是**通过角色列表查询权限列表。**这里使用的是Mybatis

public interface MyUserDetailsServiceMapper {

    //根据userID查询用户信息
    @Select("SELECT username,password,enabled\n" +
            "FROM sys_user u\n" +
            "WHERE u.username = #{userId}")
    MyUserDetails findByUserName(@Param("userId") String userId);

    //根据userID查询用户角色
    @Select("SELECT role_code\n" +
            "FROM sys_role r\n" +
            "LEFT JOIN sys_user_role ur ON r.id = ur.role_id\n" +
            "LEFT JOIN sys_user u ON u.id = ur.user_id\n" +
            "WHERE u.username = #{userId}")
    List<String> findRoleByUserName(@Param("userId") String userId);


    //根据用户角色查询用户权限
    @Select({
      "<script>",
         "SELECT url " ,
         "FROM sys_menu m " ,
         "LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id " ,
         "LEFT JOIN sys_role r ON r.id = rm.role_id ",
         "WHERE r.role_code IN ",
         "<foreach collection='roleCodes' item='roleCode' open='(' separator=',' close=')'>",
            "#{roleCode}",
         "</foreach>",
      "</script>"
    })
    List<String> findAuthorityByRoleCodes(@Param("roleCodes") List<String> roleCodes);

}
  • 通常数据库表sys_user字段要和SysUser属性一一对应,比如username、password、enabled。但是比如accountNonLocked字段用于登录多次错误锁定,但我们一般不会在表里存是否锁定,而是存一个锁定时间字段。通过锁定时间是否大于当前时间判断账号是否锁定,所以实现过程中可以灵活做判断并用好set方法,不必拘泥于一一对应的形式。
  • 角色是一种特殊的权限,在Spring Security我们可以使用hasRole(角色标识)表达式判断用户是否具有某个角色,决定他是否可以做某个操作;通过hasAuthority(权限标识)表达式判断是否具有某个操作权限。
package com.springSecurityDemo.basicserver.config.auth;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 实现UserDetailsService,来通过用户名获取用户信息(也是Security的起始验证)
 */
@Component
public class MyUserDetailsService implements UserDetailsService {

    //注入之前写的dao接口
    @Resource
    private MyUserDetailsServiceMapper myUserDetailsServiceMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.加载基础用户信息 MyUserDetails是实现了UserDetails的实体类
        MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(username);

        if(myUserDetails == null){
            throw new UsernameNotFoundException("用户名不存在");
        }

        //2.加载用户角色列表
        List<String> roleCodes = myUserDetailsServiceMapper.findRoleByUserName(username);
        //3.通过用户角色列表加载用户的资源权限列表
        List<String> authority = myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
        //3.1角色是一个特殊的权限,也要添加到查出来的权限列表中,Security中必须有ROLE_前缀(规定标识)
        roleCodes.stream()
                .map(rc->"ROLE_"+rc) //每个对象前加前缀
                .collect(Collectors.toList()); //再转换回List
        //4.添加修改好前缀的角色前缀的角色权限
        authority.addAll(roleCodes);

        //5.把权限类型的权限给UserDetails
        myUserDetails.setAuthorities(
                //逗号分隔的字符串转换成权限权限类型列表
                AuthorityUtils.commaSeparatedStringToAuthorityList(
                        //List转字符串,逗号分隔
                        String.join(",",authority)
                )
        );
        return myUserDetails; //全部交给springsecurity
    }
}

5.4注册UserDetailsService

重写WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法

   @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
//        //静态配置用户
//        auth.inMemoryAuthentication()
//                .withUser("user")
//                .password(passwordEncoder().encode("123456"))
//                .roles("user")
//                    .and()
//                .withUser("admin")
//                .password(passwordEncoder().encode("123456"))
//                .authorities("sys:log","sys:user") //赋予资源id,放行其访问资源
//                //.roles("admin")
//                    .and()
//                .passwordEncoder(passwordEncoder());//配置BCrypt加密

        //从数据库中动态加载用户信息与权限
        //把做好的一系列myUserDetailsService信息交给security,并且设置加密方式
        auth.userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder());

    }

使用BCryptPasswordEncoder,表示存储中(数据库)取出的密码必须是经过BCrypt加密算法加密的。

六、动态加载资源鉴权规则(重要)

简单说“资源鉴权规则”就是:你有哪些权限?这些权限能够访问哪些资源?即:权限与资源的匹配关系。

6.1SecurityConfiger中的配置:

                //权限校验规则
             .authorizeRequests()
                //login页面和login的url谁都可以访问
                .antMatchers("/login.html","/login").permitAll()
//                        //权限表达式的使用:访问该url需要admin角色或ROLE_admin权限
//                .antMatchers("/system/*").access("hasAnyRole('admin') or hasAnyAuthority('ROLE_admin')")
                .antMatchers("/index").authenticated() //首页是只要登录了就可以访问
                //使用权限表达式规则 将自定义权限规则传入,所有url必须走我们写的权限规则方法,才能访问
                .anyRequest().access("@rbcaService.hasPermission(request,authentication)")
  • 首先将静态规则去掉(注释掉的部分内容),这部分内容我们将替换为动态从数据库加载
  • 登录页面“login.html”和登录认证处理路径“/login”需完全对外开发,不需任何鉴权就可以访问
  • 首页**"/index"必须authenticated,即:登陆之后才能访问**。不做其他额外鉴权规则控制。
  • 最后,其他的资源的访问我们通过权限规则表达式实现,表达式规则中使用了rbacService,这个类我们自定义实现。该类服务hasPermission从内存(或数据库)动态加载资源匹配规则,进行资源访问鉴权。

6.2动态资源鉴权规则

  • 首先通过登录用户名加载用户的urls(即资源访问路径、资源唯一标识)。
  • 如果urls列表中任何一个元素,能够和request.getRequestURI()请求资源路径相匹配,则表示该用户具有访问该资源的权限。
  • urls.stream().anyMatch是java8的语法,可以遍历数组,返回一个boolean类型。
  • hasPermission有两个参数,第一个参数是HttpServletRequest ,第二个参数是Authentication认证主体
  • 用户每一次访问系统资源的时候,都会执行这个方法,判断该用户是否具有访问该资源的权限。
package com.springSecurityDemo.basicserver.config.auth;


import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Component("rbcaService") //给这个bean取名
public class MyRBACService {


    @Resource
    private MyRBACServiceMapper rbacServiceMapper;

    //security提供的工具类
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 判断某用户是否有该请求资源的访问权限
     *
     * @param request
     * @param authentication
     * @return
     */
    public boolean hasPermission(HttpServletRequest request,
                                 Authentication authentication) {
        //从security中拿出用户主体,实际上是我们之前封装的UserDetials,
        //但是又被封了一层
        Object principal = authentication.getPrincipal();


        //如果取出的principal是我们放进去的UserDetails类,并且已经登录
        if (principal instanceof UserDetails) {
            //1.强转获取name
            String username = ((UserDetails) principal).getUsername();
			
            
            //2.从内存中获取权限(因为已经登录),放入security容器中,如果有的话返回true
    List<GrantedAuthority> authorityList =
          AuthorityUtils.commaSeparatedStringToAuthorityList(request.getRequestURI());
           
            return userDetails.getAuthorities().contains(authorityList.get(0));
            
            
            //2.通过用户名获取用户资源(用户找角色,角色找资源)(这里拿url做的标识,所以是url)
 //           List<String> urlByUserName = rbacServiceMapper.findUrlByUserName(username);

            //3.遍历urls,然后通过antPathMatcher判断是否匹配,匹配的上返回true
       //     return urlByUserName.stream().anyMatch(
      //              url -> antPathMatcher.match(url, request.getRequestURI())
       //     );

        }
        return false;
    }

}

鉴权加载规则与方法级别的权限验证与参数验证略,可以自己找资料如果需要

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值