SpringSecurity学习

1、什么是SpringSecurity

Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。

​ 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

​ 一般Web应用的需要进行认证授权

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

​ 而认证和授权也是SpringSecurity作为安全框架的核心功能。

  • SpringSecurity的完成流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor:负责权限校验的过滤器。

2、SpringSecurity快速入门

2.1 准备工作

  • 首先创建一个Maven项目并导入依赖
 1    <parent>
 2        <groupId>org.springframework.boot</groupId>
 3        <artifactId>spring-boot-starter-parent</artifactId>
 4        <version>2.5.0</version>
 5    </parent>
 6    <dependencies>
 7        <dependency>
 8            <groupId>org.springframework.boot</groupId>
 9            <artifactId>spring-boot-starter-web</artifactId>
10        </dependency>
11        <dependency>
12            <groupId>org.projectlombok</groupId>
13            <artifactId>lombok</artifactId>
14            <optional>true</optional>
15        </dependency>
16    </dependencies>
  • 然后创建SpringBoot的启动项
1@SpringBootApplication
2public class SecurityApplication {
3
4    public static void main(String[] args) {
5        SpringApplication.run(SecurityApplication.class,args);
6    }
7}
  • 创建一个Controller类
 1
 2import org.springframework.web.bind.annotation.RequestMapping;
 3import org.springframework.web.bind.annotation.RestController;
 4
 5@RestController
 6public class HelloController {
 7
 8    @RequestMapping("/hello")
 9    public String hello(){
10        return "hello";
11    }
12}

2.2引入SpringSecurity相关依赖

1  <dependency>
2            <groupId>org.springframework.boot</groupId>
3            <artifactId>spring-boot-starter-security</artifactId>
4  </dependency>

在引入该依赖后,我们在访问/hello接口时会自动跳转到security自带的登录界面。 登录的用户名默认是 user,登录的密码会在控制台输出。

3、认证

3.1 简单认证流程

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

3.2自定义登录用户查询

从之前的分析我们可以知道,我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。

  • 我们先创建我们需要的用户信息表
 1CREATE TABLE `sys_user` (
 2  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
 3  `user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
 4  `nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
 5  `password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
 6  `status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
 7  `email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
 8  `phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
 9  `sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
10  `avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
11  `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
12  `create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
13  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
14  `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
15  `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
16  `del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
17  PRIMARY KEY (`id`)
18) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
  • 在刚才的初始项目中引入MybatisPuls和mysql驱动的依赖
1        <dependency>
2            <groupId>com.baomidou</groupId>
3            <artifactId>mybatis-plus-boot-starter</artifactId>
4            <version>3.4.3</version>
5        </dependency>
6        <dependency>
7            <groupId>mysql</groupId>
8            <artifactId>mysql-connector-java</artifactId>
9        </dependency>
  • 在资源目录下创建application.ymal文件导入下面的配置,记得修改自己的数据库密码
1spring:
2  datasource:
3    url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTC
4    username: root
5    password: root
6    driver-class-name: com.mysql.cj.jdbc.Driver
  • 定义Mapper接口
1public interface UserMapper extends BaseMapper<User> {
2}
  • 创建用户实体类
 1@Data
 2@AllArgsConstructor
 3@NoArgsConstructor
 4@TableName(value = "sys_user")
 5public class User implements Serializable {
 6    private static final long serialVersionUID = -40356785423868312L;
 7
 8    /**
 9     * 主键
10     */
11    @TableId
12    private Long id;
13    /**
14     * 用户名
15     */
16    private String userName;
17    /**
18     * 昵称
19     */
20    private String nickName;
21    /**
22     * 密码
23     */
24    private String password;
25    /**
26     * 账号状态(0正常 1停用)
27     */
28    private String status;
29    /**
30     * 邮箱
31     */
32    private String email;
33    /**
34     * 手机号
35     */
36    private String phonenumber;
37    /**
38     * 用户性别(0男,1女,2未知)
39     */
40    private String sex;
41    /**
42     * 头像
43     */
44    private String avatar;
45    /**
46     * 用户类型(0管理员,1普通用户)
47     */
48    private String userType;
49    /**
50     * 创建人的用户id
51     */
52    private Long createBy;
53    /**
54     * 创建时间
55     */
56    private Date createTime;
57    /**
58     * 更新人
59     */
60    private Long updateBy;
61    /**
62     * 更新时间
63     */
64    private Date updateTime;
65    /**
66     * 删除标志(0代表未删除,1代表已删除)
67     */
68    private Integer delFlag;
69}
  • 在启动类中配置mapper扫描
1@SpringBootApplication
2@MapperScans({@MapperScan("com.sg.mapper")})
3public class App2
4{
5    public static void main( String[] args )
6    {
7        SpringApplication.run(App2.class);
8    }
9}
  • 核心代码实现

创建一个类实现UserDetailsService接口,重写其中的方法。更加用户名从数据库中查询用户信息

 1@Service
 2public class UserDetailsServiceImpl implements UserDetailsService {
 3
 4    @Autowired
 5    private UserMapper userMapper;
 6
 7    @Override
 8    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 9        //根据用户名查询用户信息
10        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
11        wrapper.eq(User::getUserName,username);
12        User user = userMapper.selectOne(wrapper);
13        //如果查询不到数据就通过抛出异常来给出提示
14        if(Objects.isNull(user)){
15            throw new RuntimeException("用户名或密码错误");
16        }
17        //TODO 根据用户查询权限信息 添加到LoginUser中
18
19        //封装成UserDetails对象返回
20        return new LoginUser(user);
21    }
22}

因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

 1@Data
 2@AllArgsConstructor
 3@NoArgsConstructor
 4public class LoginUser implements UserDetails {
 5    private User user;
 6
 7    @Override
 8    public Collection<? extends GrantedAuthority> getAuthorities() {
 9        return null;
10    }
11
12    @Override
13    public String getPassword() {
14        return user.getPassword();
15    }
16
17    @Override
18    public String getUsername() {
19        return user.getUserName();
20    }
21
22    @Override
23    public boolean isAccountNonExpired() {
24        return true;
25    }
26
27    @Override
28    public boolean isAccountNonLocked() {
29        return true;
30    }
31
32    @Override
33    public boolean isCredentialsNonExpired() {
34        return true;
35    }
36
37    @Override
38    public boolean isEnabled() {
39        return true;
40    }
41}

然后我们运行项目使用数据库中的用户名和密码登录就会发现无法登录,这是因为spring security自带的有密码加密 我们想要在这次测试中不使用密码就需要在数据库中的密码前面加上{noop}这个表示不进行密码加密,

但是在日后的开发中 我们的用户创建个人账户时肯定是需要加密的,这个加密方式的选择就需要在WebSecurityConfigurerAdapter中进行编写了。

 1@Configuration
 2public class SecurityConfig extends WebSecurityConfigurerAdapter {
 3
 4
 5    @Bean
 6    public PasswordEncoder passwordEncoder(){
 7        return new BCryptPasswordEncoder();
 8    }
 9
10}

这里我们使用的是BCryptPasswordEncoder方法进行的加密吗,但是需要注意这里的加密的意思是把密码加密后与数据库中已经加密的密码进行比较 因此我们需要把加密后的密码存入数据库,我们可以在测试方法中调用BCryptPasswordEncoder来获取正常密码加密后的密码。

 1@SpringBootTest
 2public class AppTest {
 3    @Test
 4    public void Test2(){
 5        BCryptPasswordEncoder bc=new BCryptPasswordEncoder();
 6        String encode = bc.encode("1234");
 7        System.out.println(encode);
 8        System.out.println(bc.matches("1234", "$2a$10$dtsT5D5X5YjHKXYCNYH.KeEIbgp.EOpOL6ILDVHAp82SHk1oRCjla"));
 9    }
10
11}

在进行登录测试就可以正常登录了

3.3自定义登录接口

我们上面完成了自定义用户信息登录的测试,但是在前后端分离的项目中我们是需要一个登录的接口,具体的登录界面是由前端实现的所以我们需要自定义登录接口 首先就是在controller中定义一个相关接口

 1@RestController
 2public class LoginController {
 3
 4    @Autowired
 5    private LoginServcie loginServcie;
 6
 7    @PostMapping("/user/login")
 8    public String login(@RequestBody User user){
 9        return loginServcie.login(user);
10    }
11}
1public interface LoginServcie {
2    String login(User user);
3}
1@Component
2public class LoginServcieImpl implements LoginServcie {
3    @Override
4    public String login(User user) {
5        return "登录成功";
6    }
7}

在定义完该接后我们还需要对securityConfig进行一些相关设置使/user/login接口不需要登录

 1 */
 2@Configuration
 3public class securityConfig extends WebSecurityConfigurerAdapter {
 4    @Autowired
 5    private AuthenticationSuccessHandler successHandler;
 6
 7    //设置密码编码格式
 8    @Bean
 9    public PasswordEncoder passwordEncoder(){
10        return new BCryptPasswordEncoder();
11    }
12    
13    @Override
14    protected void configure(HttpSecurity http) throws Exception {
15        //启用默认的登录表单
16        http.formLogin();
17        http
18                //关闭csrf
19                .csrf().disable()
20                //不通过Session获取SecurityContext
21                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
22                .and()
23                .authorizeRequests()
24                // 对于登录接口 允许匿名访问
25                .antMatchers("/user/login").anonymous()
26                // 除上面外的所有请求全部需要鉴权认证
27                .anyRequest().authenticated();
28
29
30    }
31}

我们这里定义的登录接口是使用post方法并且需要携带参数所以就使用postman进行测试

  • 这是我们正常请求登录时

  • 但是请求其他接口时都需要进行登录验证