IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了 -> 正文阅读

[Java知识库]循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了

回顾

前面我们介绍了如何通过配置的方式自定义页面,如何配置用户,如何从数据库读取用户并配置到spring security上进行登录登录验证,如果你不熟悉,请回去看我的前几篇文章,这样有助于您对本文的理解

  1. 面试不要在说不熟悉spring security了,一个demo让你使劲忽悠面试官
  2. 循序渐进学习spring security 第二篇,如何修改默认用户?
  3. 循序渐进学习spring security 第三篇,如何自定义登录页面?登录回调?
  4. 循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里?
  5. 循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串?
  6. 循序渐进学spring security第六篇,手把手教你如何从数据库读取用户进行登录验证,mybatis集成

好了,今天我们就通过一个简单的案例来看看 Spring Security 中的授权操作。

源码下载

授权

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

创建项目

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

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

创建项目:security-mybatis-roles

引入依赖:
在这里插入图片描述
具体依赖如下:

 <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会自动在权限前加个前缀,所以我们从数据库读取权限出来时,因为我们存储的时候没有这前缀,因此要得加上

我们也可以跟踪配置到源码看看,确实也是会自动加上的前缀
在这里插入图片描述

创建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:
在这里插入图片描述

这从语义上很好理解,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
在这里插入图片描述
可以看到,登录成功后,返回来的用户harry,具有两个权限admin 和user
我们来看看能访问哪些接口
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

接下里在看看mike,登录mike
在这里插入图片描述

mike登录成功,开始访问接口
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以发现,mike 因为没有admin的权限,因此无法访问到接口/admin/home,其他两个都能正常访问

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

源码下载

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-07-29 14:47:04  更:2021-07-29 14:47:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/28 3:46:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码