SpringSecurity学习笔记

SpringSecurity

一、概述

  • 官网:https://spring.io/projects/spring-securitySpringSecurity官网
  • SpringSecurity是Spring公司开发一个基于认证授权的安全框架
  • 认证:登录的过程
  • 授权:在登录的过程中,授予相应的操作权限
  • 一般权限管理都是RBAC的
  • RBAC:基于角色的访问控制(Role-Based Access Control)
    SpringSecurity学习笔记

二、快速入门

1. 实现步骤

  1. 创建SpringBoot项目
  2. 导入依赖
  3. 控制器
  4. 启动项目
  5. 访问控制器

2. 具体实现

2.1 导入依赖

<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>

2.2 接口

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        return "Hello,Success!";
    }

}

2.3 测试

  1. 访问接口
    若被拦截,则跳回登录页
    若登录成功,则跳转的之前访问的资源
    用户名:user
    密码:控制台随机生成

    SpringSecurity学习笔记

三、配置用户名、密码

1. SpringBoot配置文件

spring:
  #指定用户名与密码
  security:
    user:
      name: java2107
      password: java2107

2. 配置类

2.1 基于内存【了解】

  1. 注释配置文件
  2. 引导类增加加密Bean
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
  1. 增加配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        String newPwd = passwordEncoder.encode("java2107");

        //基于内存配置用户名与密码
        auth.inMemoryAuthentication()
                .withUser("java2107")
                //这个密码一定需要加密,需要指定加密器
                .password(newPwd)
                //指定角色,具体角色可以不指定
                .roles("");
    }

}
  1. 测试

2.2 自定义认证类

  1. 基于内存的配置要注释掉
  2. 实现接口UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        if(null != username && "java2107".equals(username)) {
            //比对密码,security框架会自动去比对密码
            //参数一:数据库的用户名
            //参数二:数据库的密码 $2a$10$mFcvAbMb7iMtuS0vuv/sXuSiKSeb/EVAM8xQekJ0PEWmo9M23UMfS  --> java2107
            //参数三:角色权限列表
            Collection<? extends GrantedAuthority> authorities =
                    AuthorityUtils.commaSeparatedStringToAuthorityList("");
            return new User(username,
                    "$2a$10$mFcvAbMb7iMtuS0vuv/sXuSiKSeb/EVAM8xQekJ0PEWmo9M23UMfS",
                    authorities);
        }

        //认证未通过
        return null;
    }
}

  1. 基于认证类的配置类
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //基于认证类
        //要通过哪个认证类去进行认证。比对密码时,密文密码要明文密码比对【需要指定加密器】
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder);
    }

四、连接数据库

1. 数据库表

CREATE TABLE `tb_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(30) DEFAULT NULL,
  `password` varchar(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

insert  into `tb_user`(`id`,`username`,`password`) values (1,'zhangsan','123456');
#密码需要加密,自己加密

2. 导入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

3. 实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_user")
public class TbUser implements Serializable {

    @TableId
    private Integer id;
    private String username;
    private String password;

}

4. Mapper接口

public interface TbUserMapper extends BaseMapper<TbUser> {
}

5. 引导类注解

@MapperScan(basePackages = {"com.qf.java2107.springsecurity.demo01.mapper"})
引导类

6. 改造认证类

通过用户名查询用户数据

package com.qf.java2107.springsecurity.demo01.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.qf.java2107.springsecurity.demo01.mapper.TbUserMapper;
import com.qf.java2107.springsecurity.demo01.pojo.TbUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Collection;

/**
 * 自定义认证类
 * @author ghy
 * @version 1.0
 * @date 2022-01-13
 **/
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    TbUserMapper tbUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<TbUser> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        //通过用户名查询用户表
        TbUser tbUser = tbUserMapper.selectOne(wrapper);

        if(null != tbUser) {
            Collection<? extends GrantedAuthority> authorities =
                    AuthorityUtils.commaSeparatedStringToAuthorityList("");
            return new User(tbUser.getUsername(), tbUser.getPassword(), authorities);
        }

        //认证未通过
        return null;
    }

}

7. 测试

输入数据库的用户名与密码

8. 获取用户名

  • Security框架会为经过该框架过滤后的资源分配一个匿名账户,该账户没有认证过,但是Security为未认证用户分配一个临时账户anonymousUser,供其访问部分资源
@GetMapping("/show/name")
public String name(){
    //认证对象:当该资源经过Security框架后,就不会为空了。Security框架会为未认证的用户分配一个 匿名 账户,供其访问部分资源
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if(null == principal) {
        return "匿名";
    }
    //经过认证
    if(principal instanceof User) {
        User user = (User) principal;
        return user.getUsername();
    }

    return principal.toString();
}

五、Security的配置

1. 自定义登录表单

  • 默认情况下,Security会为未认证的用户自动生成一个登录表单
  • 如果是我们自己的项目,那么这个登录表单跟我们的项目的静态资源很可能不匹配

1.1 实现

自定义登录表单:

  • 用户名:username
  • 密码:password
  • 登录请求URL:/login
  1. 前端页面
<!-- 使用security之后, 登录请求交给它处理,不用自己写Controller -->
<form action="/user/login" method="post">
    用户名:<input type="text" name="username"/><br>
    密  码:<input type="text" name="password"/><br>
    <input type="submit" value="登录"/><br>
</form>
  1. 配置类【关闭跨站伪造请求】
/**
 * 认证资源的配置
 * @param http
 * @throws Exception
 */
@Override
protected void configure(HttpSecurity http) throws Exception {

    http.formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/user/login")
            //登录表单中的用户名跟密码的name值
            //.usernameParameter("username").passwordParameter("password")
            //如果直接访问登录页,未指定目标资源,则跳到/success
            .defaultSuccessUrl("/success")
            //放行
            .permitAll()
            //开启另一个配置,就使用and分开
            .and()
            //指定拦截资源的规则
            //所有请求,只要认证通过,即可访问
            .authorizeRequests().anyRequest().authenticated()
            //关闭跨站伪造请求
            .and().csrf().disable();
}

2. 配置拦截规则

2.1 配置"经过Security框架,但不需要认证"的资源

  • 比如:欢迎页、注册页…
  1. 新增测试接口
@RestController
public class HelloController {

	//新增测试接口
    @RequestMapping({"/","/index"})
    public String index(){
        return "Hello,index!!";
    }
}
  1. Security配置类

//配置拦截规则,要放行的资源[经过security]
.and().authorizeRequests().antMatchers("/","/index","/user/show/name").permitAll()

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

    http.formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/user/login")
            //登录表单中的用户名跟密码的name值
            //.usernameParameter("username").passwordParameter("password")
            //如果直接访问登录页,未指定目标资源,则跳到/success
            .defaultSuccessUrl("/success")
            //放行
            .permitAll()

            //配置拦截规则,要放行的资源[经过security]
            .and().authorizeRequests().antMatchers("/","/index","/user/show/name").permitAll()

            //开启另一个配置,就使用and分开
            .and()
            //指定拦截资源的规则
            //所有请求,只要认证通过,即可访问
            .authorizeRequests().anyRequest().authenticated()
            //关闭跨站伪造请求
            .and().csrf().disable();
}

SpringSecurity学习笔记

3. 基于角色权限拦截

Security是如何判断当前给予是角色还是权限呢

  • 角色:以ROLE_开头
  • 权限:不是角色就是权限,普通字符串就是权限
  1. 新增测试接口
@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping("/list")
    public String list(){
        return "order list";
    }

}
  1. Security配置类
// hasRole : 单个角色。
// hasAnyRole : 只要满足其中一个角色即可
// hasAuthority()  : 单个权限
// hasAnyAuthority() : 只要满足其中一个权限即可
// hasRole和hasAnyRole 在这里不要加ROLE_前缀

//拦截资源时,先写精确匹配,再写模糊匹配
//访问product/list资源需要product_list权限
.and().authorizeRequests().antMatchers("/product/list").hasAnyAuthority("product_list")  
//访问product控制器中的资源需要user角色
.and().authorizeRequests().antMatchers("/product/**").hasRole("user")  
//访问/order/**资源需要order角色
.and().authorizeRequests().antMatchers("/order/**").hasAnyRole("order") 

SpringSecurity学习笔记

  1. 测试(在认证过程中给角色或权限,以供测试)
    SpringSecurity学习笔记
  2. 如果没有相应的角色或者权限,则会出403错误【权限不足】
    SpringSecurity学习笔记
  3. 自定义权限不足页面
    SpringSecurity学习笔记
  4. Security配置类增加配置
    SpringSecurity学习笔记

4. 基于方法级别拦截

4.1 注释全局拦截配置

SpringSecurity学习笔记

4.2 实现注解(常用)

4.2.1 @Secured

基于角色控制访问权限:是否有权限访问目标方法

  1. 处理器注解
    SpringSecurity学习笔记

  2. 开启注解生效

@EnableGlobalMethodSecurity(securedEnabled = true)

引导类
SpringSecurity学习笔记

4.2.2 @PreAuthorize

基于API指定角色或权限控制是否有权限访问目标方法【方法之前控制】

  1. 处理器打注解
    SpringSecurity学习笔记

  2. 开启注解生效
    引导类
    SpringSecurity学习笔记

4.2.3 @PostAuthorize

可以访问目标处理器,但是要具备相应的角色或者权限才能得到返回结果

SpringSecurity学习笔记

5. 表达式写法

https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#el-access

6. 登出

默认的登出URL:/logout

  1. Security配置类
.and().logout().logoutUrl("/user/logout").logoutSuccessUrl("/login.html")

SpringSecurity学习笔记

  1. 制作一个登录成功页面,让其退出
    SpringSecurity学习笔记

六、工作原理

客户端向服务器请求资源,其实是经过一个SpringSecurity封装的过滤器链才最终到达目录资源。得到目录资源之后,又执行过滤器链,再响应给客户端

  • 过滤器链:由 N 多个过滤器组成一个过滤器链,而SpringSecurity就是通过这些过滤器完成的认证和授权。
  • 执行什么样的操作,就会找对应的过滤器来干活
    SpringSecurity学习笔记
    SpringSecurity学习笔记
上一篇:js递归案例


下一篇:ACM-ICPC寒假算法训练4:图论1(图的遍历)