文章目录

  • 回顾
  • 授权
  • 创建项目
  • 创建项目:security-mybatis-roles
  • 数据库表设计
  • mybatis 关联数据库配置
  • 创建用户 User类和权限 Authority类
  • 创建Mapper接口和映射xml
  • 创建service 类UserDetailsServiceImpl 实现接口UserDetailsService
  • 配置关联spring security
  • 创建HelloController ,定义对应接口
  • 启动测试

授权

用户如果要访问某一个资源,我们要去检查用户是否具备这样的权限,如果具备就允许访问,如果不具备,则不允许访问,这就是授权
我们通过案例讲解权限基于数据库的配置和权限的配置

创建项目

由于 Spring Security 支持多种数据源,例如内存、数据库、LDAP 等,这些不同来源的数据被共同封装成了一个 UserDetailService 接口,任何实现了该接口的对象都可以作为认证数据源

通过上一篇文章循序渐进学spring security第六篇,手把手教你如何从数据库读取用户进行登录验证,mybatis集成 我们介绍了如何从数据库关联到spring security 用户认证的配置,今天,我们还是基于mybatis 读取数据库用户和权限来进行授权案例的讲解

创建项目:security-mybatis-roles

引入依赖:

springSecurity权限精确到按钮_spring


具体依赖如下:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>

        <!--mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

增加了JSON的解析依赖,这样我们就可以基于前后端分离的方式,登录验证后返回JSON,如果不了解的同学,可以先学习下循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串?

数据库表设计

CREATE TABLE `h_user` (
                          `username` varchar(50) NOT NULL,
                          `password` varchar(500) NOT NULL,
                          `enabled` tinyint(1) NOT NULL,
                          PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

CREATE TABLE `h_authorities` (
                                 `username` varchar(50) NOT NULL,
                                 `authority` varchar(50) NOT NULL,
                                 UNIQUE KEY `ix_auth_username` (`username`,`authority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

这里简单的准备了两张表,一张是用户表,只有三个字段用户名、密码、是否可用;还有一张权限表,只有两个字段,用户名和权限,用户和权限确定一条唯一的权限记录。一个用户可能存在多个权限,一个权限可能同时有多个用户,因此,用户和权限的关系是多对多的关系,
插入默认数据

INSERT INTO h_user (username, password, enabled) VALUES (‘harry’, ‘123456’, ‘1’);
INSERT INTO h_user (username, password, enabled) VALUES (‘mike’, ‘123456’, ‘1’);

INSERT INTO h_authorities (username, authority) VALUES (‘harry’, ‘admin’);
INSERT INTO h_authorities (username, authority) VALUES (‘harry’, ‘user’);
INSERT INTO h_authorities (username, authority) VALUES (‘mike’, ‘user’);

这些数据中,用户:harry,具备有admin,user的权限,而mike ,只有user的权限

mybatis 关联数据库配置

创建用户 User类和权限 Authority类

public class User implements UserDetails {
    private String password;  //密码
    private String username;   //用户名
    private boolean accountNonExpired=true;
    private boolean accountNonLocked=true;
    private boolean credentialsNonExpired=true;
    private boolean enabled;   //是否可用

    private List<Authority>authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
public class Authority implements GrantedAuthority {
    private String username;
    //权限名称
    private String authority;
    @Override
    public String getAuthority() {
        return "ROLE_"+authority;
    }
}

这里,一个用户可能有多个权限,因此,在用户的实体类中,有一个List权限的字段authorities,通过前面的学习,我们知道,用户要实现接口UserDetails ,当然也可以不用实现UserDetails ,但是最终也要转换,我们这里是图方便,就实现了接口UserDetails 。权限需要实现接口GrantedAuthority ,也是为了方便;

这里 getAuthority()返回的权限前加“ROLE_” 前缀,为什么要加这个前缀?主要是因为在进行配置权限时,默认spring security会自动在权限前加个前缀,所以我们从数据库读取权限出来时,因为我们存储的时候没有这前缀,因此要得加上

我们也可以跟踪配置到源码看看,确实也是会自动加上的前缀

springSecurity权限精确到按钮_java_02

创建Mapper接口和映射xml

@Mapper//指定这是一个操作数据库的mapper
public interface UserMapper {
	//根据用户名查找用户
    User findUserByUsername(String username);
}
<mapper namespace="com.harry.security.mapper.UserMapper">

    <resultMap id="BaseUser" type="com.harry.security.entity.User" >
        <id property="username" column="username" ></id>
        <result property="password" column="password" ></result>
        <result property="enabled" column="enabled" ></result>
        <collection property="authorities" ofType="com.harry.security.entity.Authority">
            <result property="username" column="username"/>
            <result property="authority" column="authority"/>
        </collection>
    </resultMap>
    <select id="findUserByUsername" resultMap="BaseUser" parameterType="string">
        select u.username,u.`password`,u.enabled,hauth.authority from h_user u LEFT JOIN h_authorities hauth on hauth.username=u.username where u.username=#{username}
    </select>

</mapper>

这里介绍一下,resultMap 中的collection 标签的配置,主要是为了将SQL联表查询权限映射为一个集合,这样一来,一个用户读取出来,就包含了他所有的权限集合

创建service 类UserDetailsServiceImpl 实现接口UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userByUsername = userMapper.findUserByUsername(username);
        return userByUsername;
    }

}

配置关联spring security

创建类SecurityConfig 继承 WebSecurityConfigurerAdapter ,并配置关联UserDetailsServiceImpl 从数据库读取,配置登录验证以JSON形式交互,同时配置两个接口的访问权限,凡是接口是/admin/** 类型的都要具备admin的权限才能访问,凡是/user/*类型要有user权限才能访问

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated()
                .and().formLogin()
                .successHandler((req,resp,authentication)->{
                    Object principal = authentication.getPrincipal();
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(JSON.toJSONString(principal));
                    out.flush();
                    out.close();
                })
                .failureHandler((req, resp, e) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(JSON.toJSONString(e));
                    out.flush();
                    out.close();
                })
        .permitAll()
                .and().exceptionHandling()
                .authenticationEntryPoint((req, resp, authException) -> {
                            resp.setContentType("application/json;charset=utf-8");
                            PrintWriter out = resp.getWriter();
                            out.write(JSON.toJSONString("尚未登录,请先登录"));
                            out.flush();
                            out.close();
                        }
                )
                .and().logout().logoutSuccessHandler((req,resp,authentication)->{
                    Object principal = authentication.getPrincipal();
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(JSON.toJSONString(principal));
                    out.flush();
                    out.close();
                })
                .and().csrf().disable()
        ;
    }
}

这里的匹配规则我们采用了 Ant 风格的路径匹配符,Ant 风格的路径匹配符在 Spring 家族中使用非常广泛,它的匹配规则也非常简单:

通配符

含义

**

匹配多层路径

*

匹配一层路径

?

匹配任意单个字符

上面配置的含义是:

如果请求路径满足 /admin/** 格式,则用户需要具备 admin 角色。

如果请求路径满足 /user/** 格式,则用户需要具备 user 角色。

剩余的其他格式的请求路径,只需要认证(登录)后就可以访问。

另一方面,如果你强制将 anyRequest 配置在 antMatchers 前面,像下面这样:

http.authorizeRequests()
        .anyRequest().authenticated()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .and()

此时项目在启动的时候,就会报错,会提示不能在 anyRequest 之后添加 antMatchers:

springSecurity权限精确到按钮_spring_03

这从语义上很好理解,anyRequest 已经包含了其他请求了,在它之后如果还配置其他请求也没有任何意义。

从语义上理解,anyRequest 应该放在最后,表示除了前面拦截规则之外,剩下的请求要如何处理。

在拦截规则的配置类 AbstractRequestMatcherRegistry 中,我们可以看到如下一些代码(部分源码):

public abstract class AbstractRequestMatcherRegistry<C> {
 private boolean anyRequestConfigured = false;
 public C anyRequest() {
  Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself");
  this.anyRequestConfigured = true;
  return configurer;
 }
 public C antMatchers(HttpMethod method, String... antPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.antMatchers(method, antPatterns));
 }
 public C antMatchers(String... antPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
 }
 protected final List<MvcRequestMatcher> createMvcMatchers(HttpMethod method,
   String... mvcPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure mvcMatchers after anyRequest");
  return matchers;
 }
 public C regexMatchers(HttpMethod method, String... regexPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.regexMatchers(method, regexPatterns));
 }
 public C regexMatchers(String... regexPatterns) {
  Assert.state(!this.anyRequestConfigured, "Can't configure regexMatchers after anyRequest");
  return chainRequestMatchers(RequestMatchers.regexMatchers(regexPatterns));
 }
 public C requestMatchers(RequestMatcher... requestMatchers) {
  Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
  return chainRequestMatchers(Arrays.asList(requestMatchers));
 }
}

从这段源码中,可以看到,在任何antMatchers拦截规则之前(包括 anyRequest 自身),都会先判断 anyRequest 是否已经配置,如果已经配置,则会抛出异常,系统启动失败。

这样大家就理解了为什么 anyRequest 一定要放在最后。

创建HelloController ,定义对应接口

@RestController
public class HelloController {

//    @PreAuthorize("admin")
    @RequestMapping("/sayHello")
    public String sayHello(){

        return "十年生死两茫茫,不思量,自难忘----苏轼,hello "+getLoginUser();
    }

    @RequestMapping("/admin/home")
    public String home(){
        return "天生我材必有用,千金散尽还复来----李白,hello admin :"+getLoginUser();
    }

    @RequestMapping("/user/home")
    public String userHome(){
        return "滚滚长江东逝水----苏轼,hello user: "+getLoginUser();
    }

    //获取登录用户
    private String getLoginUser(){
        return SecurityContextHolder.getContext().getAuthentication().getName();
    }
}

这三个测试接口,我们的规划是这样的:

/sayHello 是任何人只要登录了都可以访问的接口
/admin/home 是具有 admin 身份的人才能访问的接口
/user/home 是具有 user 身份的人才能访问的接口

这样,因为我们数据库脚本中默认的harry 具有admin和user的权限,因此,harry是可以访问所有接口的,而mike 只有user的权限,因此,mike 只能访问/user/home 和/sayHello

接下来我们来见证奇迹

启动测试

启动项目,因为我们配置的是前后端分离以JSON串方式数据交互的,我这里演示是用postman,访问接口:http://127.0.0.1:8080/login 进行登录,先登录harry

springSecurity权限精确到按钮_mybatis_04


可以看到,登录成功后,返回来的用户harry,具有两个权限admin 和user

我们来看看能访问哪些接口

springSecurity权限精确到按钮_spring boot_05


springSecurity权限精确到按钮_spring boot_06


springSecurity权限精确到按钮_mybatis_07

经过测试发现,harry已经具备了访问这三个接口的权限了

接下里在看看mike,登录mike

springSecurity权限精确到按钮_mybatis_08

mike登录成功,开始访问接口

springSecurity权限精确到按钮_ide_09


springSecurity权限精确到按钮_mybatis_10


springSecurity权限精确到按钮_spring boot_11


可以发现,mike 因为没有admin的权限,因此无法访问到接口/admin/home,其他两个都能正常访问

OK,关于权限和数据库结合的配置,就介绍到这里