SpringBoot2.3.4整合SpringSecurity实现权限管理

导读:本篇文章讲解 SpringBoot2.3.4整合SpringSecurity实现权限管理,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

概述

Spring是非常流行的Java应用开发框架,Spring Security是基于Spring框架,提供了一套Web应用安全性的完整解决方案。主要有两个方面,用户认证(Authentication)用户授权(Authorization),用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户授权指的是验证某个用户是否有权限执行某个操作。
SpringSecurity的主要特点如下:

  • 和Spring无缝整合
  • 全面的权限控制
  • 专门为Web开发而设计
  • 重量级框架

SpringSecurity更多知识请查阅SpringSecurity官网

Shiro是另一个非常流行的权限控制框架,它是Apache旗下的产品,相对于Spring Security,Shiro有以下特点:

  • 轻量级,针对对性能有更高要求的互联网应用有更好的表现
  • 通用性,不局限于Web环境,可以脱离Web环境使用,缺陷就是在Web环境下一些特定的需求需要手动编写代码定制

Shiro更多知识请查阅Shiro官网

对于如何选择权限框架,一般来说,常见的技术栈组合如下:

  • SSM + Shrio
  • Spring Boot/Spring Cloud + Spring Security

若想了解SpringBoot整合SpringSecurity+jwt的步骤,请参阅SpringBoot整合SpringSecurity+Jwt实现权限管理
若想了解SpringBoot整合Shiro的步骤,请参阅SpringBoot2.3.4整合Apache Shiro实现权限管理

相关概念

principal(主体):使用系统的用户或设备或从其他系统远程登录的用户
authentication(认证):权限管理系统确认一个主体的身份,允许主体进入系统,简单的说就”主体”证明自己是谁,也就是我们常说的登录操作
authorization(授权):将系统的”权力”授予给”主体”,这样主体就拥有了操作系统的某些特定功能的能力
UserDetailsService:Spring Security提供的登录逻辑接口,当什么也没有配置的时候,账号和密码是由Spring Security自定义生成的,在实际开发中我们都是需要从数据库中查询出来,这样就需要实现该接口,自定义登录逻辑
UserDetails:上面登录接口返回的用户”主体”,是系统默认的用户,常用的方法有以下几个:

//获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//判断账户是否过期
boolean isAccountNonExpired();
//判断账户是否被锁定
boolean isAccountNonLocked();
//判断密码是否过期
boolean isCredentialsNonExpired();
//判断当前用户是否可用
boolean isEnabled();

在使用的时候可以直接调用其实现类User就可以了,只需要传入用户名、密码和权限即可

public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
    this(username, password, true, true, true, true, authorities);
}

PasswordEncoder:密码加密,Spring Security提供的密码加密类,默认的密码解析器是BCryptPasswordEncoder,需要在配置文件中实例化,BCryptPasswordEncoder是对bcrypt强散列方法的具体实现,是基于Hash算法实现的单向加密,可以通过strength控制加密强度,默认10

新建表和初始化数据

需要使用的表有:
sys_user:用户表
sys_role:角色表
sys_menu:菜单权限表
sys_user_role:用户角色关联表
sys_role_menu:角色菜单关联表
persistent_logins:记住我登录表(Spring Security可以自动生成改表,用于记住我时保存用户信息)
建表语句和初始化数据如下:

CREATE TABLE `sys_user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_name` varchar(50) NOT NULL COMMENT '用户名',
  `real_name` varchar(20) NOT NULL COMMENT '真实名称',
  `password` varchar(64) NOT NULL COMMENT '密码',
  `sex` tinyint(2) NOT NULL DEFAULT '10' COMMENT '性别10:男;11:女;12:其他',
  `avatar` varchar(100) DEFAULT NULL COMMENT '头像路径',
  `status` tinyint(2) NOT NULL DEFAULT '10' COMMENT '状态10:正常;11:锁定;12:注销',
  `del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识0:未删除;1:已删除',
  `create_by` varchar(20) DEFAULT NULL COMMENT '创建者',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(20) DEFAULT NULL COMMENT '更新者',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='用户表';
INSERT INTO sys_user (user_name,real_name,password,sex,avatar,status,del_flag,create_by,create_time,update_by,update_time,remark) VALUES
('admin','超级管理员','$2a$10$j3XmUHGzMFLZFH.Qioq4Z.nM/iFMd4Wk6GDI.mC7U2yIztdyV6oUe',10,'',10,0,'admin','2020-09-30 08:34:18.0','admin','2020-09-30 08:34:18.0','')
CREATE TABLE `sys_role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `role_code` varchar(20) NOT NULL COMMENT '角色编号',
  `role_name` varchar(30) NOT NULL COMMENT '角色名称',
  `status` tinyint(2) unsigned NOT NULL DEFAULT '10' COMMENT '角色状态10:正常;11:停用',
  `del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识0:未删除;1:已删除',
  `create_by` varchar(20) DEFAULT NULL COMMENT '创建者',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(20) DEFAULT NULL COMMENT '更新者',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='角色表';
INSERT INTO sys_role (role_code,role_name,status,del_flag,create_by,create_time,update_by,update_time,remark) VALUES
('admin','超级管理员',10,0,'admin',NULL,'admin',NULL,NULL);
CREATE TABLE `sys_menu` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) unsigned DEFAULT '0' COMMENT '父级菜单ID',
  `order_num` tinyint(2) unsigned DEFAULT '0' COMMENT '显示顺序',
  `url` varchar(100) NOT NULL DEFAULT '#' COMMENT '请求地址',
  `menu_type` tinyint(2) unsigned DEFAULT NULL COMMENT '菜单类型10:目录;20:菜单;30:按钮',
  `visible` tinyint(2) unsigned NOT NULL DEFAULT '10' COMMENT '菜单状态10:显示;20:隐藏',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) NOT NULL DEFAULT '#' COMMENT '菜单图标',
  `create_by` varchar(20) DEFAULT NULL COMMENT '创建者',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(20) DEFAULT NULL COMMENT '更新者',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8 COMMENT='菜单表';
INSERT INTO sys_menu (menu_name,parent_id,order_num,url,menu_type,visible,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES
('系统管理',0,1,'#',10,10,'','#','admin',NULL,'admin',NULL,NULL)
,('用户管理',1,1,'/system/user',20,10,'user:view','#','admin',NULL,'admin',NULL,NULL)
,('用户查询',10,1,'#',30,10,'user:list','#','admin',NULL,'admin',NULL,NULL)
,('用户新增',10,2,'#',30,10,'user:add','#','admin',NULL,'admin',NULL,NULL)
,('用户修改',10,3,'#',30,10,'user:update','#','admin',NULL,'admin',NULL,NULL)
,('用户删除',10,4,'#',30,10,'user:delete','#','admin',NULL,'admin',NULL,NULL);
CREATE TABLE `sys_user_role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
INSERT INTO sys_user_role (user_id,role_id) VALUES (1,1);
CREATE TABLE `sys_role_menu` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `role_id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) unsigned NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='角色菜单表';
INSERT INTO sys_role_menu (role_id,menu_id) VALUES
(1,1),(1,10),(1,101),(1,102),(1,103),(1,104);
CREATE TABLE `persistent_logins`  (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE = InnoDB CHARACTER SET = utf8;

引入项目依赖

整合SpringSecurity核心的依赖如下:

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

配置文件

application.yml配置文件如下:

server:
  port: 8090
spring:
  application:
    name: springboot-security
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.108.11:3306/springboot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: root
    hikari:
      minimum-idle: 10 #最小空闲连接,默认10
      maximum-pool-size: 20 #最大连接数
      idle-timeout: 600000 #空闲连接超时时间,默认600000(10分钟)
      max-lifetime: 540000 #连接最大存活时间,默认30分钟
      connection-timeout: 60000 #连接超时时间,默认30秒
      connection-test-query: SELECT 1 #测试连接是否可用查询语句
    type: com.zaxxer.hikari.HikariDataSource
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  thymeleaf:
    mode: HTML
    encoding: UTF-8
    cache: false
  resources:
    static-locations: classpath:/templates/, classpath:/public/, classpath:/static/
mybatis-plus:
  mapper-locations: classpath:mapper/**/*Mapper.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-aliases-package: com.xlhj.security.entity
logging:
  config: classpath:logback-spring.xml
  level:
    com.xlhj.security: debug

Spring Security关键配置类SecurityConfig代码如下:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private DataSource dataSource;
    @Resource
    private SecurityAuthenticationFailureHandler authenticationFailureHandler;
    @Resource
    private AuthenticationEntryPointHandler authenticationEntryPoint;
    @Resource
    private SecurityAccessDeniedHandler accessDeniedHandler;
    /**
     * 配置密码解析
     * @return
     */
    @Bean
    protected PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 配置用户名和密码
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf防护
        http.csrf().disable()
                //表单登录
                .formLogin()
                //登录页面
                .loginPage("/login.html")
                //登录访问路径,与页面表单提交路径一致
                .loginProcessingUrl("/login")
                //登录成功后访问路径
                .defaultSuccessUrl("/index.html").permitAll()
                .failureHandler(authenticationFailureHandler)
                .and()
                //认证配置
                .authorizeRequests()
                .antMatchers("/login.html", "/login").permitAll()
                //配置静态页面可以访问
                .antMatchers("/js/**", "/css/**", "/images/**", "/favicon.ico").permitAll()
                //任何请求
                .anyRequest()
                //都需要身份验证
                .authenticated();
        //配置无权限访问页面
        //http.exceptionHandling().accessDeniedPage("/uanuth.html");
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
        //配置记住我
        http.rememberMe()
                //持久层对象
                .tokenRepository(persistentTokenRepository())
                //失效时间()
                .tokenValiditySeconds(60)
                //配置自定义登录逻辑
                .userDetailsService(userDetailsService);
        //配置退出
        http.logout()
                //退出路径
                .logoutUrl("/logout")
                //退出后跳转页面
                .logoutSuccessUrl("/login.html");
    }
    /**
     * 配置记住我
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //jdbcTokenRepository.setCreateTableOnStartup(true);//请求时创建表
        return jdbcTokenRepository;
    }
}

自定义登录实现类

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserMapper userMapper;
    @Autowired
    private SysMenuMapper menuMapper;
    @Autowired
    private SysRoleMapper roleMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
        wrapper.eq("user_name", username);
        SysUser currentUser = userMapper.selectOne(wrapper);
        if (currentUser == null) {
            throw new UsernameNotFoundException("用户不存在!");
        }
        System.out.println(currentUser);
        //获取用户角色和菜单权限
        List<GrantedAuthority> authorityList = new ArrayList<>();
        List<SysRole> roleList = roleMapper.selectRoleCodesByUserId(currentUser.getId());
        for (SysRole role : roleList) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role.getRoleCode());
            authorityList.add(authority);
        }
        List<SysMenu> permsList = menuMapper.selectMenuPermsByUserId(currentUser.getId());
        for (SysMenu perm : permsList) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(perm.getPerms());
            authorityList.add(authority);
        }
        //List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authStr);
        return new User(currentUser.getUserName(), currentUser.getPassword(), authorityList);
    }
}

控制器代码

@RestController
@RequestMapping("/user")
public class SysUserController {
    @GetMapping("/list")
    @PreAuthorize("hasAuthority('user:list')")
    public ResultData userList() {
        return ResultData.ok(ResultCode.SUCCESS, "访问用户查询界面成功!");
    }
    @GetMapping("/add")
    @PreAuthorize("hasAuthority('user:add')")
    public ResultData userAdd() {
        return ResultData.ok(ResultCode.SUCCESS, "访问用户新增界面成功!");
    }
    /**
     * 测试无权限访问,数据库中权限是user:update
     * @return
     */
    @GetMapping("/update")
    @PreAuthorize("hasAuthority('user:edit')")
    public ResultData userUpdate() {
        return ResultData.ok(ResultCode.SUCCESS, "访问用户修改界面成功!");
    }
    @GetMapping("/delete")
    @Secured("ROLE_admin")
    public ResultData userDelete() {
        return ResultData.ok(ResultCode.SUCCESS, "访问用户删除界面成功!");
    }
}

页面登录代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/login" method="post" >
        用户名:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"/><br/>
        <input type="checkbox" name="remember-me"/>自动登录<br/>
        <input type="submit" value="登录"/>
    </form>
</body>
</html>

注意:页面提交方式必须是post,用户名必须是username,密码必须是password。因为在执行登录逻辑时会调一个过滤器UsernamePasswordAuthenticationFilter,其部分源码如下:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;
    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
}

如果需要输入其他字段,必须在配置文件中指定

http.formLogin().usernameParameter("").passwordParameter("")

测试

启动项目,在浏览器输入http://localhost:8090/login.html
登录
输入用户名和密码admin/123456,密码需要提前使用BCryptPasswordEncoder的encode方法生成一个
进入首页
首页
点击有权限的连接会返回如{“success”:true,“code”:20000,“message”:“访问用户查询界面成功!”,“data”:{}}的信息,点击无权限的连接会返回如{“code”:20003,“data”:{},“message”:“没有权限访问!”,“success”:false}信息

权限控制

SpringSecurity提供基于角色或权限进行访问控制,常见的有以下几种方法
hasAuthority
如果当前的主体具有指定权限,则返回true,否则返回false
可以在配置类中配置,也可以在controller的方法中使用注解@PreAuthorize(“hasAuthority(‘user:add’)”)
hasAnyAuthority
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
hasRole
如果用户具备给定角色就允许访问,否则出现403。
如果当前主体具有指定的角色,则返回true。
注意:在给用户添加角色时,要在角色前面加”ROLE_”,在配置类或注解时不需要添加
hasAnyRole
表示用户具备任何一个条件都可以访问

注解使用

@Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“,使用注解先要开启注解功能@EnableGlobalMethodSecurity(securedEnabled = true)
例如:@Secured({“ROLE_normal”,“ROLE_管理员”})
@PreAuthorize
同样在使用时需要开启注解功能@EnableGlobalMethodSecurity(prePostEnabled = true)
适合进入方法前的权限验证,可以将登录用户的roles/permissions参数传到方法中,例如:@PreAuthorize(“hasAnyAuthority(‘menu:system’)”)
@PostAuthorize
使用前需要开启@EnableGlobalMethodSecurity(prePostEnabled = true)
在方法执行后再进行权限验证,适合验证带有返回值的权限
例如:@PostAuthorize(“hasAnyAuthority(‘menu:system’)”)
@PostFilter
权限验证之后对数据进行过滤,表达式中的 filterObject 引用的是方法返回值List中的某一个元素
例如:@PostFilter(“filterObject.username == ‘admin1’”),表示留下用户名是admin1的数据
@PreFilter
进入控制器之前对数据进行过滤
例如:@PreFilter(value = “filterObject.id%2==0”)

完整代码详见码云地址

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76834.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!