SpringSecurity简单入门
1、SpringSecurity简介
Spring Security
是 Spring
家族中的一个安全管理框架,相比于另外一个安全框架 Shiro
,它提供了更丰富
的功能,社区资源也比 Shiro
丰富。
一般来说中大型的项目都是使用 SpringSecurity
来做安全框架,小项目使用 Shiro
的比较多,因为相比于
SpringSecurity
,Shiro
上手更加的简单。
一般 Web
应用需要进行认证和授权:
-
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。
-
授权:经过认证后判断当前用户是否有权限进行某个操作。
认证和授权也是 SpringSecurity
作为安全框架的核心功能。
2、快速入门
2.1 新建测试控制器
package com.example.springsecuritydemo1.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author test
*/
@RestController
public class HelloWorldController {
@GetMapping("hello/world")
public String helloWorld() {
return "hello world!";
}
}
2.2 pom依赖
在 SpringBoot
项目中使用 SpringSecurity
我们只需要引入依赖即可实现入门案例。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>SpringSecurityDemo1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringSecurityDemo1</name>
<description>SpringSecurityDemo1</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
</dependency>
<!-- 引入spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.3 使用
引入依赖后我们在尝试去访问接口就会自动跳转到一个 SpringSecurity
的默认登陆页面,默认用户名是
user
,密码会输出在控制台,必须登陆之后才能对接口进行访问。
# 控制台输出的密码
Using generated security password: 83cf6d36-d553-4c52-bd4b-f5a107a0a945
我们访问之前的接口需要输入用户名和密码才可以访问:
访问接口:
3、认证
3.1 登录校验流程
3.2 原理初探
想要知道如何实现自己的登陆流程就必须要先知道入门案例中 SpringSecurity
的流程。
3.2.1 SpringSecurity完整流程
SpringSecurity
的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,这里我们可以看看入门案
例中的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
-
UsernamePasswordAuthenticationFilter
:负责处理我们在登陆页面填写了用户名密码后的登陆请求,入门案例的认证工作主要有它负责。
-
ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
。 -
FilterSecuritylnterceptor
:负责权限校验的过滤器。
我们可以通过 Debug
查看当前系统中 SpringSecurity
过滤器链中有哪些过滤器及它们的顺序。
3.2.2 认证流程详解
概念速查:
-
Authentication
接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。 -
AuthenticationManager
接口:定义了认证Authentication
的方法 。 -
UserDetailsService
接口:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。
-
UserDetails
接口:提供核心用户信息,通过UserDetailsService
根据用户名获取处理的用户信息要封装成
UserDetails
对象返回,然后将这些信息封装到Authentication
对象中。
3.3 解决问题和思路分析
登录:
①、自定义登录接口
-
调用
ProviderManager
的方法进行认证,如果认证通过生成jwt
-
把用户信息存入
redis
中
②、自定义 UserDetailsService
- 在这个实现类中去查询数据库
校验:
①、定义 Jwt
认证过滤器
- 获取
token
- 解析
token
获取其中的userid
- 从
redis
中获取用户信息 - 存入
SecurityContextHolder
4、认证案例
4.1 pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>SpringSecurityDemo2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringSecurityDemo2</name>
<description>SpringSecurityDemo2</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
</dependency>
<!-- 引入spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 引入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.83</version>
</dependency>
<!-- 引入jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 引入lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.2 Redis相关配置
# Redis配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000ms
4.3 Redis配置类
package com.example.springsecuritydemo2.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author test
*/
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 定义Jackson2JsonRedisSerializer序列化对象
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会报异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 创建RedisTemplate<String, Object>对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringSerial = new StringRedisSerializer();
// redis key 序列化方式使用stringSerial
template.setKeySerializer(stringSerial);
// redis value 序列化方式使用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// redis hash key 序列化方式使用stringSerial
template.setHashKeySerializer(stringSerial);
// redis hash value 序列化方式使用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4.4 Redis工具类
package com.example.springsecuritydemo2.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* @author test
*/
@Component
public class RedisUtils {
private RedisTemplate<String, Object> redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 删除缓存
*
* @param key 可以传一个值或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
}
4.5 Jwt工具类
package com.example.springsecuritydemo2.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author test
*/
@Component
public class JwtTokenUtil {
/**
* 荷载claim的名称
*/
private static final String CLAIM_KEY_USERNAME = "sub";
/**
* 荷载的创建时间
*/
private static final String CLAIM_KEY_CREATED = "created";
/**
* jwt令牌的秘钥
*/
private final String secret = "yeb-secret";
/**
* jwt的实效时间
*/
private final Long expiration = 604800L;
/**
* 根据用户信息生成token
*
* @param username 用户名
* @return String
*/
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, username);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 根据荷载生成JWTToken
*
* @param claims 生成token的信息
* @return String
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.