草庐IT

SpringBoot整合SpringSecurity

轻轻敲醒沉睡的心灵 2023-03-28 原文

先说一下SpringSecurity是干什么的,SpringSecurity主要作用有2方面:认证、授权。

  • 认证:Authentication, 用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
  • 授权: Authorize,授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问

权限管理涉及到几个概念:

  • 主体(用户id、账号、密码、...)
  • 资源(资源id、资源名称、访问地址、...)
  • 权限(权限id、权限标识、权限名称、资源id、...)
  • 角色(角色id、角色名称、...)

业界通常基于RBAC实现授权。
在单体应用中,我觉得理解为基于角色的访问控制(Role-Based Access Control)是比较合适的,用起来比较方便。
而在当前动辄微服务开发的环境下,个人觉得理解为基于资源的访问控制(Resource-Based Access Control)用起来更方便,因为微服务中各个微服务都当做资源来看待了。
整合
SpringBoot整合SpringSecurity还是比较简单的:

  1. 引入相关jar包
  2. 配置Security(配置时会稍麻烦,因为需要理解的比较多)

1. 引入Jar包

比较简单,引入web包和security包就行

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

2. 启动测试

引入jar包后就可以启动了,启动时会生成随机密码:


随机密码
  • 访问项目就会跳转到登陆页面,默认账号:user

    登陆

  • SpringSecurity自带注销地址:/logout,访问这个地址会弹出注销页面。

    注销

3. 自定义配置

以上是SpringSecurity自带的认证功能,我们使用时需要根据我们自己的需要自定义一些内容(2方面配置:认证配置,授权配置),例如:

  • 登陆的账号密码
  • 是否允许表单登陆
  • 密码加密的情况
  • 权限鉴定
  • ......

3.1 认证配置

自定义的配置其实都在同一个类中,认证和授权在不同的方法中,配置类继承WebSecurityConfigurerAdapter类,重写2个方法就行。
注意,就是这个父类,想要配置什么,点进源码去里面找对应的方法
认证的方式有2种:一是账号密码等认证信息写在配置中,二是账号密码等信息从数据库读取。使用时,取其一

3.1.1 认证信息写在配置中
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 1. 认证配置
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 测试用的,写死的账号密码
        auth.inMemoryAuthentication()
            .withUser("admin")
            .password(passwordEncoder().encode("123456"))
            .roles("ADMIN")
            .authorities("/test/t1")
            .and()
            .withUser("user")
            .password(passwordEncoder().encode("123456"))
            .roles("USER")
            .authorities("/test/t2")
            ;
    }
    
   // 设置密码加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 2. 授权的配置方法,下面再讲,先空着
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception { 
        
    }   
}
3.1.2 认证信息从数据库拿

这种方式,配置类简单,但是需要一个用户服务类,来返回一个SpringSecurity封装的一个user对象,直接将服务类放到配置文件中就行。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 注入服务类
    @Autowired 
    private UserDetailsServiceImpl userDetailsServiceImpl;
    
    /**
     * 1. 认证配置
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceImpl);
    }
    
    // 设置密码加密方法,必须设置
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 2. 授权的配置方法,下面再讲,先空着
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception { 
        
    }
}

看一下这个服务类怎么写的,先准备一个服务类要返回的UserDetails对象

  • 自定义user实体对象
package com.example.demo.security.userdetails;

import java.util.Collection;
import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.Data;

/**
 * 参考{@link org.springframework.security.core.userdetails.User}这个类,
 * 这个类是security设置实体类参数值的时候用的,里面很多方法可以参考使用。
 * 比如设置roles和设置authorities的过程,在User类的内部类UserBuilder中
 */

@Data
public class MyUserDetail implements UserDetails {

    private static final long serialVersionUID = 1L;

    
    private Long userId;

    private String username;

    private String name;

    private String password;
    
    private boolean status;

    private Long deptId;

    private String email;

    private String mobile;

    private String sex;

    private String avatar;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    private Date lastLoginTime;
    
    // 角色权限:SpringSecurity中角色和权限都是放在这个里面的,使用起来是一样的,区别在于,角色要加前缀 ROLE_
    private Collection<GrantedAuthority> authorities;
    
    /**
     * 参考{@link org.springframework.security.core.userdetails.User.UserBuilder}
     * 中的roles方法
     * @param roles
     * @return
     */
    public List<GrantedAuthority> roles(String... roles) {
        List<GrantedAuthority> authorities = new ArrayList<>(roles.length);
        for (String role : roles) {
            Assert.isTrue(!role.startsWith("ROLE_"),
                    () -> role + " cannot start with ROLE_ (it is automatically added)");
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        }
        return authorities;
    }
    
    /**
     * 参考{@link org.springframework.security.core.userdetails.User.UserBuilder}
     * 中的 authorities 方法
     * @param authorities
     * @return
     */
    public List<GrantedAuthority> authorities(String authorities) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
    }
    
    public List<GrantedAuthority> authorities(String... authorities) {
        return AuthorityUtils.createAuthorityList(authorities);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        // 先按这个判断,需要什么自己添加
        return this.isStatus();
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.isStatus();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.isStatus();
    }

    @Override
    public boolean isEnabled() {
        return this.isStatus();
    }
}
  • 用户服务类
package com.example.demo.security.userdetails;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;

import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.example.demo.dao.MyUserDao;

public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private MyUserDao myUserDao;
    
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        
        MyUserDetail userDetail = myUserDao.getMyUserDetail(username);
        // 注意数据库中保存的密码,要是加密过的,就是在配置类中设置的加密方法
        if (!userDetail.isEnabled()) {
            throw new DisabledException("账号状态异常!");
        } else if (!userDetail.isCredentialsNonExpired()) {
            throw new LockedException("密码过期!");
        }
        
        // 模拟一点角色权限信息,角色前面要加 ROLE_ 前缀
        userDetail.setAuthorities(userDetail.authorities("/test/t1", "/test/t2", "ROLE_ADMIN", "ROLE_ROOT"));
        
        return userDetail;
    }

}

使用上面这2种方法之一,我们就可以用我们自己的账号和密码登陆了。

3.1.3 看看SpringSecurity自带的

上面是我们自己实现的接口,写了过程。其实,SpringSecurity自己也封装了很多,我们也可以看看。

实现类

官方包里面的就是用户的实现过程,其实我们可以用自带的这些,但是限制比较多,拿jdbc这个来说,他也重写了loadUsersByUsername()
JdbcUserDetailsManager

但是它限制了很多东西,表名、字段等要符合人家要求:你要有users表,表中要包含这些字段:
sql

如果你要想使用,初始化时传入DataSource即可,他会根据你传入的数据源自动查找数据。
构造方法

3.2 授权配置

注意:前提条件是,认证时,账号信息中加入了角色和权限的一些信息,这里才能进行权限判定。
授权配置常用的有2种方式,一是在SecurityConfig类中,一是用注解表达式。

3.2.1 在SecurityConfig类配置权限

授权配置还是在上面的SecurityConfig类中,只不过是在下面的那个方法中配置。

package com.example.demo.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.example.demo.entity.Result;
import com.example.demo.security.userdetails.UserDetailsServiceImpl;

import cn.hutool.json.JSONUtil;

/**
 * SpringSecurity配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    // 注入服务类
    @Autowired 
    private UserDetailsServiceImpl userDetailsServiceImpl;
    
    /**
     * 1. 认证配置
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceImpl);
    }
    
    // 设置密码加密方法
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    
    /**
     * 2. 授权的配置方法,下面再讲,先空着
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception { 
        // 1. 登陆登出设置
        http
        // 允许表单登陆
        .formLogin()
        // 自定义登陆页面,注意action提交地址 和 账号密码表单name
//        .loginPage("/login.html")
        // 自定义后端登陆地址,security默认的是/login
//        .loginProcessingUrl("/doLogin")
        // 自定义登陆成功后的处理,前后端分离一般返回json数据
        .successHandler(new MyAuthenticationSuccessHandler())
        // 自定义登陆失败后的处理,前后端分离一般返回json数据
        .failureHandler(new MyAuthenticationFailureHandler())
        .and()
        .logout()
        // 自定义退出地址
//        .logoutUrl("/logout")
        // 退出成功后的处理
        .logoutSuccessHandler((req,res,aut)->{
            res.setContentType("application/json;charset=utf-8");
            Result<String> result = new Result<>();
            result.setStatus(1);
            result.setCode("200");
            result.setMsg("退出成功");
            res.getWriter().write(JSONUtil.toJsonStr(result));
        })
        //使得session失效,默认true
//        .invalidateHttpSession(true)
        //清除认证信息,默认true
//        .clearAuthentication(true)
        //删除指定的cookie
//        .deleteCookies("cookie01")
        ;
       
        // 2. 跨域问题
        http.csrf().disable();
        
        // 3. 权限设置
        http
            // 对url进行访问权限控制
            .authorizeRequests() 
            // 按角色来控制权限的
            .antMatchers("/test/t2").hasRole("ADMIN")
            .antMatchers(
                    "/admin1/**",
                    "/admin2/**"
                    ).hasAnyRole("ADMIN1", "ADMIN2")
            // 按Authority,有权限才能访问
            .antMatchers("/user/**").hasAuthority("/u/a")
            .antMatchers("/test/t1").hasAuthority("/test/t1")
            // 直接放行的
            .antMatchers("/app/**").permitAll()
            // 其他任何请求都需要登陆
            .anyRequest().authenticated()
            ;
    }

}
3.2.2 注解表达式配置权限

SpringSecurity的权限注解有5个,都是用在方法上的,分别是:

  • @Secured:检查指定的角色权限,角色要加前缀ROLE_,可以多个,如:@Secured({"ROLE_A", "ROLE_B"})
  • @PreAuthorize:方法执行前进行权限检查,一般都是用这个。用法:@PreAuthorize("hasRole('admin')")@PreAuthorize("hasAuthority('/t1') and hasAuthority('/t2')")@PreAuthorize("hasAnyRole('root','admin')")
  • @PostAuthorize:方法执行后进行权限检查,还没用过
  • @PreFilter:过滤函数,未用过
  • @PostFilter:过滤函数,未用过

注意:

  • 要想使用注解,需要开启注解,在security的配置类加上@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    开启注解
  • 注释掉配置类方法中,关于权限的配置。

注解表达式使用如下:

package com.example.demo.controller;

import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    
    @GetMapping("/t1")
    public String test1(String name, HttpServletResponse response) {
        response.addHeader("userId","123");
        return name == null?"zhangsan":name;
    }
    
    @GetMapping("/t2")
    @PreAuthorize("hasAnyRole('ROOT','ADMIN')")
    public String test2() {
        return "test2";
    }
    
    
    @GetMapping("/t3")
    @PreAuthorize("hasAuthority('/test/t3')")
    public String test3() {
        return "test3";
    }
    
    @GetMapping("/t4")
    @PreAuthorize("hasAuthority('/t4') and hasAuthority('/t5')")
    public String test4() {
        return "test4";
    }
    
    @GetMapping("/t5")
    @PreAuthorize("hasRole('admin')")
    public String test5() {
        return "test5";
    }

}

注解的判断方法走的是类org.springframework.security.access.expression.SecurityExpressionRoot中的,可以看看逻辑。

有关SpringBoot整合SpringSecurity的更多相关文章

  1. springboot定时任务 - 2

    如果您希望在Spring中启用定时任务功能,则需要在主类上添加 @EnableScheduling 注解。这样Spring才会扫描 @Scheduled 注解并执行定时任务。在大多数情况下,只需要在主类上添加 @EnableScheduling 注解即可,不需要在Service层或其他类中再次添加。以下是一个示例,演示如何在SpringBoot中启用定时任务功能:@SpringBootApplication@EnableSchedulingpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.ru

  2. 基于SpringBoot的线上日志阅读器 - 2

    软件特点部署后能通过浏览器查看线上日志。支持Linux、Windows服务器。采用随机读取的方式,支持大文件的读取。支持实时打印新增的日志(类终端)。支持日志搜索。使用手册基本页面配置路径配置日志所在的目录,配置后按回车键生效,下拉框选择日志名称。选择日志后点击生效,即可加载日志。windows路径E:\java\project\log-view\logslinux路径/usr/local/XX历史模式历史模式下,不会读取新增的日志。针对历史文件可以分页读取,配置分页大小、跳转。历史模式下,支持根据关键词搜索。目前搜索引擎使用的是jdk自带类库,搜索速度相对较低,优点是比较简单。2G日志全文搜

  3. springboot使用validator进行参数校验 - 2

    1.依赖导入org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-validation2.validation常用注解@Null被注释的元素必须为null@NotNull被注释的元素不能为null,可以为空字符串@AssertTrue被注释的元素必须为true@AssertFalse被注释的元素必须为false@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值@D

  4. ruby - 如何将 Interactive Ruby 整合到我的开发过程中? - 2

    我正在尝试找到一种更好的方法将IRB与我的常规ruby​​开发集成。目前我很少在我的代码中使用IRB。我只用它来验证语法或尝试一些小的东西。我知道我可以将我自己的代码加载到ruby​​中作为一个require'mycode'但这通常不符合我的编程风格。有时我要检查的变量超出范围或在循环内。有没有一种简单的方法可以启动我的脚本并在IRB内的某个点卡住?我想我正在寻找一种更简单的方法来调试我的ruby​​代码而不破坏我的F5(编译)键。也许有经验的ruby开发者可以和我分享一个更精简的开发方法。 最佳答案 安装ruby​​-debugg

  5. ruby - 使用 Drupal 和 Ruby。有没有人整合两者? - 2

    我开始了一个小型网络项目并使用Drupal来构建它。到目前为止,还不错:您可以快速建立一个不错的面向CMS的网站,通过模块添加社交功能,并且您有一个广泛的API可以在一个架构良好的平台中进行自定义。现在问题来了:网站的增长超出了最初的计划,我发现自己正处于认真开始为它编写代码的境地。由于Drupal项目,我对PHP有了新的认识,但我想用Ruby来做。我会感觉更舒服,以后维护起来更容易,我可以在其他Ruby/Rails应用程序中重用它。随着时间的推移,我想我会用Ruby重写Drupal中的现有部分。基于此,问题是:是否有人将两者(成功或失败的故事)结合起来?这是一个相当大的决定,但我在G

  6. 停车系统源码-基于springboot+uniapp开源项目 - 2

    Iparking停车收费管理系统-可商用介绍Iparking是一款基于springBoot的停车收费管理系统,支持封闭车场和路边车场,支持微信支付宝多种支付渠道,支持多种硬件,涵盖了停车场管理系统的所有基础功能。技术栈Springboot,MybatisPlus,Beetl,Mysql,Redis,RabbitMQ,UniApp功能云端功能序号模块功能描述1系统管理菜单管理配置系统菜单2系统管理组织管理管理组织机构3系统管理角色管理配置系统角色,包含数据权限和功能权限配置4系统管理用户管理管理后台用户5系统管理租户管理多租户管理6系统管理公众号配置租户公众号配置7系统管理操作日志审计日志8系统

  7. 优化大数据量查询方案——SpringBoot(Cloud)整合ES - 2

    一、Elasticsearch简介实际业务场景中,多端的查询功能都有很大的优化空间。常见的处理方式有:建索引、建物化视图简化查询逻辑、DB层之上建立缓存、分页…然而随着业务数据量的不断增多,总有那么一张表或一个业务,是无法通过常规的处理方式来缩短查询时间的。在查询功能优化上,作为开发人员应该站在公司的角度,本着优化客户体验的目的去寻找解决方案。本人有幸做过Tomcat整合solr,今天一起研究一下当前比较火热的Elasticsearch搜索引擎。Elasticsearch是一个非常强大的搜索引擎。它目前被广泛地使用于各个IT公司。Elasticsearch是由Elastic公司创建。它的代码位

  8. Spring Security详细讲解(JWT+SpringSecurity登入案例) - 2

    本篇博文目录:一.SpringSecurity简介1.SpringSecurity2.SpringSecurity相关概念二.认证和授权1.认证(1)使用SpringSecurity进行简单的认证(SpringBoot项目中)(2)SpringSecurity的原理(3)SpringSecurity核心类(4)认证登入案例(JWT+SpringSecurity实现登入案例)2.授权(1)加入权限到Authentication中(2)SecurityConfig配置文件中开启注解权限配置(3)给接口中的方法添加访问权限(4)用户权限表的建立3.自定义失败处理(1)创建异常处理类(2)配置移除处理

  9. SpringSecurity 源码理解及使用(三) - 2

    目录springSecurity授权权限管理策略基于url的权限管理基于方法的权限管理将url权限管理设为动态会话管理会话并发管理会话失效处理禁止再次登录会话共享源码分析CSRF跨站请求伪造开启CSRF防御传统web开发前后端分离开启CSRF防护csrf防御过程CORS跨域问题springBoot解决跨域的三种方式springSecurity解决跨域springSecurity授权认证与授权解耦授权:据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有权限,去执行相应操作。GrantedAuthority应该如何理解呢?是角色还是权限?权限是具体一些操作,角色是一些权

  10. SpringBoot+Netty实现TCP客户端实现接收数据按照16进制解析并存储到Mysql以及Netty断线重连检测与自动重连 - 2

    场景在SpringBoot项目中需要对接三方系统,对接协议是TCP,需实现一个TCP客户端接收服务端发送的数据并按照16进制进行解析数据,然后对数据进行过滤,将指定类型的数据通过mybatis存储进mysql数据库中。并且当tcp服务端断连时,tcp客户端能定时检测并发起重连。全流程效果 注:博客:霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主实现1、SpringBoot+Netty实现TCP客户端本篇参考如下博客,在如下博客基础上进行修改Springboot+Netty搭建基于TCP协议的客户端(二):https://www.cnblogs.com/haolb

随机推荐