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服务输出的结果,如下图: