spring security(一)-基本认证

1.简介

当前绝大多数网站都都存在着用户认证用户授权这最基本的功能,关于这两个功能概述如下:

  • 用户认证:验证某个用户身份为系统中合法的身份,说白了就是验证用户有没有权限来操作系统某些功能。传统做法通过==用户名==和==密码==来完成认证的功能
  • 用户授权:校验某个用户是否有权限去执行某个操作。在一个系统中,不同的用户拥有的权限是不同的。例如:后台管理系统,不同的用户登录进去,看到的界面不同,这就是用户授权

Spring Security 就是一个这样的用户认证与授权框架,其介绍如下:

image-20201221141447471

官网地址:https://spring.io/projects/spring-security

image-20201221141542308

官方文档地址:https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/

image-20201221141619207

2.对比

除了Spring Security可以进行授权认证以外,Apach Shiro也可以进行授权认证,简单对比如下:

  • 使用方面

    ShiroSpring更容易使用,实现和最重要的理解,在SSM阶段,授权认证一致都是Shiro的天下,虽然Spring Security已经出现好久,但是由于其配置复杂性,就让很多人望而却步了,同时对于一般的项目来说,Shiro也完全能够胜任。

但是SpringBoot以后,它的自动配置功能,简化了Spring Security 配置步骤,只需要使用更少的配置来使用该框架

因此,具体是使用Shiro还是Security具体看整个项目的架构,常见组合如下:

  • ssm + shiro

  • springboot / spring cloud + spring security

  • 其他方面

    Spring SecuritySpring 天然无缝结合,同时还提供了 对 OAuth 与 OpenId的支持,但是Shiro则需要手动实现

3.实现

在这里先通过实现一个最基本的HelloWorld,来了解Spring Security工作原理

  • 版本:

    Spring Boot : 2.3.7.RELEASE

  • 步骤

    创建SpringBoot项目,并且导入相关依赖,具体POM文件如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <!-- spring security 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    

    创建Controller,具体如下:

    package com.briup.security.web;
    
    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("/hello")
        public String add() {
            return "hello security";
        }
    
    }
    

    由于SpringBoot项目默认使用8080端口,在这里修改其默认端口,如下:

    server.port=8081
    

    启动项目,并且访问地址:http://127.0.0.1:8081/test/hello

    发现地址自动跳转到了登录界面,进行授权认证

    image-20201221143409495

    只要导入Spring Security 依赖,那么默认就会有一个用户名为user,其密码为启动时打印出的加密字符串,如下

    image-20201221143518978

    将用户名,密码填充进去即可完成认证,如下

    image

    这样就完成了一个基本Spring Security的简单用户认证,实际项目中,用户名与密码肯定是要从数据库中查询出来,这里只是做一个简单的认证感受一下该框架的魅力

4.原理

思考:

==从上述的例子中,体验了Spring Security认证流程,那么它的原理到底是什么呢?==

Spring Security 是 基于 Servlet过滤器链进行安全认证的,如下:

image-20201221144756445

当客户端发送请求,那么过滤器就会把该请求拦截下来,进行校验,校验通过过滤器则放行,具体的过滤器如下:

image

从上图中可以看出当用户发送请求,首先经过了用户名密码校验过滤器,我们来看一下该过滤器的源码

UsernamePasswordAuthenticationFilter部分源码如下:

image-20201221145210665

从源码可以以下特点:

  • 先判断认证请求是否是Post请求,如果不是则抛出异常
  • 再获取用户名密码,进行校验,校验通过则把请求传入下一个过滤器

经过一系列的过滤器最终传入到ExceptionTranslationFilter,其源码如下:

image-20201221145829717

从图中源码可以看出,在该过滤器中主要是对异常进行处理,如果没有异常,过滤器则直接放行到下一个过滤器FilterSecurityInterceptor过滤器

FilterSecurityInterceptor 位于过滤器链的最底部,一个方法级别的过滤器,其源码如下:

image-20201221150115865
image-20201221150138195

从源码可以看出在请求放行之前需要先执行之前所有的过滤器,才会进行放行。

总结如下:

image-20201221151819316

5.加载

从上述中,知道Spring Security本质就是一个过滤器链,通过不同的过滤器组合使用从而实现认证与授权。

==那么这些过滤器是如何被加载的呢,与Spring容器又存在什么关系呢?==

Spring Security主要是用过DelegatingFilterProxy去管理过滤器实例。

当然该类也是一个过滤器,使用该类最大的好处就是可以通过Spring容器来管理 Servler Filter的生命周期

如果过滤器需要Spring容器中的实例,也可以直接注入

该类部分源码如下:

image-20201221153204453

在该类的源码中,发现在doFilter方法中会调用initDelegate方法,该方法源码如下:

image-20201221153434869

该方法的主要作用就是从Spring容器中拿到代理过滤器实例对象,当该方法执行完毕.

那么doFilter方法紧接着就会调用invokeDelegate,该方法的作用就是让代理过滤器(FilterChainProxy)去执行doFilter方法,其源码如下:

image-20201221154126973

从上图源码可知,在doFilter方法中调用了doFilterInternal方法,该方法源码如下:

image-20201221154303050

在源码中:

List<Filter> filters = getFilters(fwRequest);

该句代码的意思就是将Spring Security中所有的过滤器全部加载到过滤器链中。这样就把所有的过滤器加载进来了

总结:

image-20201221161002884

6 认证

6.1 简介

==思考:==

通过之前的HelloWorld例子知道用户名为user,密码则是启动时随机产生的一段加密字符串

但是在开发中,用户名密码都需要自定义或者从数据库表查询账号跟密码,那么这些在操作

SpringSecurity中如何实现?

实现上述问题一共有三种方式:

  • 通过配置文件
  • 通过配置类
  • 自定义实现类

接下来就让挨个来实现这三种方式

6.2 配置文件

  • 创建SpringBoot项目(spring-security-config-file),并且导入Spring Security依赖, pom.xml部分内容如下:

    <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    
  • 通过配置指定用户名密码

    server.port=8081
    spring.security.user.name=lisi
    spring.security.user.password=123456
    
  • 创建Controller,内容如下:

    package com.briup.security.web;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/file")
    public class ConfigFileController {
    
        @GetMapping("/hello")
        public String hello() {
            return "Hello Security";
        }
    }
    
    
  • 启动并且进行测试

    image-20201223155209867

    启动后并没有给我们产生密码,这是因为制定了用户名密码,所以就不会产生密码

    访问地址: http://127.0.0.1:8081/config/hello

    image-20201223155707855

    输入配置文件配置的用户名密码即可看到返回的结果,如下:

    image-20201223155752230

    注意:这种方式只能用在学习阶段,真正开发项目不会用这个

6.3 配置类

  • 创建SpringBoot项目(spring-security-config-class),并且导入Spring Security依赖, pom.xml部分内容如下:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    
  • 创建配置类SecurityConfig,内容如下:

    package com.briup.security.config;
    
    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.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     *  Security 配置类 必须继承  WebSecurityConfigurerAdapter
     *  同时必须加上 @Configuration注解
     */
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        /**
         * 重写该方法,并且通过 auth 参数设置用户名密码
         * @param auth
         * @throws Exception
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 对密码进行加密
            String password = passwordEncoder().encode("123456");
            // 设置用户名与密码 以及 角色 由于这里只是学习,没有用户名和密码,
            // 因此直接写死为admin
            auth.inMemoryAuthentication()
                .withUser("lisi")
                .password(password)
                .roles("adimin");
        }
    
        /**
         * 配置加密 解密实例
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    

    了解内容(start)

    PasswordEncoder接口 是 Security提供用来对密码进行加密的接口,源码如下:

    image-20201223161654828

    BCryptPasswordEncoder是该接口的实现类,使用算法将接口三个方法全部实现

    因此使用该实现类的实例就可以对密码进行加密

    了解内容(end)

  • 创建Controller,内容如下:

    package com.briup.security.web;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/class")
    public class ConfigFileController {
    
        @GetMapping("/hello")
        public String hello() {
            return "Hello Security";
        }
    }
    
    
  • 启动测试

    注意:将项目端口设置为8082

    访问地址:http://127.0.0.1:8082/class/hello

    image-20201223162746551

    输入配置类配置好的用户名密码即可看到返回的结果,如下:

    image-20201223162826317

    注意:这种方式在实际开发项目中也不会用到,只用作学习阶段

6.4 自定义

  • 简介

    实际开发中,更多的是用户名密码甚至包括角色是从数据库中查询出来,而且在登录的时候会有一些用户自定义的逻辑存在,例如 判断账号的状态等等

    但是上述两种方式,用户均不可以添加自定义逻辑,认证走的都是Security本身的那一套逻辑,因此急需要一套用户可以自己定义认证逻辑的流程。

    Spring Security中就要想自定义逻辑,只需要实现UserDetailsSerivice接口即可

  • 准备工作

    • 新建账号表,存储账号数据,以便认证时用户名密码从表中查询

      -- ----------------------------
      -- Table structure for account
      -- ----------------------------
      DROP TABLE IF EXISTS `account`;
      CREATE TABLE `account`  (
        `id` bigint(20) NOT NULL COMMENT '主键',
        `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
        `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
        PRIMARY KEY (`id`) USING BTREE
      ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
      
      -- ----------------------------
      -- Records of account
      -- ----------------------------
      INSERT INTO `account` VALUES (1, 'lisi', '123321');
      
    • 创建SpringBoot项目(spring-security-config-account),pom.xml内容如下:

      
      <dependencies>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-data-jpa</artifactId>
          </dependency>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-security</artifactId>
          </dependency>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
      
          <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
              <scope>runtime</scope>
          </dependency>
          <dependency>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
              <optional>true</optional>
          </dependency>
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-test</artifactId>
              <scope>test</scope>
              <exclusions>
                  <exclusion>
                      <groupId>org.junit.vintage</groupId>
                      <artifactId>junit-vintage-engine</artifactId>
                  </exclusion>
              </exclusions>
          </dependency>
          <dependency>
              <groupId>org.springframework.security</groupId>
              <artifactId>spring-security-test</artifactId>
              <scope>test</scope>
          </dependency>
      </dependencies>
      

      数据操作框架为Spring Data JPA

    • application.yml

      src/main/resources下新增application.yml,内容如下:

      server:
        port: 9999
      spring:
        datasource:
          driver-class-name: com.mysql.cj.jdbc.Driver
          # 数据库地址
          url: jdbc:mysql://172.16.0.154:3306/test?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
          username: root
          password: root
      
    • 启动类内容如下:

      @SpringBootApplication
      public class SpringSecurityConfigAccountApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(SpringSecurityConfigAccountApplication.class, args);
          }
      
      
          /*
              加密实例
          */
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      }
      
      
    • POJO类 内容如下:

      package com.briup.security.bean;
      
      import lombok.Data;
      
      import javax.persistence.Entity;
      import javax.persistence.Id;
      import javax.persistence.Table;
      import java.io.Serializable;
      
      @Data
      @Table(name = "account")
      @Entity
      public class Account implements Serializable {
          @Id
          private Long id;
          private String username;
          private String password;
      }
      
    • DAO层 内容如下

      package com.briup.security.dao;
      
      import com.briup.security.bean.Account;
      import org.springframework.data.jpa.repository.JpaRepository;
      
      public interface AccountDao extends JpaRepository<Account,Long> {
          Account  findByUsername(String username);
      }
      
      
  • 服务层开发

    要想让Spring Security走自定义登录逻辑流程,就只需要实现UserDetailsService接口,然后通过配置类进行指定即可。

    创建MyDetailService类,内容如下:

    package com.briup.security.service;
    
    import com.briup.security.bean.Account;
    import com.briup.security.dao.AccountDao;
    import org.springframework.beans.factory.annotation.Autowired;
    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.Objects;
    
    @Service("myDetailService")
    public class MyDetailService implements UserDetailsService {
    
        @Autowired
        private AccountDao accountDao;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Account account = accountDao.findByUsername(username);
            if (Objects.isNull(account)) {
                throw new UsernameNotFoundException("用户名不存在");
            }
            User user =
                    new User(account.getUsername(),
                            passwordEncoder.encode(account.getPassword()),
                            AuthorityUtils.createAuthorityList("admin"));
            return user;
        }
    }
    
    

    代码解释:

    image-20201228204643115

    之所以实现这个接口,是因为SpringSecurity默认走的登录逻辑流程就是UserDetailsService接口实现类对象的登录逻辑,从下图可以看出该接口的实现类有多个

    image-20201228204940508

    如果用户不实现该接口,那么登录逻辑默认就是其他实现类实例的登录逻辑

    image-20201228205131336

    返回的UserDetails,该接口主要包含一些用户信息,其部分源码如下:

    public interface UserDetails extends Serializable {}
      /**
           *  返回获取用户的所有权限
           */
          Collection<? extends GrantedAuthority> getAuthorities();
      
          /**
           * 返回取用户密码
           */
          String getPassword();
      
          /**
           * 返回获取
           */
          String getUsername();
      
          /**
           * 判断账户是否为过期
           */
          boolean isAccountNonExpired();
      
          /**
           * 判断账户是被否锁定
           */
          boolean isAccountNonLocked();
      
          /**
           * 凭证(密码) 是否过期
           */
          boolean isCredentialsNonExpired();
      
          /**
           * 账户是否禁用 
           */
          boolean isEnabled();
    }
      
    

    这是一个接口,因此最后返回其实现类对象User如下图:

    image-20201228210502115

    由于设计表时,并没有设计账户权限和是否过期等等,因此全部设置为null,权限集合为admin

    注意:虽然上图中的User对象,只设置了User对象的用户名,密码以及角色权限,但是查看其构造器源码,在源码中设置了帮助用户设置了其他权限,如下

    image-20201228210808083
  • 配置

    新建SecurityConfig配置类,内容如下:

    package com.briup.security.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("myDetailService")
        private UserDetailsService userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             * 设置认证逻辑为用户自定义认证逻辑
             * 设置密码加密处理器为 BCryptPasswordEncoder
             */
            auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
        }
    }
    
    
  • web层开发

    新建Controller,内容如下:

    package com.briup.security.web;
    
    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("/hello")
        public String hello() {
            return "hello security";
        }
    
    }
    
  • 测试

    image-20201229142451654
    image-20201229142604812

    当输入的账号与密码错误时,则直接报错

    image-20201229142642963
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容