当前位置: 首页 > news >正文

Day118 Spring Security

Spring Security

https://gitee.com/peng-chuanbin/framework-learn.git

SpringSecurity:保护JavaWeb网站安全的框架

环境需求:

JDK1.8

SpringBoot2.7.6 版本下的 SpringSecurity版本为 5.7.5

阿里云脚手架

1.SpringSecurity结构(内部原理)

1.1 JavaWeb 内部结构

客户端(浏览器或者其他APP)发送请求,先经过多个过滤器,然后最后交给Servlet进行处理

在这里插入图片描述

1.2 SpringSecurity 内部结构

SpringSecurity的核心其实就是Filter

SecurityFilterChain 是SpringSecurity的默认过滤器链,这个过滤器链提供了拦截规则以及登录逻辑

在这里插入图片描述

2.SpringSecurity内置案例

官方内置了一套简单的验证逻辑,自带登录页面(登录功能)和注销以及内置账户和密码,方便开发者快速入门

2.1 环境准备

1.创建SpringBoot项目,添加依赖

SpringBoot版本采用的 2.7.6,Java开发环境基于JDK1.8

   <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- SpringSecurity 测试依赖 --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>

2.项目创建完成后,添加测试代码 HelloController.java,运行项目
通过浏览器 http://localhost:1002/FrameworkLearn/test1或者test2,查看效果

package com.learn.frameworklearn.controller;@RestController
public class HelloController {//测试方法1  匿名访问@GetMapping(value = "/test1")public Result test1(){return Result.success("test1");}//测试方法2  认证后才能访问@GetMapping(value = "/test2")public Result test2(){return Result.success("test2");}}

在这里插入图片描述

用户名:user

密码:控制台

在这里插入图片描述

注意:如果需要放开security拦截,不走security内置登录界面

spring:autoconfigure:exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

2.2 内置案例详细介绍

SpringSecurity内置案例核心API

InMemoryUserDetailsManager:内置账户信息管理类,是UserDetailsService的子类

UserDetailsService:SpringSecurity用户信息管理类的核心接口,管理用户信息来源(数据库还是内存以及其他…)

UserDetails:SpringSecurity封装用户信息的核心接口,给SpringSecurity送用户信息时,SpringSecurity只认UserDetails

2.3 修改内置的用户名和密码

配置文件方式

spring:security:user:name: adminpassword: 123

3.替换系统自带用户名和密码的获取方式

3.1 认证的运行原理

在这里插入图片描述

3.2 替换默认生成的用户信息

不修改前端,只修改后端,从数据库拿账号密码(从数据库中获取账号密码传递给SpringSecurity上下文)

3.2.1 数据库表设计

创建securityrabc数据库,导入sql文件

-- 当前流行的权限控制系统 RBAC 模式,所以数据库表设计基于 RBAC-- 用户表
CREATE TABLE user(user_id  BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID主键',phone  VARCHAR(50) NOT NULL UNIQUE   COMMENT '手机号,唯一',password  VARCHAR(255) NOT NULL  COMMENT '密码',usernameVARCHAR(100)  NOT NULL   COMMENT '用户名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='用户表';-- 角色表
CREATE TABLE role(role_id  BIGINT  NOT NULL  PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID主键',role_name   VARCHAR(100)    NOT NULL  COMMENT '角色名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='角色表';-- 权限表
CREATE TABLE permission(permission_id     BIGINT  NOT NULL PRIMARY KEY  AUTO_INCREMENT COMMENT '权限ID主键',permission_name   VARCHAR(100)    NOT NULL                                        COMMENT '权限名'
)AUTO_INCREMENT=1000 DEFAULT charset=utf8 COMMENT='权限表';-- 用户角色关联表
CREATE TABLE user_role(user_id     BIGINT  NOT NULL   COMMENT '用户ID',role_id     BIGINT  NOT NULL   COMMENT '角色ID',PRIMARY KEY (user_id,role_id)
) DEFAULT charset=utf8 COMMENT='用户角色关联表';-- 角色权限关联表
CREATE TABLE role_permission(role_id             BIGINT  NOT NULL   COMMENT '角色ID',permission_id       BIGINT  NOT NULL   COMMENT '权限ID',PRIMARY KEY (role_id,permission_id)
) DEFAULT charset=utf8 COMMENT='角色权限关联表';
3.2.2 添加依赖
<!-- 数据库驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- ORM框架 -->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.0</version>
</dependency>
<!-- 省略GET/SET等工具类 -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
3.2.3 代码实现

1.配置文件(配置数据库,配置mybatis)

spring:# 数据源配置(Druid)datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverdruid:url: jdbc:mysql://127.0.0.1:3306/securityrabc?characterEncoding=utf-8username: rootpassword: 123456# MyBatis 配置
mybatis:# 别名包(自动为该包下类注册别名)typeAliasesPackage: com.learn.frameworklearn.**.entity# Mapper XML 文件位置mapperLocations: classpath:mybatis/**/*Mapper.xmlconfiguration:map-underscore-to-camel-case: true # 驼峰命名

2.映射数据库表的实体类

/*** 数据库用户表 - user*/
@Data
public class User {/*** 用户ID*/private Long userId;/*** 手机号*/private String phone;/*** 密码*/private String password;/*** 用户名*/private String username;
}/*** 数据库角色表 - role*/
@Data
public class Role {/*** 角色ID*/private Long roleId;/*** 角色名称*/private String roleName;
}/*** 数据库权限表 - permission*/
@Data
public class Permission {/*** 权限ID*/private Long permissionId;/*** 权限名称*/private String permissionName;
}/*** 数据库用户角色关联表 - user_role*/
@Data
public class UserRole {/*** 用户ID*/private Long userId;/*** 角色ID*/private Long roleId;
}/*** 数据库角色权限关联表 - role_permission*/
@Data
public class RolePermission {/*** 角色ID*/private Long roleId;/*** 权限ID*/private Long permissionId;
}

3.mapper层实现使用用户名获取用户信息的函数

在mapper包中新建一个UserMapper接口

package com.learn.frameworklearn.mapper;/*** 用户表操作,使用手机号用来代替用户登录账号*/
@Mapper
public interface UserMapper {/*** 通过手机号获取用户信息* phone 手机号* @return 用户信息*/User getUserByPhone(String phone);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.UserMapper"><select id="getUserByPhone" resultType="com.learn.frameworklearn.entity.User">SELECT * FROM user WHERE phone=#{phone}</select>
</mapper>

4.UserDetails接口实现,封装用户信息(给SpringSecurity送数据)

package com.learn.frameworklearn.security;/*** 封装用户信息* SpringSecurity规定给他传递的用户信息,必须是UserDetails接口的子类实例对象进行封装*/
@Data
public class LoginUserDetails implements UserDetails {private User user;public LoginUserDetails() {}public LoginUserDetails(User user) {this.user = user;}//当前账户的权限列表,暂时只实现认证,不实现授权,所以这边权限给空集合@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return new ArrayList<>();}//获取密码@Overridepublic String getPassword() {return user.getPassword();}//用户名@Overridepublic String getUsername() {return user.getPhone();}//账号是否过期,在数据库中没有设置,给默认值不过期@Overridepublic boolean isAccountNonExpired() {return true;}//账号是否锁定,在数据库中没有设置,给默认值不锁定@Overridepublic boolean isAccountNonLocked() {return true;}//密码是否过期,在数据库中没有设置,给默认值不过期@Overridepublic boolean isCredentialsNonExpired() {return true;}//账号是否可用,在数据库中没有设置,给默认值可用@Overridepublic boolean isEnabled() {return true;}
}

5.替换 InMemoryUserDetailsManager 实现 UserDetailsService方法

package com.learn.frameworklearn.security;/*** UserDetailsService是Spring Security提供的从数据库获取数据的核心接口* 实现 UserDetailsService 重写里面的 loadUserByUsername方法,替换默认从内存中获取用户信息*/
@Transactional
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;/*** 根据用户名查询用户信息* @return 不能直接返回user对象,必须返回UserDetails对象,所以需要重写一个UserDetails对象返回*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//通过用户名从数据库中查询用户信息(这个用户名从前端传递过来时可以使用手机号,邮箱或者其他用户账号)User user = userMapper.getUserByPhone(username);//判断当前账号是否存在if(Objects.isNull(user)){//如果为空,直接抛出异常throw new UsernameNotFoundException(username);}/** 不为空说明数据库中存在,将信息送到SpringSecurity上下文中*/return new LoginUserDetails(user);}}

在这里插入图片描述

6.在数据库user表中添加测试账户

在这里插入图片描述

7.验证基于数据库的登录是否生效替换了默认用户名和密码的形式

验证是否生效
第一步: 启动项目
第二步: 在浏览器中输入 http://localhost:1002/FrameworkLearn/test1 服务会自动跳转到SpringSecurity的内置登录页面
第三步: 数据在数据库中自己添加的用户信息
第四步: 查看现象
注意: 肯定会失败,因为密码,因为密码在数据库中是明文的,需要设置明文规则,如果在密码前添加{noop},可以不需要进行加密(security默认密码是加密的)

修改密码后,浏览器中继续测试,就会成功

在这里插入图片描述

8.如果想数据库中的密码是密文,可以使用 BCryptPasswordEncoder 进行加密和解密

创建配置类

将 BCryptPasswordEncoder 加入到IOC容器中,SpringSecurity 自动生效

package com.learn.frameworklearn.security;/*** Spring Security配置类*/
@Configuration
public class SecurityConfig {/*** 配置加密工具*/@Beanpublic PasswordEncoder encoder(){return new BCryptPasswordEncoder();}}

在这里插入图片描述

编写测试代码,将明文密码加密成密文,保存到数据库中,然后在继续使用浏览器测试

在这里插入图片描述

替换数据库中的密码

注意:数据库中的密码不能明文存储,要加密,而且对于加密工具,只有加密没有解密的说法,不能把加密后的密码资再解密成123456,没有这种做法

参考:市面上大部分软件的忘记密码功能,都是修改密码,再重新登录,而不是真正的找回密码

在这里插入图片描述

4.替换页面登录为前后端分离方式

修改前端界面(封装前端认证信息传递给SpringSecurity上下文)

前后端分离架构是当前最流行的软件架构模式,下面需要修改SpringSecurity默认的逻辑,改成成前后端分离的架构模式

1.替换掉登陆页面

2.修改从内置登录页面获取用户名和密码的逻辑

3.修改内部默认跳转登录页面的逻辑

4.修改登录失败的逻辑

4.1 替换内置登录验证逻辑

4.1.1 登录验证流程图

此流程图是基于前后端不分离的表单流程图,和前后端分离的流程基本上一模一样,后面实现前后端分离的登录,参考此流程

在这里插入图片描述

4.1.2 登录核心API介绍

SecurityFilterChain:SpringSecurity核心过滤器,SpringSecurity默认会自动创建一个此对象,用来支持自带的登录,现在采用前后端分离,登录逻辑发生变化,所以需要我们自己创建SpringSecurity来覆盖默认的

UsernamePasswordAuthenticationToken:封装前端页面传递过来的用户名和密码,封装好后通过

AuthenticationManager传递给SpringSecurity上下文

AuthenticationManager:SpringSecurity的认证管理器,用来进行认证

Authentication:认证实例,认证成功后,里面封装认证成功后的信息

4.1.3 登录逻辑代码实现

配置认证管理器实例

package com.learn.frameworklearn.security;@Configuration
public class SecurityConfig {/*** SpringSecurity 认证管理器*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}}

配置 SecurityFilterChain

package com.learn.frameworklearn.security;/*** Spring Security配置类*/
@Configuration
public class SecurityConfig {/*** 配置加密工具*/@Beanpublic PasswordEncoder encoder(){return new BCryptPasswordEncoder();}/*** SpringSecurity 认证管理器*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}/*** SpringSecurity过滤器*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable() //防止跨站请求伪造.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session.and().authorizeRequests().antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问return http.build();}}

登录逻辑实现

@RestController
public class LoginController {@Resourceprivate AuthenticationManager authenticationManager;/*** 登录* phone 手机号* password 密码* @return 响应结果*/@PostMapping(value = "/login")public Result login(String phone, String password){/** SpringSecurity 的认证逻辑* 默认情况SpringSecurity内置了登录页面,内置了从页面获取数据,并将其数据送到SpringSecurity上下文的方式* 当前前后端分离的逻辑,数据不再从页面获取,所以不能再使用内置逻辑,需要程序员自己实现将数据送到SpringSecurity上下文中*///封装用户名(手机号作为用户名)和密码UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone,password);//调用认证管理中的认证方法,成功就封装用户的全部信息,但是调用后可能出现异常,所以需要try...catchtry {Authentication authenticate = authenticationManager.authenticate(authenticationToken);//如果认证成功Authentication中就会有用户信息,否则为空if(Objects.isNull(authenticate)){//认证失败return Result.error(301,"认证失败,用户名或密码错误");}}catch (RuntimeException e){e.printStackTrace();//认证失败return Result.error(302,"认证失败,用户名或密码错误");}return Result.success();}}

代码完成后进行测试,因为没有前端页面所以需要一个客户端工具进行测试,这里采用postman进行测试

注意:.antMatchers(“/login”).permitAll() //这个/login地址,登陆和未登录的人都可以访问访问

在这里插入图片描述

在这里插入图片描述

还有另外一种情况,在登录了的情况下,访问了其它资源,例如: http://localhost:1002/FrameworkLearn/test2
注意: 我前面已经登陆了为什么还不能访问,因为前后端分离项目不是Session-Cookie机制

在前后端分离的项目中,传统的Session-Cookie机制可能不再适用,因为前端和后端是独立运行的。在这种情况下,通常会使用其他认证和授权机制,如JWT(JSON Web Tokens)或OAuth2等

在这里插入图片描述

先写一个不携带token访问其他资源的逻辑处理

4.2 修改未登录访问其它资源的逻辑处理

实现此功能需要的步骤如下:

第二步: 实现AuthenticationEntryPoint接口,实现自定义的处理器

第三步: 将自定义的处理器注册到Spring Security的核心Filter中

第四步: 测试是否生效

第一步: 添加依赖(json工具依赖)

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.32</version>
</dependency>

第二步

package com.learn.frameworklearn.security;/*** 匿名请求访问私有化请求时的处理器(直接告诉用户未认证)* 在未认证或者认证错误的情况下访问需要认证的资源时的处理类*/
@Component  //加入到IOC容器
public class LoginUnAuthenticationEntryPointHandler implements AuthenticationEntryPoint {/** 当访问一个需要认证的资源时因为当前用户没有认证或者认证失败,直接访问资源会交给此函数进行处理* 因为架构是前后端分离的项目,所以给客户端的提示保持和控制器的返回值格式相同*/@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setCharacterEncoding("utf-8");response.setContentType("application/json");Result result = Result.error(303, "用户未认证或登录已过期,请重新登录后再访问");//将消息json化String json = JSONUtil.toJsonStr(result);//送到客户端response.getWriter().print(json);}
}

第三步

将自定义的处理器注册到Spring Security的核心过滤器中

package com.learn.frameworklearn.security;@Configuration
public class SecurityConfig {//注入进来@Resourceprivate LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;/*** SpringSecurity过滤器*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable() //防止跨站请求伪造.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session.and().authorizeRequests().antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问//自定义未登录处理(注册匿名请求访问私有化请求时的处理器) 添加这个http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);return http.build();}}

第四步:测试

在这里插入图片描述

4.3 解决HTTP协议无状态

传统前后端不分离的架构,采用Session+Cookie机制(解决HTTP协议无状态)

现在前后端分离架构,采用token令牌方式

token生成采用JWT工具生成和校,使用Redis数据库进行校验和保存

4.3.1 Redis客户端安装和启动

省略

3.2 JWT工具类编写

添加依赖

<!-- JWT工具类 -->
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.4.0</version>
</dependency><!-- redis客户端 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

工具类代码实现

package com.learn.frameworklearn.utils;/*** JWT工具类*/
public class JwtUtils {private static Algorithm hmac256 = Algorithm.HMAC256("YLWTSMTJFYHDCMGSCWHSSYBZSDKC");/*** 生成token* @param pub  负载* @param expiresTime 过期时间(单位 毫秒)* @return token*/public static String sign(String pub, Long expiresTime){return JWT.create() //生成令牌函数.withIssuer(pub) //自定义负载部分,其实就是添加Claim(jwt结构中的payload部分),可以通过源码查看.withExpiresAt(new Date(System.currentTimeMillis()+expiresTime)) //添加过期时间.sign(hmac256);}/*** 校验token*/public static boolean verify(String token){JWTVerifier verifier = JWT.require(hmac256).build();//如果正确,直接代码向下执行,如果错误,抛异常verifier.verify(token);return true;}/*** 从token中获取负载* @param token 令牌* @return 保存的负载*/public static String getClaim(String token){DecodedJWT jwt = JWT.decode(token);Claim iss = jwt.getClaim("iss");return iss.asString();}
}
4.3.3 Redis客户端实现

配置文件修改

spring:redis:host: 127.0.0.1  # redis服务器地址password:        # redis服务器密码,我这里没有设置密码database: 0      # redis的库,我这里用0号库

代码实现

package com.learn.frameworklearn.utils;/*** Redis客户端*/
@Component
public class RedisClient {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 保存数据*/public void set (String key,String value){stringRedisTemplate.opsForValue().set(key,value);}/*** 保存数据-过期时间* @param key    键* @param value  值* @param time   过期时间,单位是 毫秒*/public void set (String key,String value,Long time){stringRedisTemplate.opsForValue().set(key,value,time, TimeUnit.MILLISECONDS);}/*** 通过键获取对应的值* @param key 键* @return    值*/public String get(String key){return stringRedisTemplate.opsForValue().get(key);}/*** 通过键删除对应的值* @param key 键*/public void del(String key){stringRedisTemplate.delete(key);}/*** 判断key是否存在*/public Boolean exists(String key){return stringRedisTemplate.hasKey(key);}
}
4.3.4 登录逻辑修改
package com.learn.frameworklearn.controller;@RestController
public class LoginController {@Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate RedisClient redisClient;/*** 登录* phone 手机号* password 密码** @return 响应结果*/@PostMapping(value = "/login")public Result login(String phone, String password) {/** SpringSecurity 的认证逻辑* 默认情况SpringSecurity内置了登录页面,内置了从页面获取数据,并将其数据送到SpringSecurity上下文的方式* 当前前后端分离的逻辑,数据不再从页面获取,所以不能再使用内置逻辑,需要程序员自己实现将数据送到SpringSecurity上下文中*///1.封装用户名(手机号作为用户名)和密码UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(phone, password);//2.调用认证管理中的认证方法,成功就封装用户的全部信息,但是调用后可能出现异常,所以需要try...catchtry {Authentication authenticate = authenticationManager.authenticate(authenticationToken);//如果认证成功Authentication中就会有用户信息,否则为空if (Objects.isNull(authenticate)) {//认证失败return Result.error(301, "认证失败,用户名或密码错误");}//3.登录成功将用户信息保存到redis中,以token作为key//获取用户信息LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();if (Objects.isNull(principal)) {return Result.error(303,"认证失败,用户名或密码错误");}//使用token作为redis的key,格式为 login:tokenString token = JwtUtils.sign(principal.getUsername(), 1000 * 60 * 60 * 24 * 7L);//过期时间为7天String key = "login:token:" + token;//将用户信息json化String json = JSONUtil.toJsonStr(principal);//将用户信息json化后保存到redis中redisClient.set(key, json, 1000 * 60 * 60 * 24 * 7L);//4.将token返回给客户端(前端)Map<String, Object> map = new HashMap<>();map.put("token", token);return Result.success(map);} catch (RuntimeException e) {e.printStackTrace();//认证失败return Result.error(302, "认证失败,用户名或密码错误");}}
}

测试,查看是否生成token,并且会把这个token存储在redis中

在这里插入图片描述

redis客户端工具查看

在这里插入图片描述

注意:之后的每次请求,都要带着token一起发送请求

4.3.5 反复登录,多token解决

客户端发送的所有请求都需要带token,在进行登录时单独进行token的校验,如果登陆过,刷新token

在登录中添加一个校验逻辑,删除原来的key

package com.learn.frameworklearn.controller;@RestController
public class LoginController {@Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate RedisClient redisClient;/*** 登录*/@PostMapping(value = "/login")public Result login(String phone, String password, HttpServletRequest request) {//添加这个方法/** 登录前判断是否上次的登录未过期,如果未过期直接删除,重新登录生成新token* 就是刷新token,登录一次刷新一次*/String token_ = request.getHeader("token");//判断token是否存在if (StringUtils.hasText(token_)) {//判断redis中是否存在String claim = JwtUtils.getClaim(token_);//校验是否是同一个账户if (StringUtils.hasText(claim) && claim.equals(phone)) {String key = "login:token:" + token_;//从redis中删除redisClient.del(key);}}//......}
}
4.3.6 其它资源访问基于token令牌方式

使用令牌方式,替换前后端不分离的session-cookie机制,解决HTTP无状态的问题

SpringSecurity运行原理

pringSecurity的功能实现:SpringSecurity的核心是过滤器链,核心功能实现也是由一个个Filter(过滤器)组成

下面是SpringSecurity内置的过滤器

在这里插入图片描述

会经过一系列的过滤器链
在这里插入图片描述

简单画了一个SpringSecurity原理图,客戶端发送过来的请求会经过一个个的过滤器,每一个过滤器承担着不同的功能
例如,UsernamePasswordAuthenticationFilter过滤器帮助我们校验账户和密码

现在我们要模拟session+cookie机制,通过token凭据实现权限控制,方式如下

在这里插入图片描述

4.3.7 基于token机制的实现

第一步: 自定义 Filter

第二步: 注册到SpringSecurity的Filter中

第一步

package com.learn.frameworklearn.security;/*** 自定义过滤器,实现token令牌的判断(统一校验token凭证)*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Resourceprivate RedisClient redisClient;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头tokenString token = request.getHeader("token");if(StringUtils.hasLength(token)){//redis中获取用户信息String key = "login:token:"+token;String json = redisClient.get(key);if(StringUtils.hasLength(json)){//反序列化LoginUserDetails user = JSONUtil.toBean(json, LoginUserDetails.class);if(Objects.nonNull(user)){//封装用户信息,送到下一个过滤器  UsernamePasswordAuthenticationFilterUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());//将Redis数据库中的信息送到SpringSecurity上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);}else {SecurityContextHolder.getContext().setAuthentication(null);}}}//放行,后面交给Spring Security 框架filterChain.doFilter(request,response);}
}

第二步

package com.learn.frameworklearn.security;@Configuration
public class SecurityConfig {@Resourceprivate LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;//注入进来@Resourceprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;/*** SpringSecurity过滤器*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable() //防止跨站请求伪造.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session.and().authorizeRequests().antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面     添加这句话http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//自定义未登录处理(注册匿名请求访问私有化请求时的处理器)http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);return http.build();}}

测试验证

在这里插入图片描述

在这里插入图片描述

5.注销

注销比较简单,直接将redis数据清空了即可

SpringSecurity内置注销功能logout,我们使用内置的注销,覆盖注销的逻辑即可

package com.learn.frameworklearn.security;/*** 注销成功的处理器*/
@Component
public class LogoutStatusSuccessHandler implements LogoutSuccessHandler {@Resourceprivate RedisClient redisClient;@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {String token = request.getHeader("token");if(StringUtils.hasText(token)){redisClient.del("login:token:"+token);}response.setCharacterEncoding("utf-8");response.setContentType("application/json");Result result = Result.success("注销成功");//将消息json化String json = JSONUtil.toJsonStr(result);//送到客户端response.getWriter().print(json);}
}

在security中注册一下注销成功的处理器

package com.learn.frameworklearn.security;@Configuration
public class SecurityConfig {@Resourceprivate LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;@Resourceprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Resourceprivate LogoutStatusSuccessHandler logoutStatusSuccessHandler;@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable() //防止跨站请求伪造.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session.and().authorizeRequests().antMatchers("/login").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//自定义未登录处理(注册匿名请求访问私有化请求时的处理器)http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);//注册自定义处理器(注销处理器,注销成功后删除redis中的数据)  添加这句话http.logout().logoutSuccessHandler(logoutStatusSuccessHandler);return http.build();}}

http://localhost:1002/FrameworkLearn/logout,使用security内置的即可,改一下逻辑

在这里插入图片描述

6.授权

授权的意义就是当一个用户登录成功后,此账户本身拥有的权限

eg:当前用户用哪些角色,当前角色都能干什么(删除、更新等)

在这里插入图片描述

6.1 将权限从数据库送到SpringSecurity上下文

从数据库中将用户信息送到上下文,在 UserDetailsService接口的实现类中

实现步骤:

第一步:mapper提供查询方法,将用户相关的权限信息查询到封装到UserDetails的对象中
通过用户ID查询角色名称列表
通过角色ID查询权限名称列表

第二步:在UserDetailsService中进行封装
在UserDetailsService中调用mapper并且封装传递给 SpringSecurity 上下文,修改UserDetails结构添加属性等

第三步:在控制器层进行注解控制(配置文件)

第四步:开启注解配置否则不生效

第五步:权限不够的处理器

6.1.1 第一步
/*** 用户角色关联表*/
@Mapper
public interface UserRoleMapper {/*** 通过用户ID查询用户角色列表* @param userId 用户ID* @return 用户角色列表*/List<UserRole> getUserRolesByUserId(Long userId);
}/*** 操作数据库角色表*/
@Mapper
public interface RoleMapper {/*** 批量查询* @param roleIds 角色ID列表* @return 角色列表*/List<Role> batchGetRolesByRoleIds(List<Long> roleIds);
}/*** 操作数据库角色权限表*/
@Mapper
public interface RolePermissionMapper {/*** 通过角色ID列表查询权限角色权限列表* @param roleIds 角色IDs* @return 角色权限列表*/List<RolePermission> getRolePermissionsByRoleIds(List<Long> roleIds);
}/*** 操作数据库权限表*/
@Mapper
public interface PermissionMapper {/*** 通过权限ID列表查询权限列表* @param permissionIds 权限ID列表* @return 权限列表*/List<Permission> batchGetPermissionsByPermissionIds(List<Long> permissionIds);
}

mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.PermissionMapper"><select id="batchGetPermissionsByPermissionIds" resultType="com.learn.frameworklearn.entity.Permission">SELECT * FROM permission WHERE permission_id IN<foreach collection="list" open="(" close=")" separator="," item="item">#{item}</foreach></select>
</mapper><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.RolePermissionMapper"><select id="batchGetRolePermissionsByRoleIds" resultType="com.learn.frameworklearn.entity.RolePermission">SELECT * FROM role_permission WHERE role_id IN<foreach collection="list" open="(" close=")" separator="," item="item">#{item}</foreach></select>
</mapper><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.RoleMapper"><select id="batchGetRolesByRoleIds" resultType="com.learn.frameworklearn.entity.Role">SELECT * FROM role WHERE role_id IN<foreach collection="list" open="(" close=")" separator="," item="item">#{item}</foreach></select>
</mapper><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learn.frameworklearn.mapper.UserRoleMapper"><select id="getUserRoleByUserId" resultType="com.learn.frameworklearn.entity.UserRole">SELECT * FROM user_role WHERE user_id=#{userId}</select>
</mapper>

在这里插入图片描述

6.1.2 第二步

UserDetails 修改

package com.learn.frameworklearn.security;/*** 封装用户信息* SpringSecurity规定给他传递的用户信息,必须是UserDetails接口的子类实例对象进行封装*/
@Data
public class LoginUserDetails implements UserDetails {private User user;//角色名称列表,用于授权private List<String> roleNames;//权限名称列表,用户授权private List<String> permissionNames;public LoginUserDetails() {}public LoginUserDetails(User user, List<String> roleNames, List<String> permissionNames) {this.user = user;this.roleNames = roleNames;this.permissionNames = permissionNames;}//当前账户的权限列表   现在写这里@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> grantedAuthorities = new ArrayList<>();/** 当前用户的权限信息* 1. 角色表权限   ROLE_  admin  ROLE_admin* 2. 权限表权限  del  add query edit*///添加角色if(!CollectionUtils.isEmpty(roleNames)){//将角色设置到GrantedAuthority中,官网要求角色要加上前缀 ROLE_xxx 区分其它权限for (String roleName : roleNames) {roleName = "ROLE_"+roleName;grantedAuthorities.add(new SimpleGrantedAuthority(roleName));}}//添加权限if(!CollectionUtils.isEmpty(permissionNames)){//将权限设置到GrantedAuthority中for (String permissionName : permissionNames) {grantedAuthorities.add(new SimpleGrantedAuthority(permissionName));}}return grantedAuthorities;}//获取密码@Overridepublic String getPassword() {return user.getPassword();}//......
}

UserDetailsService修改

package com.learn.frameworklearn.security;/*** UserDetailsService是Spring Security提供的从数据库获取数据的核心接口* 实现 UserDetailsService 重写里面的 loadUserByUsername方法,替换默认从内存中获取用户信息*/
@Transactional
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Resourceprivate UserMapper userMapper;@Resourceprivate UserRoleMapper userRoleMapper;@Resourceprivate RoleMapper roleMapper;@Resourceprivate RolePermissionMapper rolePermissionMapper;@Resourceprivate PermissionMapper permissionMapper;/*** 根据用户名查询用户信息** @param username* @return 不能直接返回user对象,必须返回UserDetails对象,所以需要重写一个UserDetails对象返回*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//通过用户名从数据库中查询用户信息(这个用户名从前端传递过来时可以使用手机号,邮箱或者其他用户账号)User user = userMapper.getUserByPhone(username);//判断当前账号是否存在if (Objects.isNull(user)) {//如果为空,直接抛出异常throw new UsernameNotFoundException(username);}//添加这里/** 不为空说明数据库中存在,将信息送到SpringSecurity上下文中(用户信息,角色信息,权限信息都要返回给客户端)*///角色名称列表List<String> roleNames = new ArrayList<>();//权限名称列表List<String> permissionNames = new ArrayList<>();//获取用户角色列表List<UserRole> userRoles = userRoleMapper.getUserRoleByUserId(user.getUserId());if (!CollectionUtils.isEmpty(userRoles)) {//查询角色信息List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());if (!CollectionUtils.isEmpty(roleIds)) {//查询角色信息List<Role> roles = roleMapper.batchGetRolesByRoleIds(roleIds);if (!CollectionUtils.isEmpty(roles)) {List<String> roleNameList = roles.stream().map(Role::getRoleName).collect(Collectors.toList());roleNames.addAll(roleNameList);}//当前角色的权限列表List<RolePermission> rolePermissions = rolePermissionMapper.batchGetRolePermissionsByRoleIds(roleIds);if (!CollectionUtils.isEmpty(rolePermissions)) {//权限id列表List<Long> permissionIdList = rolePermissions.stream().map(RolePermission::getPermissionId).collect(Collectors.toList());if (!CollectionUtils.isEmpty(permissionIdList)) {//查询权限信息List<Permission> permissions = permissionMapper.batchGetPermissionsByPermissionIds(permissionIdList);if (!CollectionUtils.isEmpty(permissions)) {List<String> permissionNameList = permissions.stream().map(Permission::getPermissionName).collect(Collectors.toList());if (!CollectionUtils.isEmpty(permissionNameList)) {//权限名称列表permissionNames.addAll(permissionNameList);}}}}}}//用户信息,角色信息,权限信息都要返回给客户端return new LoginUserDetails(user, roleNames, permissionNames);}
}
6.1.3 第三步

在配置文件上添加注解,开启注解校验

/*** 测试控制器* 验证SpringSecurity是否生效*/
@RestController
public class HelloController {//测试方法1  匿名访问(pom.xml中注释掉security的依赖就可以随便访问,添加了依赖,访问网址就会默认跳转到内置网页)@GetMapping(value = "/test1")public Result test1(){return Result.success("test1");}//测试方法2  认证后才能访问@GetMapping(value = "/test2")public Result test2(){return Result.success("test2");}//测试方法3  admin角色可以访问@PreAuthorize(value = "hasRole('admin')") //开启@EnableMethodSecurity(securedEnabled = true)测试@GetMapping(value = "/test3")public Result test3(){return Result.success("test3");}//测试方法4 cto角色或者cfo角色可以访问@PreAuthorize(value = "hasAnyRole('cfo','cto')")@GetMapping(value = "/test4")public Result test4(){return Result.success("test4");}//测试方法5 cto角色和cfo角色可以访问@PreAuthorize(value = "hasRole('cto') and hasRole('admin')")@GetMapping(value = "/test5")public Result test5(){return Result.success("test5");}//测试方法6 del权限可以访问@PreAuthorize(value = "hasAuthority('del')")@GetMapping(value = "/test6")public Result test6(){return Result.success("test6");}//测试方法7 del或者edit权限可以访问@PreAuthorize(value = "hasAnyAuthority('del','edit')")@GetMapping(value = "/test7")public Result test7(){return Result.success("test7");}//测试方法8 del和edit权限可以访问@PreAuthorize(value = "hasAuthority('del') and hasAuthority('edit')")@GetMapping(value = "/test8")public Result test8(){return Result.success("test8");}
}
6.1.4 第四步

如果登录成功,使用当前用户进行访问;如果登录失败,发现权限不够,报权限错误,定义处理器进行处理

package com.learn.frameworklearn.security;/*** 权限不足处理器* 用户登录成功,访问某一个资源时因为权限不足,报异常*/
@Component
public class LoginUnAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setCharacterEncoding("utf-8");response.setContentType("application/json");Result result = Result.error(305,"权限不足,请重新授权。");//将消息json化String json = JSONUtil.toJsonStr(result);//送到客户端response.getWriter().print(json);}
}
6.1.5 第五步

自定义权限不足处理器后,需要进行注册,注册到SecurityConfig配置文件中

package com.learn.frameworklearn.security;@Configuration
@EnableMethodSecurity(securedEnabled = true) //开发方法权限验证
public class SecurityConfig {@Resourceprivate LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;@Resourceprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Resourceprivate LogoutStatusSuccessHandler logoutStatusSuccessHandler;@Resourceprivate LoginUnAccessDeniedHandler loginUnAccessDeniedHandler;/*** SpringSecurity过滤器*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable() //防止跨站请求伪造.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //取消session.and().authorizeRequests().antMatchers("/login","/test1").permitAll() //这个/login地址,登陆和未登录的人都可以访问访问.anyRequest().authenticated(); //除了上面设置的地址可以匿名访问,其它所有的请求地址需要认证访问//将自定义的过滤器注册到SpringSecurity过滤器链中,并且设置到UsernamePasswordAuthenticationFilter前面http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//自定义未登录处理(注册匿名请求访问私有化请求时的处理器)http.exceptionHandling().authenticationEntryPoint(loginUnAuthenticationEntryPointHandler);//注册自定义的处理器(认证后的用户访问需要认证资源时因为权限不足走的处理器)  添加这里http.exceptionHandling().accessDeniedHandler(loginUnAccessDeniedHandler);//注册自定义处理器(注销处理器,注销成功后删除redis中的数据)http.logout().logoutSuccessHandler(logoutStatusSuccessHandler);return http.build();}}

http://localhost:1002/FrameworkLearn/login 先登录,拿到token

在这里插入图片描述

携带token,访问http://localhost:1002/FrameworkLearn/test2

在这里插入图片描述

携带token,访问http://localhost:1002/FrameworkLearn/test3

在这里插入图片描述

http://www.xdnf.cn/news/1278523.html

相关文章:

  • 论文阅读:Aircraft Trajectory Prediction Model Based on Improved GRU Structure
  • LabVIEW模糊逻辑控制车辆停靠
  • 视觉相机偏移补偿
  • .NET Core MVC中CSHTML
  • 嵌入式硬件中AI硬件设计方法与技巧
  • 18.WEB 服务器
  • docker compose和docker-compose命令的区别
  • Vue2篇——第二章 Vue从指令修饰符到侦听器的全面解析(重点)
  • MATLAB绘制水的蒸汽压曲线(Antoine方程)
  • jdk17下载安装教程【超详细图文】
  • 《设计模式》策略模式
  • vue3-基础语法
  • JUC学习笔记-----ReentrantLock
  • TC39x STM(System Timer)学习记录
  • 机器学习数学基础:46.Mann-Kendall 序贯检验(Sequential MK Test)
  • Spring Boot - 内置的9个过滤器用法
  • Day 9-2: Transformer翻译实例演示 - 翻译的基础设施
  • AI大模型 教师方向应用探索
  • Audio Flamingo
  • 第4章 程序段的反复执行4 多重循环练习(题及答案)
  • Python day40
  • C++ list类
  • 【深度学习新浪潮】遥感图像风格化迁移研究工作介绍
  • JS中typeof与instanceof的区别
  • 腾讯云EdgeOne KV存储在游戏资源发布中的技术实践与架构解析
  • 数学建模——回归分析
  • 【GPT入门】第44课 检查 LlamaFactory微调Llama3的效果
  • 集成电路学习:什么是Parameter Server参数服务器
  • 机器学习-增加样本、精确率与召回率
  • Java开源代码源码研究:我的成长之路与实战心得分享