SpringSecurity简介
文章目录
- SpringSecurity简介
- 一、基本概念
- 1.1 认证(Authentication)方式
- 1.2 会话(Session)介绍
- 1.3 授权(Authorization)介绍
- 1.4 RBAC
 
- 二、SpringSecurity入门
- 2.1 快速入门
- 2.1.1 Maven坐标
- 2.1.2 接口
- 2.1.3 源码
 
- 2.2 配置文件配置用户名和密码
- 2.3 基于内存的多用户管理
 
- 三、加密
- 3.1 密码加密学习
- 3.2 BCryptPasswordEncoder
 
- 四、权限信息
- 4.1 获取登录用户信息
- 4.2 配置用户权限
- 4.3 针对URL授权
- 4.3.1 拒绝任何请求
- 4.3.2 允许登录表单
- 4.3.3 允许任何请求
- 4.3.4 匹配请求
- 4.3.5 总结
 
- 4.4 针对方法进行授权
- 4.4.1 抽象类
- 4.4.2 实现类
- 4.4.3 teacher的Controller
- 4.4.4 用户详情服务接口配置类
- 4.4.5 安全配置
- 4.4.6 测试
 
 
一、基本概念
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) 基于角色的访问控制

用户,角色,权限 本质:就是把权限打包给角色(角色拥有一组权限),又将角色分配给用户(用户拥有多个角色)。
最少包括五张表 (用户表、角色表、用户角色表、权限表、角色权限表)
二、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";
    }
}
访问上面的接口就会出现下面的页面(登录)

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

登录上后便可以访问接口。
之后不会使用这种方式
我们也可以退出,访问IP:端口号/logout

2.1.3 源码
为什么默认的用户名是user?并且密码是一串UUID?
如下代码所示

2.2 配置文件配置用户名和密码
在配置文件中配置如下信息
spring:
  security:
    user:
      name: admin
      password: 123456
此时我们再使用默认的用户就不行了,而且控制台也不会输出UUID形式的密码了

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中配置的用户名和密码(此时配置文件中的用户名和密码会失效)


UserDetails类如下所示:
三、加密
3.1 密码加密学习
- 密码为什么要加密?
CSDN网站六百万用户信息外泄-月光博客 (williamlong.info)
在加解密时一定要注意,我们要使用明文能加密成密文但是密文解密不成明文的加密算法
如果密文能解密,那我们加密的效果其实不算太好,黑客照样还是可以破译

-  加密的方式有哪些?涉及到密码加密问题 密码加密一般使用散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密码,反之不行) 常见的散列算法有MD5和SHA SpringSecurity提供了多种密码加密方案,基本上都实现了PasswordEncode接口,官方推荐使用BCryptPasswordEncode  

-  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
以下就是错误的情况

我们可以将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)

如果用户名和密码正确,说明认证成功
此时会把认证信息放到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中定义的内容
"accountNonExpired"账户是否未过期,true表示未过期
"accountNonLocked"账户是否未锁定,true表示未锁定
"credentialsNonExpired"凭证是否未过期,true表示未过期,这里的凭证一般是指密码
**“enabled”**账户是否可用,true表示可用只有当"accountNonExpired"、“accountNonLocked”、“credentialsNonExpired”、"enabled"都为true时,账户才能使用

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

其中roles可以配置多个角色,其有一个可变数组,这样来说一个用户可以配置多个角色
UserDetails user1 = User.builder().username("zhangjingqi-1").password(passwordEncoder().encode("123456")).roles("student","manager").build();

下面来配置一下用户的权限
    @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”变成权限

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

所以配置用户权限我们得出一个结论:
-  在代码中配置“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如下所示

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();//允许表单登录
    }
此时就可以看到下面的登录界面了,但是如果访问其他请求,就会被拒绝

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

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

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

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

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

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

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

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

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

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

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






















