Spring Boot(十一):Spring Security 实现权限控制

Spring Boot(十一):Spring Security 实现权限控制

大家好,我是 杰哥

我们知道,Web 应用的安全一般关注用户认证(authentication)以及用户授权(authorization)两个部分,即确定你是谁以及你能干什么

Spring Security 是基于 Spring 框架,用于解决 Web 应用安全性的一种方案,它是一款优秀的权限管理框架

虽然相对于 shiro (是 Apache 的一个轻量级权限控制框架,不与任何框架捆绑)来说,它比较重(依赖的东西较多),但是 Spring Boot 提供了自动化配置方案,通过较少的配置就可以使用 Spring Security 来进行权限控制。因此,若项目使用的是 Spring Boot 框架,使用 Spring Security 将会是一个更优的选择

Spring Boot 集成 Security 实战

Spring Security 采用的是责任链的设计模式,它有一条很长的过滤器链来实现各个环节的控制逻辑。要使用它,只需要自定义类继承自 WebSecurityConfigurerAdapter 来配置。我们主要配置三个地方

  • 配置可登录用户,包括用户名、密码和权限/角色

configure(AuthenticationManagerBuilder auth)

  • 配置权限关系(url、authority的所有对应关系)

configure(HttpSecurity http)

  • 配置放行规则

configure(WebSecurity web)

具体怎样使用呢?我们一起来看看具体步骤

一 公共配置

1 pom 依赖

首先引入 security 的 pom 依赖,直接引入 spring-boot-starter-security 依赖



    org.springframework.boot
    spring-boot-starter-security

 
    org.springframework.boot
    spring-boot-starter-web
 


    org.projectlombok
    lombok
    true

其他两个依赖 spring-boot-starter-weblombok 则分别用于构建 Web 应用、采用 lombok 的依赖包进行快捷开发

2 定义 User 实体类

@Data
public class User  implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    // 用户所有权限
    private List authorities = new ArrayList();
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

由于后续的配置需要使用到用户的用户名密码以及所拥有的权限这三个信息,因此需要为原 User 实体类,添加一个 authorities 属性,表示用户所拥有的的权限列表

3 定义测试 API

@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/common")
    public String common() {
        return "hello~ common";
    }
    @GetMapping("/admin")
    public String admin() {
        return "hello~ admin";
    }
}

分别定义 /user/common/user/admin 两个 API,作为用户的权限,用于测试不同用户的权限

二 核心权限代码配置

好了,做好了准备工作之后,接下来就进入到我们的重点部分。为了让初学者有一个理解过程,我们先通过手动配置的方式,让大家对于 Security 的使用方式有个大致的概念

(一) 方式一:手动配置

1 权限配置

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    /**
     * 将用户配置在内存中(可登录用户,包括用户名、密码和权限)
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 方式一:写到内存
        auth.inMemoryAuthentication()
               .withUser("admin").password(passwordEncoder().encode("admin")).authorities("admin")
               .and()
                .withUser("user").password(passwordEncoder().encode("123456")).authorities("common");
    }
    /**
     * 登录处理-配置对应用户所拥有的权限(url、authority对应的所有关系)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        方式一:写死的方式
                http.authorizeRequests().
        antMatchers("/user/common").hasAnyAuthority("admin")
                .antMatchers("/user/admin").hasAnyAuthority("admin")
                .antMatchers("/user/common").hasAnyAuthority("common")
                .antMatchers("/**").fullyAuthenticated()
                .anyRequest().authenticated()
                .and().formLogin();
    }
    /**
     * 放行规则(忽略拦截配置)
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 设置拦截忽略url - 会直接过滤该url - 将不会经过Spring Security过滤器链
        web.ignoring().antMatchers("/hello");
        // 设置拦截忽略文件夹,可以对静态资源放行
        web.ignoring().antMatchers("/css/**", "/js/**");
    }
    
     /**
     *  密码加密类
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

也就是说,我们将 Web 应用中的所有用户:adminuser 以及对应的权限,通过实现 configure(AuthenticationManagerBuilder auth) 方法,直接手动写入内存中,这里需要指定用户的用户名、密码以及用户所拥有的权限

 @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 方式一:写到内存
        auth.inMemoryAuthentication()
               .withUser("admin").password(passwordEncoder().encode("admin")).authorities("admin")
               .and()
                .withUser("user").password(passwordEncoder().encode("123456")).authorities("common");
    }
通过实现 configure(HttpSecurity http) 方法,来配置不同资源与权限的对应关系。这里表示 资源 /user/common 能够分别被具有 admin 和 common 权限的用户访问,而 /user/admin 则 只能够被具有 admin 权限的用户访问
@Override
    protected void configure(HttpSecurity http) throws Exception {
//        方式一:写死的方式
                http.authorizeRequests().
        antMatchers("/user/common").hasAnyAuthority("admin")
                .antMatchers("/user/admin").hasAnyAuthority("admin")
                .antMatchers("/user/common").hasAnyAuthority("common")
                .antMatchers("/**").fullyAuthenticated()
                .anyRequest().authenticated()
                .and().formLogin();
    }

通过实现 configure(WebSecurity web) 方法,来配置 Web 应用的放行规则。这里表示资源 /hello/css/、/js/ 资源是不用做权限校验的,也就是说任何用户都有访问他们的权限

@Override
    public void configure(WebSecurity web) throws Exception {
        // 设置拦截忽略url - 会直接过滤该url - 将不会经过Spring Security过滤器链
        web.ignoring().antMatchers("/hello");
        // 设置拦截忽略文件夹,可以对静态资源放行
        web.ignoring().antMatchers("/css/**", "/js/**");
    }

2)验证 启动项目,进行访问验证

输入 /user/common,便自动跳转到了 login 登录页面。使用 user 进行登录

Spring Boot(十一):Spring Security 实现权限控制

用户名密码输入正确之后,点击登录,则可以自动重定向至 /user/common 接口

Spring Boot(十一):Spring Security 实现权限控制

我们修改浏览器的访问地址为 localhost:8082/user/admin,点击回车

Spring Boot(十一):Spring Security 实现权限控制

由于当前登录用户 user ,并没有 admin 权限,因此被禁止访问 /user/admin 接口,返回码为 403

那么,也就是说,user 用户可以访问 /user/common 接口,但并没有权限访问 /user/adimin 接口

同样地,清掉 cookie 缓存之后,再试试采用 admin 用户重新登录,分别访问 /user/common/user/admin 接口,均可以正常请求

这两个用户的访问效果,均符合我们的预期,说明我们的权限功能已经成功实现啦

(二) 数据库方式

Spring Boot Security 提供了基于用户的权限控制以及基于角色来进行权限控制这两种方式,即通过分别配置用户对应的权限和资源对应的权限(hasAuthority)、分别配置用户对应的角色和资源对应的角色(hasRole)这两种方式,这里使用的是直接指定用户的权限的方式,即 hasAuthority 方法,不过两者其实都是采用 RBAC -基于角色的权限访问控制(Role-Based Access Control) 进行权限管理的。即:

Spring Boot(十一):Spring Security 实现权限控制

所以,若需要采用数据库的方式进行权限逻辑控制,我们需要创建 5 张表:user 表、role 表、permission 表、user_role 表以及 role_permission

1)建表语句

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `pid` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of permission
-- ----------------------------
BEGIN;
INSERT INTO `permission` VALUES (1, '/user/common', 'common', NULL, 0);
INSERT INTO `permission` VALUES (2, '/user/admin', 'admin', NULL, 0);
COMMIT;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
BEGIN;
INSERT INTO `role` VALUES (1, 'USER');
INSERT INTO `role` VALUES (2, 'ADMIN');
COMMIT;
-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(11) NOT NULL,
  `permission_id` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role_permission
-- ----------------------------
BEGIN;
INSERT INTO `role_permission` VALUES (1, 1, 1);
INSERT INTO `role_permission` VALUES (2, 2, 1);
INSERT INTO `role_permission` VALUES (3, 2, 2);
COMMIT;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
BEGIN;
INSERT INTO `user` VALUES (1, 'user', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
INSERT INTO `user` VALUES (2, 'admin', '$2a$10$4zd/aj2BNJhuM5PIs5BupO8tiN2yikzP7JMzNaq1fXhcXUefWCOF2');
COMMIT;
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) NOT NULL,
  `role_id` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_role
-- ----------------------------
BEGIN;
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (3, 2, 2);
COMMIT;

其中,user 表中的两个用户的密码,均为 123456,是提前采用代码new BCryptPasswordEncoder().encode("123456")加密之后所得的结果

2)引入依赖

再引入数据库连接相关依赖



    mysql
    mysql-connector-java



    org.springframework.boot
    spring-boot-starter-jdbc
    
        
            com.zaxxer
            HikariCP
        
    


    com.alibaba
    druid-spring-boot-starter
    1.2.9

3)数据库配置

在配置文件 application.yml 中,添加数据库配置

server:
  port: 8082
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security_demo?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
    username: root
    password: 123456

4)定义 UserMapper 接口


public interface UserMapper  {
    /**
     * 根据用户名称查询
     *
     * @param userName
     * @return
     */
    @Select(" select * from user where username = #{userName}")
    User findByUsername(@Param("userName") String userName);
    /**
     * 查询用户的权限根据用户查询权限
     *
     * @param userName
     * @return
     */
    @Select(" SELECT d.*
" +
            "from user a,user_role b,role_permission c,permission d
" +
            "WHERE 
" +
            "a.id = b.user_id
" +
            "and b.role_id = c.role_id
" +
            "and c.permission_id = d.id
" +
            "and 
" +
            "a.username= #{userName};")
    List findPermissionByUsername(@Param("userName") String userName);
}

5)定义 UserService 接口

该接口继承自接口 UserDetaileService 接口

public interface UserService  extends UserDetailsService {
}

6) 接口实现类

@Slf4j
@Service
public class UserServiceImpl implements UserService  {
    @Resource
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.根据用户名称查询到user用户 
        User userDetails = userMapper.findByUsername(username);
        if (userDetails == null) {
            return null;
        }
        // 2.查询该用户对应的权限 
        List permissionList = userMapper.findPermissionByUsername(username);
        List grantedAuthorities = new ArrayList<>();
        permissionList.forEach((a) -> grantedAuthorities.add(new SimpleGrantedAuthority(a.getName())));
        log.info(">>permissionList:{}<<", permissionList);
        // 设置权限 
        userDetails.setAuthorities(grantedAuthorities);
        return userDetails;
    }
}

该实现类,主要实现 UserDetailsService 接口的 loadUserByUsername(String username) 方法,根据用户名,从数据库中获取到用户的信息,包括用户名、密码以及权限列表,用户后续的配置

7) 配置类

接下来,则进入具体代码逻辑

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserService userService;
    @Resource
    private PermissionMapper permissionMapper;
    /**
     * 将用户配置在内存或者mysql中(可登录用户,包括用户名、密码和角色)
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 方式二:来源于数据库
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }
    /**
     * 登录处理-配置对应用户所拥有的权限(url、authority对应的所有关系)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        方式二:配置来源于数据库
        ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry
               authorizeRequests = http.csrf().disable().authorizeRequests();
        // 1.查询到所有的权限
        List allPermission = permissionMapper.findAllPermission();
        // 2.分别添加权限规则
        allPermission.forEach((p -> {
            authorizeRequests.antMatchers(p.getUrl()).hasAnyAuthority(p.getName()) ;
         }));
        authorizeRequests.antMatchers("/**").fullyAuthenticated()
                .anyRequest().authenticated().and().formLogin();
    }
    /**
     * 忽略拦截处理
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 设置拦截忽略url - 会直接过滤该url - 将不会经过Spring Security过滤器链
        web.ignoring().antMatchers("/getUserInfo");
        // 设置拦截忽略文件夹,可以对静态资源放行
        web.ignoring().antMatchers("/css/**", "/js/**");
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

其实只需要将 configure(AuthenticationManagerBuilder auth)configure(HttpSecurity http) 中写死的部分改写为从数据库中获取的方式即可

完成之后,只需要重新启动项目,再次进行验证,发现效果与方式一的一样

项目中,一般也就是采用数据库配置的方式进行权限配置,当然根据不同的需要,只需要进行不同的数据库更换即可,底层逻辑并未发生变化。那么,这样我们就轻松实现了采用 Spring Boot 集成 Security 进行权限控制的效果

但是,你可能还没有发现,到了 Spring Boot 2.7 以后,WebSecurityConfigurerAdapter 被标记为了过期

Spring Boot(十一):Spring Security 实现权限控制

Spring Boot(十一):Spring Security 实现权限控制

这么重要的一个类,突然被标记为过期,那么,我们的权限配置逻辑要如何实现呢?

别慌,车到山前必有路,既然这个类不建议再使用了,那么Spring Security 的开发大佬们一定为我们提供了一个更为方便快捷的方式 我们一起来看看~

(三) Spring Security 2.7 以后的配置方式

根据 WebSecurityConfigurerAdapter 类的注释,我们可以清楚地看到:如果想要配置过滤器链,可以通过自定义 SecurityFilterChain Bean 来实现

而如果想要配置 WebSecurity,可以通过 WebSecurityCustomizer Bean 来实现

我们再来采用最新的方式,实现一下上述例子中的效果:

1 定义一个配置类

@Configuration
@Slf4j
public class SecurityConfig {
    @Resource
    private PermissionMapper permissionMapper;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry
                authorizeRequests = http.csrf().disable().authorizeRequests();
//        方式二:配置来源于数据库
//         1.查询到所有的权限
        List allPermission = permissionMapper.findAllPermission();
//         2.分别添加权限规则
        allPermission.forEach((p -> {
            authorizeRequests.antMatchers(p.getUrl()).hasAnyAuthority(p.getName()) ;
        }));
        authorizeRequests.antMatchers("/**").fullyAuthenticated()
                .anyRequest().authenticated().and().formLogin();
        return http.build();
    }
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return new WebSecurityCustomizer() {
            @Override
            public void customize(WebSecurity web) {
                web.ignoring().antMatchers("/hello");
                web.ignoring().antMatchers("/css/**", "/js/**");
            }
        };
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

也就是说,最新的方案:

  • 只需要自定义 SecurityFilterChain(HttpSecurity http) Bean,将原先 configure(HttpSecurity http) 方法中的逻辑,转移到 SecurityFilterChain(HttpSecurity http) Bean 中实现即可
  • 放行规则,则可以将原先 configure(WebSecurity web) 方法中的逻辑转移到 WebSecurityCustomizer Bean 中进行实现即可
  • 而对于当前系统的用户信息,包括用户名、密码以及所拥有的权限列表这些信息,只需要实现 UserDetailsService 接口,并通过重写 loadUserByUsername() 方法进行获取即可(采用最新方案,这里不需要做任何修改)

总结

权限管理是每个 Web 应用都需要考虑的,而 SpringBootSecurity 提供了自动化配置方案,基于 RBAC 权限模型,通过较少的配置就可以实现较为复杂的权限管理逻辑

本文章,介绍了 Spring Security,并通过实例演示,让大家很容易理解 Security 的用法。需要注意的是,Spring Boot 2.7 以后,配置类WebSecurityConfigurerAdapter 已经被标记为过期,需要参考文章中的最新的方案,进行配置使用

文章演示代码地址:

https://github.com/helemile/Spring-Boot-Notes/tree/master/springSecurity


嗯,就这样。每天学习一点,时间会见证你的强大~


欢迎大家关注我们的公众号,一起持续性学习吧~

Spring Boot(十一):Spring Security 实现权限控制



往期精彩回顾


总结复盘

架构设计读书笔记与感悟总结

带领新人团队的沉淀总结

复盘篇:问题解决经验总结复盘

网络篇

网络篇(四):《图解 TCP/IP》读书笔记

网络篇(一):《趣谈网络协议》读书笔记(一)

事务篇章

事务篇(四):Spring事务并发问题解决

事务篇(三):分享一个隐性事务失效场景

事务篇(一):毕业三年,你真的学会事务了吗?

Docker篇章

Docker篇(六):Docker Compose如何管理多个容器?

Docker篇(二):Docker实战,命令解析

Docker篇(一):为什么要用Docker?

..........

SpringCloud篇章

Spring Cloud(十三):Feign居然这么强大?

Spring Cloud(十):消息中心篇-Kafka经典面试题,你都会吗?

Spring Cloud(九):注册中心选型篇-四种注册中心特点超全总结

Spring Cloud(四):公司内部,关于Eureka和zookeeper的一场辩论赛

..........

Spring Boot篇章

Spring Boot(七):你不能不知道的Mybatis缓存机制!

Spring Boot(六):那些好用的数据库连接池们

Spring Boot(四):让人又爱又恨的JPA

SpringBoot(一):特性概览

..........

翻译

[译]用 Mint 这门强大的语言来创建一个 Web 应用

【译】基于 50 万个浏览器指纹的新发现

使用 CSS 提升页面渲染速度

WebTransport 会在不久的将来取代 WebRTC 吗?

.........

职业、生活感悟

你有没有想过,旅行的意义是什么?

程序员的职业规划

灵魂拷问:人生最重要的是什么?

如何高效学习一个新技术?

如何让自己更坦然地度过一天?

..........

Spring Boot(十一):Spring Security 实现权限控制

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章