本章我们会使用SpringSecurity+JWT来实现认证权限,如果有什么不懂的问题可以给我发私信
什么是JWT?
JWT(JSON Web Token)是一种用于在网络应用中传输信息的开放标准(RFC 7519)。它通常被用于对用户进行身份验证和授权。JWT由三部分组成,每个部分之间使用点(.)进行分隔,这三部分分别是:
Header(头部): 包含了令牌的元数据和加密算法信息。
Payload(载荷): 存储了要传输的数据,如用户的身份信息和一些声明。Payload有三种类型:注册的声明(Reserved claims)、公共的声明(Public claims)和私有的声明(Private claims)。
Signature(签名): 使用头部指定的加密算法对头部和载荷进行签名,以确保令牌在传输过程中没有被篡改
为什么要使用JWT?
1.跨平台和语言:JWT是基于JSON的标准,因此它在不同的编程语言和平台之间都可以轻松传递和解析。
2.状态无关性:传统的会话认证在服务端需要保存用户的会话状态,而JWT是无状态的,所有信息都被包含在令牌本身中,这使得服务端不需要保存任何状态信息,从而降低了服务端的负担。
3.安全性:JWT的签名保证了令牌的完整性和真实性,确保信息不会在传输过程中被篡改或伪造。同时,由于JWT是基于标准的,可以使用加密算法来保护敏感信息,确保令牌只能被可信的接收方解密。
4.扩展性:由于JWT允许在Payload中添加自定义的声明,因此可以在令牌中携带更多的用户信息和相关权限,满足不同应用的需求。
底层原理:
JWT的底层原理主要是基于数字签名和加密算法。当用户进行身份认证时,服务端验证用户的身份,如果验证成功,服务端会生成一个JWT并将其返回给客户端。客户端在后续的请求中通过将JWT放在请求的头部(通常是Authorization头部)或其他方式进行传递。服务端在接收到请求后,会解析JWT,并验证其签名的有效性,从而确定用户的身份和权限。
签名验证的过程如下:
服务端收到请求后,从请求头部(或其他指定位置)获取JWT。
使用相同的密钥和算法,对JWT中的头部和载荷进行签名计算。
将计算得到的签名与JWT中的签名部分进行比较,如果一致,则说明JWT没有被篡改。
这样,服务端可以确信JWT的内容是可信的,因为只有拥有正确密钥的服务端才能生成有效的签名。同时,由于JWT中包含了用户信息和权限声明,服务端可以通过解析JWT获取到这些信息,无需再进行数据库查询,从而提高了性能
我们使用JWT来完成这个用户是否已经登录,一般都会放到请求头内
1、认证过程
我们如果使用前后端分离存在跨域问题,跨域的问题是如果ip地址不一样或者端口号不一样或者是协议不一样都存在跨域问题.
假如我们的后端是http://127.0.0.1:8080 意思就是我们是http协议127.0.0.1是本地IP地址,8080是我的端口号
而前端我们使用vue的话是其他的端口号这就是跨域,是浏览器为了防止某些方面而阻止的
我们上节讲过如果后端请求没有登陆的话就跳转到登陆页面,而这个登陆页面不存在跨域问题,所以我们前后端分离的话认证是需要告诉前端然后让前端去跳转登陆页,我们实现这些我们需要知道springsecurity的认证流程
其实我们的发出的请求都会经过springsecurity的一条过滤器链,springsecurity本身就是一条过滤链,这里我们只标出来最重要的三个
UsernamePasswordAuthenticationFilter: 负责处理我们在登录页面填写的用户名密码后的登录请求,认证工作主要是有它负责
ExceptionTranslationFilter:它最主要用于捕获过滤器链异常
FilterSecurityInterceptor:我们的权限是在这里来实现的 负责权限校验的过滤器
这里看不懂我给大家讲解一下,首先我们有一个请求过来,比如我们有一个请求 /test 我们先需要进行这一系列的过滤器链之后才会访问到我们/test的具体流程,我们现在要进行的是认证,所以大家应该要知道,我们的认证具体是在哪个过滤器实现的 UsernamePasswordAuthenticationFilter
我们来看一下UsernamePasswordAuthenticationFilter具体是怎么实现的认证
解释:重要
1.UsernamePasswordAuthenticationFilter实现了AbstractAuthenticationProcessingFilter这个类,我们的用户名密码会从先进入这个过滤器内
2.在这个类中有两个重要的点:第一点,将用户名和密码封装到了Authentication对象中,现在我们想象只是把数据放入了一个实体类中而已,这个实体类中还需要填写权限信息,但现在是空的,第二点,调用了一个放啊authenticate方法,这个方法是AuthenticationManager的
3.AuthenticationManager的实现类调用了DaoAuthenticationProvider的authenticate方法进行认证
4.DaoAuthenticationProvider调用了 inMemoryUserDetailsManager的loadUserByUsername方法查询用户,这里是在内存中查询的,对我们来说非常占用资源
5.根据用户名查询到这个用户以及它对应的权限,把对应的用户信息包含权限信息封装成UserDetail对象返回给DaoAuthenticationProvider
6.在DaoAuthenticationProvider中又对UserDetail内我们在内存中查询到了密码与输入的密码使用passwordEncoder做对比
7.因为我们的Authentication对象中到现在是没有权限信息的,在UserDetail内放着我们的权限信息,所以如果前面的密码对比成功,就会将UserDetail内的权限信息存放在Authentication对象
注意: 记住这个流程,我们是需要通过接口自己定义的,因为他是从内存中查询的,我们是需要用重新实现UserDetailService接口
2、准备工作
依赖
security,redis ,mybatis ,jwt,这里我们使用redis,如果没有学可以使用我们的mysql
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--security安全认证-->
<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>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!--登录成功之后生产一个令牌,每次访问页面需要带着这个令牌,令牌失效之后重新登录
避免每次访问数据库-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
redis配置文件
@SuppressWarnings("all")
@Configuration
public class RedisConfig {
// 自己定义了一个 RedisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// // 我们为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
// template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
// template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
springboot核心配置文件
spring:
datasource: #连接数据库
url: jdbc:mysql://localhost:3306/homework?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
server:
port: 8082
mybatis:
configuration:
#log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #控制台打印
map-underscore-to-camel-case: true #数据库的表字段是 create_time 到java可以转成createTime
type-aliases-package: com.aaa.entity #起别名
mapper-locations: classpath:mapper/*.xml #xml的路径
jwt的工具类
jwt的工具类,我们不需要知道是如何进行运算的,记住我们jwt是有三要素的,但是不能填写敏感数据,比如密码
public class JwtUtil {
//有效时间为
private static final Date JWT_EXPIRE =new
Date(System.currentTimeMillis()+1000*60*60);// 60*60*1000 一个小时
//设置密钥签名
private static final String JWT_KEY = "xuecheng";
//生成UUID
public static String getUUID(){
String s = UUID.randomUUID().toString().replaceAll("-", "");
return s;
}
/**
* String 生成jwt
*/
public static String createJWT(String claims){
/**
* 1.jwt创建一个id是随机生成的不会重复的id,添加载荷也就是我们的数据,添加过期时间,添加密钥(前面)
*/
String token = JWT.create().withKeyId(getUUID()).withClaim("user", claims)//添加载荷
.withExpiresAt(JWT_EXPIRE)//添加过期时间
.sign(Algorithm.HMAC256(JWT_KEY)); // 使用指定算法和密钥对JWT进行签名
return token;
}
/**
* 解密jwt
*/
public static Claim parseJWT(String token){
/**
* 验证JWT Token并解析其内容。
*
* @param token 待验证和解析的JWT Token。
* @return 解析后的JWT内容,存储在Map中。
*/
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(JWT_KEY)).build();
// 使用JWTVerifier验证token的有效性,并返回一个解析后的DecodedJWT对象
DecodedJWT verify = jwtVerifier.verify(token);
Map<String, Claim> claims = verify.getClaims();
// 从claims中获取名为"user"的claim
Claim user = claims.get("user");
return user;
}
public static void main(String[] args) {
String jwt = createJWT("123");
System.out.println(jwt);
Claim claim = parseJWT(jwt);
System.out.println(claim);
}
}
redis的工具类
redis的工具类,这里如果你使用的mysql是不需要的
package com.aaa.uitls;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @program: redis-02-springboot
* @description: 封装我们自己的redis工具类
* @author: xc
* @create: 2024-05-11 16:05
**/
@SuppressWarnings("all")
@Component
public class RedisUtil {
@Autowired
@Qualifier("redisTemplate")
RedisTemplate redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key,long time){
try{
if(time>0){
redisTemplate.expire(key,time, TimeUnit.SECONDS);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKay(String key){
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @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) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
controller
用于以后的测试
@RestController
public class DemoController {
@GetMapping("hello")
public String demo(){
return "hello,Spring Security!!";
}
@GetMapping("aaa")
public String aaa(){
return "aaa";
}
}
数据库
这里我们有一条数据,我们密码是加密之后的,密码是123,我们一会会讲解
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `sys_user`
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('2', 'niuer', '$2a$10$kN3z01SVV73V0ZiKvHceHOAVjgGJXCfu64sWNnUZqr0xsHPAL8Hc6');
实体类
我们实现了Serializable是为了序列化,以后会讲解
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
//主键
private Long id;
//用户名
private String username;
//密码
private String password;
}
3、实现认证
接下来开始使用进入我们的正题
首先我们在认证过程中说过,我们的UserDetailService的实现类inMemoryUserDetailsManager是在内存中根据用户名来查找的,对我们来说是不介意使用的,所以我们需要使用自己定义的类来实现根据用户名从数据库查询
UserDetailService
我们实现的类 ,我们可以看到与我们当时分析的流程图一样,他是需要返回一个UserDetails对象的,这里我们是现在没有给任何权限,因为另一个在内存中根据名字查询还查询到了权限信息,这里我们先不做任何对权限的操作,而我们的LoginUser是我们重新写的一个实体类
mapper
public interface UserMapper {
@Select("select *from sys_user where username=#{username}")
User queryByName(String username);
}
/**
* @program: springsecurity02
* @description: 实现UserDetailService接口
* @author: xc
* @create: 2024-05-31 10:00
* 因为这个接口本来实现的是在内存中查询,根据用户名查询对应的用户以及这个用户的对应信息的权限
* 所以我们这里需要实现这个接口,然后去数据库查询,根据用户名查询对应的用户以及这个用户的对应信息的权限
* 底层实现的是把对应的用户信息包括权限信息封装成UserDetails对象返回
**/
@SuppressWarnings("all")
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//注意如果数据库内是明文的话,需要我们在明文前加上{noop}声明他是明文
User user = userMapper.queryByName(username);
//这个会被security的异常处理器捕获到
if(StringUtils.isEmpty(user)){
throw new RuntimeException("用户名或者密码错误");
}
//权限信息
//……
return new LoginUser(user);
}
}
LoginUser
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//权限信息
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
写到这里我们就需要配置我们的Security的配置,将在内存中查询的进行覆盖,我的不需要放入就可以被覆盖,这里我们也对我们的密码进行加密的方式,因为我们将数据通DaoAuthenticationProvider方法,PasswordEncoder对比UserDetail对象中的密码和Authentication对象中的密码是否一样,如果一样就把UserDetail对象中的权限信息写入Authentication对象中
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
/**
* 因为我们使用的是BCryptPasswordEncoder,所以这里需要重写密码加密方式,
* Security的默认加密方式不是这一种类型,我们在数据库中存储的密码是加密后的,所以这里需要重写密码加密方式
* @return
* 这里我们的解密方式是BCryptPasswordEncoder,当我们输入的密码和数据库中存储的密码进行比较时,
* 会自动调用BCryptPasswordEncoder的matches方法进行解密,然后进行比较
*/
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//存入我们写的,覆盖掉从内存中查找的
http.userDetailsService(userDetailsService);
}
}
然后我们现在就可以启动我们的springboot项目,可以发现输入用户名:niuer,密码:123就登录成功了,但是前后端分离仅仅是这样还是不够的,这就是用到我们的jwt令牌了,那应该怎么用呢?
登录实现
首先我们需要知道
登录流程: 前端携带用户名密码访问/login登录接口,会拿着密码和用户名先去Security的过滤器走一圈到数据库如果正确返回给服务器,返回给服务器肯定是正确的,我们生成jwt令牌用于验证身份,登录后访问其他请求需要在请求头中携带token,每次都验证一下是什么身份,在后端获得请求头中的token进行解析,取出数据,如果有权限就让他访问
首先我们先定义一个请求,我们拿着数据调用业务层的代码
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
//我们使用post请求是将我们的数据放入请求的方法体中
@PostMapping("/user/login")
public Result login(@RequestBody User user){
return loginService.login(user);
}
}
业务层
因为我们知道这个认证的流程是调用了AuthenticationManager的认证方法才调用到我们的UserDetailService的根据用户名查询的方法,所以这一步我们可以获得我们的认证实体,到这一步
到这一步之后用户名与密码都已经认证好了,都Authentication对象中,然后将其放入我们的reids中,我们是根据用户名的id,将信息存放在了redis中,而我们的AuthenticationManager在哪来呢,在我们的Security的配置文件中,官方已经写好了,我们只需要放入bean容易即可
@Bean //创建一个authenticationManager管理器
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
RedisUtil redisUtil;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public Result login(User user) {
//使用AuthenticationManager authenticate 方法进行用户验证
/**
* 底层是UsernamePasswordAuthenticationFilter 调用了ProviderManager这个类是AuthenticationManager的实现类,
* 我们需要再写一个用来认证生成token
*/
/** authenticationManager.authenticate() 认证
* 会调用DaoAuthenticationProvider 进行认证,然后调用我们的UserDetailsServiceImpl,一个一个返回
* 它这个方法里面是需要传入一个Authentication对象,这个对象里面封装了用户名,密码,权限等,然后返回一个Authentication对象,
* 一般我们使用这个UsernamePasswordAuthenticationToken
*/
Authentication authenticationToken=new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果为空说明认证失败
if(Objects.isNull(authenticate)){
throw new RuntimeException("认证失败");
}
//获取这个用户的id 因为我们是LoginUser对象内封装了User对象
LoginUser principal = (LoginUser) authenticate.getPrincipal();//获得这个实体
String id = principal.getUser().getId().toString();
//存放redis中
redisUtil.set("login:"+id,principal);
//生成token令牌
String jwt = JwtUtil.createJWT(id);
Map<String,String> map=new HashMap<>();
map.put("token",jwt);
return new Result(200,"登录成功",map);
}
}
效果
登录成功后会给前台传递一个令牌,意思就是给他一个身份,每次请求的话都要携带这个身份令牌才可以
拦截器
每次我们都会访问我们的一个过滤来进行认证,所以我们需要自己定义一个拦截器,如果他的请求头里面有可以验证身份的令牌,我直接就让你走,然后没有身份令牌,你先去认证再来
这个拦截器是每个请求只会进来一次,首先我们先从请求头内拿到我们的身份令牌,如果没有的话,行!没有就给他放行,虽然放行了,但是会被我们的security的拦截,会显示403,让他去认证
下一个情况就是存在我们的令牌,但是可能已经过期了,也可能是错误的,进行判断,直接抛异常,这里直接抛异常是不可以的,我们在下面会讲到我们的异常处理器
再下一个就说明我们的令牌是没有问题的,从redis中获取到我们对应的信息,将我们的信息封装到
Authentication对象中,放行说明我们是一个已经认证的状态
//这个过滤器每个请求只会经过一个
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//我们的token是存放在请求头中的
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){//请求头没有就放行
filterChain.doFilter(request,response);
return;
}
//解析token
String userId =null;
try{
Claim claim = JwtUtil.parseJWT(token);
userId=claim.asString();
}catch (Exception e){
throw new RuntimeException("token非法");
}
//从redis中获取
String key="login:"+userId;
LoginUser user = (LoginUser) redisUtil.get(key);
if(Objects.isNull(user)){
throw new RuntimeException("用户未登录");
}
System.out.println(user.getAuthorities());
//存入SecurityContextHolder中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request,response);
}
}
我们的这个拦截器是需要在认证之前就要完成的,如果有身份令牌是不需要再进行认证的,所以我们需要加入我们相对于的配置
在我们的Security配置文件中的configure(HttpSecurity http)方法中
//指定我们的过滤器放在什么位置
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
我们先在Security的配置文件中配置一些东西
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//关闭csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session,不通过session获取SecurityContext
.and()
.authorizeRequests().antMatchers("/user/login").anonymous()///user/login匿名能访问,意思就是只有未登录的可以访问,登录的无法访问
.anyRequest().authenticated(); //除了上面的路径都不能访问
http.userDetailsService(userDetailsService);
//指定我们的过滤器放在什么位置
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
演示效果
首先我们先登录获取一个身份令牌
我们再去访问我们自己定义的 /hello路径,我们是会报403的没有权限访问,因为你什么身份都不是,我们在请求头中存放了我们的身份令牌,可以进行访问,这就是认证
4、权限
首页我们需要知道什么是权限,权限是在认证的基础上实现的,第一步当用户登录之后他会有一个身份,这个身份所能使用的功能就是权限
举例: 一个借阅人一个图书馆管理员,他们两个身份不同,使用的操作不一样,这个身份是我们登录的时候就给我们的
UserDetailService
当然大家可能会说,用户没有权限,我就不给他在前端显示就行了,但是这些并没有进行拦截操作,如果他知道我们的路径也是可以直接操作的 我们的全是是在UserDetailService接口实现类给我们的,我们先不从数据库内查,先写一个死的,说明我们每个登录进来的用户都有这个角色,我们查询到的用户信息以及权限信息都会第一开始存放在UserDetails的实体类中
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//注意如果数据库内是明文的话,需要我们在明文前加上{noop}声明他是明文
User user = userMapper.queryByName(username);
//这个会被security的异常处理器捕获到
if(StringUtils.isEmpty(user)){
throw new RuntimeException("用户名或者密码错误");
}
List<String> list = new ArrayList<>();
list.add("ROLE_ADMIN");
list.add("ROLE_TEST");
//根据用户查询到权限信息
return new LoginUser(user,permsByUserId);
}
UserDetail
这里我们将我们的权限信息,放入了实体类中,我们重写了UserDetails的方法,关于权限的信息就是,getAuthorities方法,获取权限信息,这里返回的是一个集合并且是基础GrantedAuthority的,到这一步我们以及将权限信息存入了我们的UserDetails的实现类当中了
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
@JSONField(serialize = false) //不让redis进行序列化
private List<SimpleGrantedAuthority> authorities;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities!=null){
return authorities;
}
authorities=new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
自定义过滤器
当然我们每次都会先走一下我们的自己定义的过滤器,我们当时写的是如果都身份令牌没有问题,并且能在redis中查询到,就存放到Security上下文当中,但是我们只是存放了用户名密码,并没有存放身份,这里我们存放一下我们的身份信息
//我们的token是存放在请求头中的
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){//请求头没有就放行
filterChain.doFilter(request,response);
return;
}
//解析token
String userId =null;
try{
Claim claim = JwtUtil.parseJWT(token);
userId=claim.asString();
}catch (Exception e){
throw new RuntimeException("token非法");
}
//从redis中获取
String key="login:"+userId;
LoginUser user = (LoginUser) redisUtil.get(key);
if(Objects.isNull(user)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder中
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request,response);
我们的权限是需要指定,我们的哪个角色能访问哪些资源,我们这里都没有写到,我建议大家使用注解的方式去完成,在我们的Security的配置文件中使@EnableGlobalMethodSecurity(prePostEnabled = true) 意思就是开启注解权限支持,在接口上使用这个注解,说明我们的ROLE_Test用户才能访问这个接口,hasAnyRoles是一个方法,可以理解为调用了这个方法,为什么不用写前缀??Role_?
因为他会自动拼接,如果你在这里写ROLE_TEST,那你这辈子都访问不了这个路径
@GetMapping("hello")
@PreAuthorize("hasAnyRole('TEST')") //只有test角色可以访问
public String demo(){
return "hello,Spring Security!!";
}
演示效果(1)
第一步就是登录了,我们登录之后,他知道我们是TEST的角色,再去访问我们的 /hello
这里我们的这个路径会先被我们自定义的过滤器拦截,虽然拦截了,但是我们给他了用户名账号密码,以及我们的身份,所以可以访问
RBAC权限模型
当我们将权限改为TEST1时就会报403权限不足,这里就不演示了,当然我们每个人都有这个权限是不行的,我们不能写成死的,在我们的UserDetailService接口的实现类中,需要从数据库内查询到,而权限模型是一种形式,不是一个第三方的工具,是一个思想
解释: A 用户有三种权限 增修查, B 用户有四种权限 增删改,一对多的关系,但是我们在中间有一层角色表, A充当经理的角色, 那经理这个角色有增修查,B充当董事长的权限,那么董事长有增删改查的权限,意思就是角色代表着一个用户组
数据表
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `sys_menu`
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`path` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '路由地址',
`compent` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '组件路径',
`visible` char(1) COLLATE utf8_unicode_ci DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) COLLATE utf8_unicode_ci NOT NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '权限标识',
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0没删除,1已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='菜单表';
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES ('1', '部门管理', 'dept', 'system/dept/index', '0', '0', 'ROLE_system:dept:list', '0');
INSERT INTO `sys_menu` VALUES ('2', '测试', 'test', 'system/test/list', '0', '0', 'ROLE_system:test:list', '0');
-- ----------------------------
-- Table structure for `sys_role`
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`role_key` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`status` char(1) COLLATE utf8_unicode_ci DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'CEO', 'ceo', '0');
INSERT INTO `sys_role` VALUES ('2', 'Coder', 'coder', '0');
-- ----------------------------
-- Table structure for `sys_role_menu`
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`role_id` bigint(20) NOT NULL,
`menu_id` bigint(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES ('1', '1');
INSERT INTO `sys_role_menu` VALUES ('1', '2');
-- ----------------------------
-- Table structure for `sys_user`
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('2', 'niuer', '$2a$10$kN3z01SVV73V0ZiKvHceHOAVjgGJXCfu64sWNnUZqr0xsHPAL8Hc6');
-- ----------------------------
-- Table structure for `sys_user_role`
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`),
KEY `asdasd1` (`role_id`),
CONSTRAINT `asdasd1` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `sdas1` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='角色表';
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('2', '1');
首先我们需要知道熟悉表,sys_user 与sys_role 是==> sys_user_role,其他三个表也是,我们现在要做的操作是根据用户的id,查询到他对应的权限,并且状态是0
SELECT DISTINCT m.perms FROM sys_user_role ur LEFT JOIN sys_role r ON ur.role_id=r.id
LEFT JOIN sys_role_menu rm ON ur.role_id=rm.role_id
LEFT JOIN sys_menu m on m.id=rm.menu_id
where user_id=2 and r.status=0 and m.`status`=0
在我们的mapper文件中编写 将user_id的值写成#{}
在我们的UserDetailService中,从数据库中查询到权限信息,封装到UserDetail类中
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//注意如果数据库内是明文的话,需要我们在明文前加上{noop}声明他是明文
User user = userMapper.queryByName(username);
//这个会被security的异常处理器捕获到
if(StringUtils.isEmpty(user)){
throw new RuntimeException("用户名或者密码错误");
}
//权限调用数据的方法进行查询
List<String> permsByUserId = userMapper.getPermsByUserId(user.getId());
// List<String> list = new ArrayList<>();
// list.add("ROLE_ADMIN");
// list.add("ROLE_TEST");
//根据用户查询到权限信息
return new LoginUser(user,permsByUserId);
}
演示效果(2)
和刚才一样……
5、扩展
注销.也就是把reids中的数据删除, 因为我们的请求到我们自定义的拦截器上会先对身份令牌做一系列的判断,然后从reids中取,没有取到也是因为账号没登录,所以我们只需要将redis中的数据删除就可以了
@Override
public Result logout() {
//先拿到认证的对象,因为我们先经过wtAuthenticationTokenFilter这个过滤器
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User principal = (User) authentication.getPrincipal();
redisUtil.del("login:"+principal.getId());
return Result.success("退出成功",null);
}
当我们认证失败的时候或者权限不足的时候要么抛异常,要么就403,这样对于我们的用户是非常不友好的,我们就需要使用到我们处理器,
AuthenticationEntryPoint: 当认证失败的时候,会被Security的异常过滤器发现,调用AuthenticationEntryPoint当中的commence方法,所以我们只需要实现这个类,让Security调用我们的类,就可以实现
AccessDeniedHandler: 这个异常处理器是专门用来处理授权失败的403
认证失败
这里我们使用了HttpStatus的枚举类,设置一个状态,Result是我们自定义的实体类
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Result result = new Result(HttpStatus.UNAUTHORIZED.value(), "认证失败", null);
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(result);
}
}
授权失败
@Component //Security有一个异常处理器,这个异常处理器是专门用来处理授权失败的403
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = new Result(HttpStatus.FORBIDDEN.value(), "权限不足", null);
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(result);
}
}
虽然我们定义过之后,但是我们的SpringSecurity是不知道的,我们需要将原来默认的给覆盖掉
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启注解权限支持
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired //认证失败处理器
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Bean //创建一个authenticationManager管理器
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//关闭csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session,不通过session获取SecurityContext
.and()
.authorizeRequests().antMatchers("/user/login").anonymous()///user/login匿名能访问,意思就是只有未登录的可以访问,登录的无法访问
.anyRequest().authenticated(); //除了上面的路径都不能访问
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.userDetailsService(userDetailsService);
//指定我们的过滤器放在什么位置
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//指定我们异常处理器的调用
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
}
}
测试
要注意的是他们只是在认证或者授权的过程中,如果在项目其他编写外是不会的
权限不足
认证失败
其他的还有我们的
AuthenticationFailureHandler:认证失败的处理器,是我们没有报异常的时候AuthenticationSuccessHandler:认证成功的处理器
LogoutSuccessHandler: 注销成功的处理器