SpringSecurity

Posted gh-xiaohe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity相关的知识,希望对你有一定的参考价值。

文章目录

😹 作者: gh-xiaohe
😻 gh-xiaohe的博客
😽 觉得博主文章写的不错的话,希望大家三连(✌关注,✌点赞,✌评论),多多支持一下!!!

🚏 SpringSecurity 认证and权限

🚀 一、入门项目

🚬 pom

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
            <!-- 引入security起步依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
</dependencies>

🚬 测试

Security 自带的登陆页面、自带退出http://localhost:8080/logout

🚄 二、认证

🚬 1、web登陆流程

缺点可以改进

  • 1、现在使用的是security自带的登陆页面,比较丑。 想换成自己项目的,优化的登录页。
  • 2、用户使用的是security给的用户名和密码。 想真实地去数据库里,tb_user获取真实的用户名和密码
  • 3、security自带的cookie\\session模式。 想自己生成jwt,无状态登陆。
  • 4、前端页面怎么携带jwt。 想请求头里带上。
  • 4、鉴权操作完全没有。 想鉴权做完善。

总而言之,自己的一些特定需求,都没有实现。

🚬 2、原理

springsecurity 就是通过一些过滤器、拦截器,实现登陆鉴权的流程的。

🚭 (1) springsecurity 登陆流程

springsecurity就是一个过滤器链,内置了关于springsecurity的16的过滤器。

注意:我只写出了几个核心过滤器,其他的如下图。

  • UsernamePasswordAuthenticationFilter 处理我们登陆页面输入的用户名和密码是否正确的过滤器。
  • ExceptionTranslationFilter 处理前面的几个过滤器中,有了问题,抛出错误,不让用户登录。
  • FilterSecurityInterceptor 经行一个权限校验的拦截器。

我们可以找到当前boot项目中的,所有有关security的过滤器链。

🚭 (2) 认证流程 再细化 UsernamePasswordAuthenticationFilter

debug UsernamePasswordAuthenticationFilter 运行机制

🚭 (3) 自定义登录

🚬 3、JWT

🚭 概念

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。无状态。

好处 不需要服务器端 存session。

特点 可以被看到,但是不能篡改,因为第三部分用了秘钥。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。asdf.asdf.asdf

头部(Header):头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

"typ":"JWT","alg":"HS256"

在头部指明了签名算法是HS256算法。 我们进行BASE64编码

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(playload):载荷就是存放有效信息的地方。

"sub":"1234567890","name":"itlils","admin":true,"age":18

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==

签证(signature):jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

hs256("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==",secret)

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=.JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJqYWNrJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

🚭 依赖

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>

🚭 JWT功能测试

  • 1、创建token
  • 2、解析token
  • 3、设置过期时间
  • 4、自定义claims
/**
     * 加密 and 解密
     */
    @Test
    public void test1()
        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("666")//设置id
                .setSubject("testJwt")//主题
                .setIssuedAt(new Date())//签发日期
                .signWith(SignatureAlgorithm.HS256, "1234565"); // 秘钥
        String jwt = jwtBuilder.compact();
        System.out.println("加密后" + jwt);

        System.out.println();
        // 解密
        String compactJwt= "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJ0ZXN0Snd0IiwiaWF0IjoxNjc4MTE1MDY1fQ.yWdBCJmgUGqYJlK0v4GRoLDENBpqJk2VVUOkI10KG8k";
        Claims claims = Jwts.parser().setSigningKey("1234565").parseClaimsJws(compactJwt).getBody();
        System.out.println("解密后" +claims);

        System.out.println();
    

    /**
     * 设置过期时间 超过时间不可以使用
     */
    @Test
    public void test2()
        // 当前时间
        long l = System.currentTimeMillis();
        System.out.println(l);
        Date date=new Date(l+10000);

        // 加密
        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("666")//设置id
                .setSubject("testJwt")//主题
                .setIssuedAt(new Date())//签发日期
                .setExpiration(date)//过期时间
                .signWith(SignatureAlgorithm.HS256, "itlils");
        String jwt = jwtBuilder.compact();
        System.out.println(jwt);

        try 
            Thread.sleep(15000);
         catch (InterruptedException e) 
            throw new RuntimeException(e);
        

        // 解密
        Claims itlils = Jwts.parser().setSigningKey("itlils").parseClaimsJws(jwt).getBody();
        System.out.println(itlils);
    

    /**
     * 自定义 claims
     *      刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
     */
    @Test
    public void test3()

        // 加密
        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("666")//设置id
                .setSubject("testJwt")//主题
                .setIssuedAt(new Date())//签发日期
                .claim("userId","123")
                .claim("name", "zhangsan")
                .signWith(SignatureAlgorithm.HS256, "itlils");
        String jwt = jwtBuilder.compact();
        System.out.println(jwt);

        // 解密
        Claims itlils = Jwts.parser().setSigningKey("itlils").parseClaimsJws(jwt).getBody();
        System.out.println(itlils);
    
	
    @Autowired
    PasswordEncoder passwordEncoder;

    /**
     * 加盐密码操作练习 BCryptPasswordEncoder
     */
    @Test
    public void test4()
        // 加密
        String encode1 = passwordEncoder.encode("123456");
        String encode2 = passwordEncoder.encode("123456");
        System.out.println("encode1 = " + encode1);
        System.out.println("encode2 = " + encode2);
        System.out.println(encode1 == encode2); // false 说明:输入两次相同的密码但是加密出来的密码是不一样的

        boolean matches1 = passwordEncoder.matches( "123456",encode1);
        System.out.println("matches1 = " + matches1);
        boolean matches2 = passwordEncoder.matches( "123456",encode2);
        System.out.println("matches2 = " + matches2);

        // 解密
    

🚬 4、JWT功能实现准备

🚭 ①添加依赖

        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

🚭 ② 添加Redis相关配置

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 * 
 * @author itlils
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>


    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    

    public FastJsonRedisSerializer(Class<T> clazz)
    
        super();
        this.clazz = clazz;
    

    @Override
    public byte[] serialize(T t) throws SerializationException
    
        if (t == null)
        
            return new byte[0];
        
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    
        if (bytes == null || bytes.length <= 0)
        
            return null;
        
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    


    protected JavaType getJavaType(Class<?> clazz)
    
        return TypeFactory.defaultInstance().constructType(clazz);
    

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig 

    @Bean
    @SuppressWarnings(value =  "unchecked", "rawtypes" )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    

🚭 ③ 响应类

import com.fasterxml.jackson.annotation.JsonInclude;

// 标签去除json数据中的空值
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> 
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息,如果有错误时,前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据,
     */
    private T data;

    public ResponseResult(Integer code, String msg) 
        this.code = code;
        this.msg = msg;
    

    public ResponseResult(Integer code
        
                

一、Spring Security的理解

  • Spring Security最核心的是过滤器链,也就是一组过滤器(Filter),所有的访问服务的请求都会经过spring security的过滤器,服务的响应也会经过spring security的过滤器再返回给客户端,并且这些过滤器在系统启动时springboot会自动都配置完成。

二、Spring Security基本原理

1、Spring Security基本原理图解


2、spring security核心过滤器

(1)、UsernamePasswordAuthenticationFilter过滤器

  • 主要用于处理formLogin表单登录。
  • 先检查请求是否是登录请求,并且检查登录请求中是否带有用户名和密码,
  • 如果有,这个过滤器就会尝试用这个用户名和密码进行登录,
  • 如果没有,就回放过该请求,交给下一个过滤器处理

(2)、BasicAuthenticationFilter过滤器

  • 主要用于处理httpBasic登录
  • 先检查请求头中是否有basic开头Authentication的信息,
  • 如果有,会尝试进行base64解码,然后取出用户名和密码,尝试进行登录
  • 如果没有,就回放过该请求,交给下一个过滤器处理

(3)、ExceptionTranslationFilter过滤器

  • 用于捕获FilterSecurityInterceptor过滤器抛出的异常。
  • ExceptionTranslationFilter 位于FilterSecurityInterceptor过滤器之前的位置。

(4)、FilterSecurityInterceptor过滤器

  • 此过滤器是Spring Security的最后一个 Filter,获取当前 request 对应的权限配置,调用访问控制器进行鉴权操作。
  • 以上任何一个过滤器成功完成登录后,会在请求上做一个标记,标记认证成功,最后会到FilterSecurityInterceptor过滤器
  • FilterSecurityInterceptor是整个spring security过滤器链的最后一环
  • FilterSecurityInterceptor后就是我们自己写的controller控制层的REST服务
  • FilterSecurityInterceptor决定当前的请求能否访问我们写的REST服务

3、FilterSecurityInterceptor依据什么判断当前的请求能否访问我们写的REST服务

  • 根据代码中的配置,即自定义的SecurityConfig配置类中继承WebSecurityConfigurerAdapter类并重写的configure方法中的内容
  • configure方法中的内容最终都会放到FilterSecurityInterceptor过滤器中,判断的结果是通过还是不通过
  • 如果通过,会跳访问我们写的REST服务
  • 如果不通过,会根据具体不能访问的原因抛出对应的异常

4、FilterSecurityInterceptor抛出的异常如何解决

  • 通过ExceptionTranslationFilter过滤器来捕获FilterSecurityInterceptor过滤器抛出的异常。
  • ExceptionTranslationFilter 位于FilterSecurityInterceptor过滤器之前的位置。

5、总结

  • 以上就是spring security最核心的基本原理,spring security提供的所有的功能、特性都是建立在此过滤器链上。
  • 比如微信登录、qq登录、短信验证码登录都是在此过滤器链上添加过滤器,来支持不同的身份认证方式
  • 在程序实际运行时候,过滤器链上的过滤器不止UsernamePasswordAuthenticationFiletr、BasicAuthenticationFilter、ExceptionTranslationFilter、FilterSecurityInterceptor这四种过滤器,还有其他的过滤器。

三、Spring Security源码解析

1、Spring Security过滤器源码

  • UsernamePasswordAuthenticationFilter过滤器源码解析

  • ExceptionTranslationFilter过滤器源码解析

  • FilterSecurityInterceptor过滤器源码解析

2、启动springboot项目,发送请求,查看过滤器执行顺序

  • 创建一个Security配置类,formLogin表单登录的配置,如下图:

  • 启动项目,如下图

  • 浏览器执行查询请求,如下图:

  • 由下图可知,因为不是表单登录,直接执行FilterSecurityInterceptor过滤器,然后执行 ExceptionTranslationFilter过滤器处理FilterSecurityInterceptor过滤器抛出的异常。

  • 然后跳转到SpringSecurity表单登录页面,如下图:

  • 由下图可知,在SpringSecurity表单登录页面输入用户名和密码,点击登录,先执行UsernamePasswordAuthenticationFilter过滤器,然后执行FilterSecurityInterceptor过滤器,

  • 最后输入rest服务输出的结果,如下图:

以上是关于SpringSecurity的主要内容,如果未能解决你的问题,请参考以下文章

使用 Spring Security Java 配置时禁用基本身份验证

LDAP 的 Spring Security Java 配置

Tomcat重启后基于spring security java的配置

具有角色的经过身份验证的用户的 Spring Security Java 配置

Spring Security Java 配置 IS_AUTHENTICATED_FULLY

基于 Spring Security Java 的配置不起作用。它会一直显示 index.jsp 页面