1.SpringSecurity -快速入门、加密、基础授权

SpringSecurity简介

一、基本概念

1.1 认证(Authentication)方式

系统为什么要认证

认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源

什么是认证(登录)

用户认证就是判断一个用户的身份是否合法的过程

常见的用户身份认证方式

  • 用户名密码登录
  • 二维码登录
  • 手机短信登录
  • 指纹认证
  • 人脸识别

1.2 会话(Session)介绍

下面这个文章中有对session的理解:基于Session实现短信登录_c# 登录session怎么使用_我爱布朗熊的博客-CSDN博客

过滤器与拦截器 - 登录校验与登录认证(JWT令牌技术)_jwt过滤器_我爱布朗熊的博客-CSDN博客

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。

会话就是系统为了保持当前用户的登录状态所提供的机制

常见的有基于session方式、基于token方式

  • 基于session的认证方式

    它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

有一些用户在使用浏览器的时候会禁用Cookie,这会导致发一次请求,用户登录一次

  • 基于token的认证方式

    它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。可以使用Redis 存储用户信息(分布式中共享session)。

    基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

1.3 授权(Authorization)介绍

为什么要授权(控制资源被访问)?

因为不同的用户可以访问的资源不一样

什么是授权(给用户颁发权限)?

授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程

拥有资源的访问权限则正常访问,没有权限则拒绝访问

1.4 RBAC

RBAC(Role-Based Access Control) 基于角色的访问控制

image-20230925222010168

用户,角色,权限 本质:就是把权限打包给角色(角色拥有一组权限),又将角色分配给用户(用户拥有多个角色)。

最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)

二、SpringSecurity入门

声明式(注解)的安全访问控制解决方案的安全框架

提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作

2.1 快速入门

2.1.1 Maven坐标

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

2.1.2 接口

@RestController
@RequestMapping("/hello")
public class HelloController {
    
    

    @GetMapping("/hello")
    private String hello(){
    
    
        return "hello";
    }

}

访问上面的接口就会出现下面的页面(登录)

image-20231007105125017

默认的用户名是User,密码是在控制台上的UUID

image-20231007105319945

登录上后便可以访问接口。

之后不会使用这种方式

我们也可以退出,访问IP:端口号/logout

image-20231007105829342

2.1.3 源码

为什么默认的用户名是user并且密码是一串UUID

如下代码所示

image-20231007111234007

2.2 配置文件配置用户名和密码

在配置文件中配置如下信息

spring:
  security:
    user:
      name: admin
      password: 123456

此时我们再使用默认的用户就不行了,而且控制台也不会输出UUID形式的密码了

image-20231007110802830


Spring Security配置文件中默认配置用户是单一的用户,大部分系统都有多个用户,多个用户如何配置

可以使用基于内存的多用户管理

2.3 基于内存的多用户管理

2.2 中配置的用户名和密码只能配置一个,但是我们的系统有许多的用户名和密码。

/**
 * 自定义类实现用户详情服务接口
 *
 *   系统中默认是有这个UserDetailsService的,也就是默认的用户名(user)和默认密码(控制台生成的)
 *   如果在yaml文件中配置了用户名和密码,那在系统中的就是yaml文件中的信息
 *
 * 我们自定义了之后,就会把系统中的UserDetailsService覆盖掉
 */
@Configuration
public class MySecurityUserConfig  {
    
    
    /**
     * 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetailsService(包括用户名、密码、用户所拥有的权限)
     *
     * UserDetails存储的是用户的用户名、密码、去权限信息
     */
    @Bean
    public UserDetailsService userDetailsService() {
    
    
//      用户细节信息,创建两个用户
//      此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
        UserDetails user1 =User.builder().username("zhangjingqi1").password("123456").roles("student").build();
        UserDetails user2 = User.builder().username("zhangjingqi2").password("123456789").roles("teacher").build();


//      InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(user1);
        userDetailsManager.createUser(user2);
        return  userDetailsManager;
    }
}   

上面的程序启动会报错,原因

SpringSecurity强制要使用密码加密,当然我们也可以不加密,但是官方要求是不管你是否加密,都必须配置一个密码编码(加密)器

/**
 * 自定义用户必须配置密码加密器。
 * NoOpPasswordEncoder.getInstance() 此实例表示不加密
 */
@Bean
public PasswordEncoder passwordEncoder(){
    
    
    return NoOpPasswordEncoder.getInstance();
}

此时用户名和密码就是user1与user2中配置的用户名和密码(此时配置文件中的用户名和密码会失效)

image-20231014192803769

image-20231014192744528



UserDetails类如下所示:

image-20231014190836999

三、加密

3.1 密码加密学习

  • 密码为什么要加密

CSDN网站六百万用户信息外泄-月光博客 (williamlong.info)

在加解密时一定要注意,我们要使用明文能加密成密文但是密文解密不成明文的加密算法

如果密文能解密,那我们加密的效果其实不算太好,黑客照样还是可以破译

image-20231014211903859

  • 加密的方式有哪些涉及到密码加密问题

    密码加密一般使用散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密码,反之不行)

    常见的散列算法有MD5和SHA

    SpringSecurity提供了多种密码加密方案,基本上都实现了PasswordEncode接口官方推荐使用BCryptPasswordEncode

    image-20231014213450153

image-20231014213514723

  • NoOpPasswordEncoder类已经过期了,而且还没有加密,怎么解决

  • 以学生身份登录,发现不但可以访问学生的页面,还可以访问教师的页面和管理员的页面,如何解决

    权限问题,后面解决

  • 如果要动态的创建用户,或者修改密码等(不是把用户名和密码写死到代码中),怎么办

热疹信息存储到数据库中

3.2 BCryptPasswordEncoder

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode1 = passwordEncoder.encode("123456");
        String encode2 = passwordEncoder.encode("123456");
        String encode3 = passwordEncoder.encode("123456");
        System.out.println(encode1);//$2a$10$hkaTFSnsEBYcZxXMrDpMQu.IiPM5ZAIQ63Vvkq01.oxxv0yVRmKly
        System.out.println(encode2);//$2a$10$nuPqwjhW0e/RZ.h3L1gZx.KanwUNQd4GEB2YoeB/LeVOhavcoBS7O
        System.out.println(encode3);//$2a$10$SPHV9tuI6JhgOwDO2hMR4eq43E5BGmYJDQJ5GltIuK7WSQqi1sHzm

//      参数1:原文   参数2:密文
        boolean result1 = passwordEncoder.matches("123456", encode1);
        boolean result2 = passwordEncoder.matches("123456", encode2);
        System.out.println(result1);//true
        System.out.println(result2);//true

补充断言的知识

如果result1的结果是true的话,运行时的标记就是”√“,反之则是“×”

assertTrue(result1);//期望result1为true
assertFalse(result2);//期望result2为true

以下就是错误的情况

image-20231014220215113

我们可以将PasswordEncoder放入到Bean工厂中

/**
 * 配置密码加密器
 * NoOpPasswordEncoder.getInstance() 此实例表示不加密
 * BCryptPasswordEncoder() 会加密
 */
@Bean
public PasswordEncoder passwordEncoder(){
    
    
    return new BCryptPasswordEncoder();
}

修改我们之前的程序

    @Bean
    public UserDetailsService userDetailsService() {
    
    
//      用户细节信息,创建两个用户
//      此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
        UserDetails user1 = User.builder().username("zhangjingqi-1").password(passwordEncoder().encode("123456")).roles("student").build();
        UserDetails user2 = User.builder().username("zhangjingqi-2").password(passwordEncoder().encode("123456")).roles("teacher").build();

//      InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(user1);
        userDetailsManager.createUser(user2);
        return  userDetailsManager;
    }

四、权限信息

4.1 获取登录用户信息

关于已经验证实体的详情被存储到安全性上下文中(SpringContext)

image-20231014222351220

如果用户名和密码正确,说明认证成功

此时会把认证信息放到Authentication中,再把Authentication放到SecurityContext中

然后可以通过安全上下文持有器SecurityContextHolder获取安全上下文

/**
 * 获取用户登录信息的方式
 */
@RestController
@Slf4j
public class CurrentLoginUserController {
    
    

    /**
     * import org.springframework.security.core.Authentication;
     * 一旦登录成功,访问下面的请求就可以得到authentication
     *
     * Authentication 继承 Principal
     *
     */
    @GetMapping("/getLoginUser1")
    public Authentication getLoginUser1(Authentication authentication) {
    
    
        return authentication;
    }



    /**
     *import java.security.Principal;
     * 一旦登录成功,访问下面的请求就可以得到principal
     */
    @GetMapping("/getLoginUser2")
    public Principal getLoginUser2(Principal principal) {
    
    
        return principal;
    }

    /**
     * 一旦我们登陆成功,框架就会把我们的信息放到安全性上文中SpringContext
     * 所以我们可以通过安全性上文SpringContext获取用户信息
     */
    @GetMapping("/getLoginUser3")
    public Principal getLoginUser3() {
    
    
//      通过安全上下文持有器获取安全上下文
        return SecurityContextHolder.getContext().getAuthentication();
    }

}

访问:localhost:8080/getLoginUser1

“name”用户名

“credentials”凭据,其实就是密码,这里是null的原因就是直接给展示密码太不安全了,所以显示为null

“authorities”权限信息,授权,权限

“principal”中的内容就是代码UserDetails中定义的内容

image-20231014224745740

"accountNonExpired"账户是否未过期,true表示未过期

"accountNonLocked"账户是否未锁定,true表示未锁定

"credentialsNonExpired"凭证是否未过期,true表示未过期,这里的凭证一般是指密码
**“enabled”**账户是否可用,true表示可用

只有当"accountNonExpired"、“accountNonLocked”、“credentialsNonExpired”、"enabled"都为true时,账户才能使用

image-20231014223719223

4.2 配置用户权限

如下图所示,便是给用户配置权限

image-20231015151149649

其中roles可以配置多个角色,其有一个可变数组,这样来说一个用户可以配置多个角色

UserDetails user1 = User.builder().username("zhangjingqi-1").password(passwordEncoder().encode("123456")).roles("student","manager").build();

image-20231015151234272

下面来配置一下用户的权限

    @Bean
    public UserDetailsService userDetailsService() {
    
    
//      用户细节信息,创建两个用户
//      此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
        UserDetails user1 = User.builder()
                .username("zhangjingqi-1")
                .password(passwordEncoder().encode("123456"))
//               配置用户角色
                .roles("student", "manager") //角色到系统中会变成权限的,比如这里会变成ROLE_student,ROLE_manager
//               配置用户权限. student:delete权限处理学生的删除,student:add权限处理学生的添加
                .authorities("student:delete","student:add")
                .build();
        
        UserDetails user2 = User.builder()
                .username("zhangjingqi-2")
                .password(passwordEncoder().encode("123456"))
//              配置权限
                .authorities("teacher:delete", "teacher:add")
//              配置角色
                .roles("teacher")
                .build();

//      InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(user1);
        userDetailsManager.createUser(user2);
        return userDetailsManager;
    }

登录zhangjingqi-1账号,访问localhost:8080/getLoginUser3

我们发现,JSON串中“authorities”的内容是中配置的”authorities“,而没有将“roles”变成权限

image-20231015152408310

之后再登录zhangjingqi-2账号访问localhost:8080/getLoginUser3,我们发现下面的JSON串中”authorities“是在代码中配置的“roles”角色信息转换成的权限

image-20231015152855592

所以配置用户权限我们得出一个结论:

  • 在代码中配置“authorities“与“roles”时,谁再后面谁执行,后者会覆盖前者;

  • 配置的”roles“角色信息,在前面添加一个“ROLE_”前缀就会变成权限信息

  • 系统不会给配置的”authorities“权限信息添加任何前缀

4.3 针对URL授权

此控制级别只在Controller层

虽然我们实现了认证功能,但是受保护的资源是默认的,默认所有认证(登录)用户均可以访问所有资源,不能根据实际情况进行角色管理

实现授权功能,需重新WebSecurityConfigurerAdapter中的一个configure方法

虽然之前我们配置了用户角色和用户权限,但是用户登录上之后依然可以随便访问,比如说“student”角色可以访问“teacher”角色对应的功能。

4.3.1 拒绝任何请求

如下代码所示:表示拒绝任何http请求

虽然可以使用zhangjingqi-1或zhangjingqi-2认证成功登录,但是Controller不让访问,并且包括springSecurity框架中的login也会被屏蔽掉

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    /**
     * 重写 configure(HttpSecurity http)方法
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

       http.authorizeRequests()//授权http请求
               .anyRequest()//任何请求
               .denyAll();//拒绝
    }

访问http://localhost:8080/login如下所示

image-20231015155702659

4.3.2 允许登录表单

为了可以访问http://localhost:8080/login,我们可以使用下面的一个配置

    /**
     * 重写 configure(HttpSecurity http)方法
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

       http.authorizeRequests()//授权http请求
               .anyRequest()//任何请求
               .denyAll();//拒绝

        http.formLogin().permitAll();//允许表单登录

    }

此时就可以看到下面的登录界面了,但是如果访问其他请求,就会被拒绝

image-20231015160916199

4.3.3 允许任何请求

    /**
     * 重写 configure(HttpSecurity http)方法
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

        http.authorizeRequests()//授权http请求
                .anyRequest()//任何请求
                .permitAll();//允许任何请求


        http.formLogin().permitAll();//允许表单登录
    }

4.3.4 匹配请求

zhangjingqi-1对应的权限:student:delete、student:add

zhangjingqi-2对应的权限:ROLE_teacher

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    /**
     * 重写 configure(HttpSecurity http)方法
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

        http.authorizeRequests()//授权http请求
                .mvcMatchers("/student/**")// 匹配/student开头的请求
                .hasAnyAuthority("student:add")// 拥有student:add权限的用户可以访问上面的url

                .mvcMatchers("/teacher/**") // 匹配/teacher/**开头的请求
                .hasAnyAuthority("ROLE_teacher")//拥有ROLE_teacher权限的用户可以访问/teacher/**开头的url

                .anyRequest()// 任何请求
                .authenticated()//需要验证。注意:没有配置的url,只要登录成功就可以访问
        ;

        http.formLogin().permitAll();//允许表单登录
    }
}

使用zhangjingqi-1访问一下localhost:8080/student/hello

image-20231015162519777

使用zhangjingqi-1访问一下localhost:8080/hello/hello

image-20231015162537580

使用zhangjingqi-1访问一下localhost:8080/teacher/hello

image-20231015162553881

使用zhangjingqi-2访问一下http://localhost:8080/student/hello

image-20231015162750496

使用zhangjingqi-2访问一下localhost:8080/hello/hello

image-20231015162537580

使用zhangjingqi-2访问一下localhost:8080/teacher/hello

image-20231015162818021

4.3.5 总结

  • 匹配请求的三种方式
.mvcMatchers("/student/**")
.regexMatchers("/student/**")
.anyMatchers("/student/**")
  • 判断权限的五种方式

推荐第一种

.hasAuthority("ROLE_teacher") //是否有单个权限,参数只能写一个权限
.hasAnyAuthority("ROLE_teacher","student:add")//是否有其中的任意一个权限
.access( "hasAuthority('student:query') or  hasAuthority('student:add')")
.hasRole("student") // 是否有单个角色
.hasAnyRole("student") //是否有其中的某个角色
  • 先写匹配路径,再写对应的权限

4.4 针对方法进行授权

下面 通过更灵活的配置方法安全,我们先通过@EnableGlobalMethodSecurity开启基于注解的安全配置

4.4.1 抽象类

public interface TeacherService {
    
    
    String add();

    String update();

    String delete();

    String query();
}

4.4.2 实现类

@Slf4j
@Service
public class TeacherServiceImpl implements TeacherService {
    
    

    //预授权注解,此处采用表达式的形式。
    //如果有teacher:add权限,才会执行下面这个方法;反之不会访问
    @PreAuthorize("hasAuthority('teacher:add')")
    @Override
    public String add() {
    
    
        log.info("添加教师成功");
        return "添加教师成功";
    }

    //只要有teacher:update或teacher:add权限其中之一,便可以执行下面的方法
    @PreAuthorize("hasAnyAuthority('teacher:update','teacher:add')")
    @Override
    public String update() {
    
    
        log.info("修改教师成功");
        return "修改教师成功";
    }

    @PreAuthorize("hasAuthority('teacher:delete')")
    @Override
    public String delete() {
    
    
        log.info("删除教师成功");
        return "删除教师成功";
    }

    @PreAuthorize("hasAuthority('teacher:query')")
    @Override
    public String query() {
    
    
        log.info("查询教师成功");
        return "查询教师成功";
    }
}

4.4.3 teacher的Controller

@RestController
@RequestMapping("/teacher")
public class TeacherController {
    
    

    @Resource
    private TeacherService teacherService;


    @GetMapping("/query")
    public String queryInfo() {
    
    
        return teacherService.query();
    }

    @GetMapping("/add")
    public String addInfo() {
    
    
        return teacherService.add();
    }

    @GetMapping("/update")
    public String updateInfo() {
    
    
        return teacherService.update();
    }

    @GetMapping("/delete")
    public String deleteInfo() {
    
    
        return teacherService.delete();
    }
}

4.4.4 用户详情服务接口配置类

@Configuration
public class MySecurityUserConfig {
    
    
    /**
     * 根据用户名把用户的详情从数据库中获取出来,封装成用户细节信息UserDetails(包括用户名、密码、用户所拥有的权限)
     * <p>
     * UserDetails存储的是用户的用户名、密码、去权限信息
     */
    @Bean
    public UserDetailsService userDetailsService() {
    
    
//      用户细节信息,创建两个用户
//      此User是SpringSecurity框架中的public class User implements UserDetails, CredentialsContainer
        UserDetails user1 = User.builder()
                .username("zhangjingqi-1")
                .password(passwordEncoder().encode("123456"))
//               配置用户角色
                .roles("student") //角色到系统中会变成权限的,比如这里会变成ROLE_student,ROLE_manager
                .build();

        UserDetails user2 = User.builder()
                .username("zhangjingqi-2")
                .password(passwordEncoder().encode("123456"))
//              配置权限
                .authorities("teacher:query")
                .build();

        UserDetails user3 = User.builder()
                .username("admin")
                .password(passwordEncoder().encode("123456"))
//              配置权限
                .authorities("teacher:query","teacher:add","teacher:update","teacher:delete")
                .build();

//      InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService,其中UserDetailsManager继承UserDetailsService
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(user1);
        userDetailsManager.createUser(user2);
        userDetailsManager.createUser(user3);
        return userDetailsManager;
    }

    /**
     * 配置密码加密器
     * NoOpPasswordEncoder.getInstance() 此实例表示不加密
     * BCryptPasswordEncoder() 会加密
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

}

4.4.5 安全配置

WebSecurityConfig这个地方其实是URL的权限配置

预授权:在访问方法之前判断有无权限(使用量最大)

后授权:访问完方法后再判断有无权限

//@Configuration
//开启全局方法安全。 prePostEnabled = true 表示预授权和后授权开启
//此注解中包含@Configuration注解,所以不用重复标识
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    

        http.authorizeRequests()//授权http请求
                .anyRequest() //任何请求
                .authenticated();//都需要认证

        http.formLogin().permitAll();//允许表单登录

//      或者下面这种形式
//        http.authorizeRequests()//授权http请求
//                .anyRequest() //任何请求
//                .authenticated()//都需要认证
//                .and()
//                .formLogin().permitAll();
    }
}

4.4.6 测试

用户admin,访问localhost:8080/teacher/query

image-20231016213447361

用户admin,访问localhost:8080/teacher/add

image-20231016213506529

用户zhangjingqi-2,访问localhost:8080/teacher/query

image-20231016213447361

用户zhangjingqi-2,访问localhost:8080/teacher/add

image-20231016213634402

用户zhangjingqi-1,访问localhost:8080/teacher/query

image-20231016213716614

猜你喜欢

转载自blog.csdn.net/weixin_51351637/article/details/133870484