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

【SpringSecurity】认证与鉴权框架SpringSecurity——认证

目录

  • SpringSecurity
    • 介绍
    • 特性
      • CSRF攻击
        • 攻击模式
        • 攻击原理
        • 预防手段
      • XSS攻击
        • 攻击模式
        • 危害
        • 预防手段
    • SpringSecurity预防CSRF攻击
    • SpringSecurity预防XSS攻击
    • SpringSecurity与OAuth2的关系
    • SpringSecurity的核心功能
  • 代码实战
    • 依赖
    • 定义一个接口
    • Redis工具类
    • 响应类
    • 直接运行
    • 工具类
    • 认证业务
    • 密码加密存储问题
    • 登录接口
    • 认证过滤器
    • 退出登陆
    • 测试

SpringSecurity

介绍

  • Spring Security 是一个开源框架,用于为 Java 应用程序提供身份验证、授权和其他安全功能。
  • 它是基于 Java 安全框架(JSR 375)的一部分,可以与 Spring 框架无缝集成,提供了一套功能强大而灵活的安全性解决方案。

特性

Spring Security 提供了一系列的安全性特性,包括:

  • 身份验证: 支持多种身份验证方式,如基于数据库、LDAP、表单登录等。
  • 授权: 可以基于角色或权限控制访问资源。
  • 加密和解密: 提供了加密和解密数据的功能,可以用于存储密码等敏感信息。
  • 防护: 提供了一系列的安全防护措施,如防止跨站点请求伪造(CSRF)攻击、跨站脚本(XSS)攻击等。
  • 记住我: 支持“记住我”功能,可以在用户下次登录时自动记住用户身份。
  • 单点登录: 支持单点登录(SSO)功能,可以在多个应用程序之间实现用户的无缝访问。

CSRF攻击

  • CSRF(Cross-Site Request Forgery)攻击,也被称为“跨站请求伪造”攻击,是一种常见的Web应用程序安全漏洞。
  • 在这种攻击中,攻击者通过欺骗用户访问恶意网站或点击恶意链接,来发送未经授权的请求,以模拟用户在被攻击网站上的操作。
攻击模式
  • 攻击者通常会在恶意网站上构建一个伪造的请求,该请求会利用被攻击网站的漏洞,以被攻击用户的身份发送请求。
  • 当受害者在恶意网站上点击或访问这个请求时,网站会认为这是合法的用户行为,并按照请求的指示执行操作,可能导致潜在的危害,如更改用户的个人信息、执行未授权的操作、盗取用户的敏感信息等。
攻击原理
  • CSRF 攻击的原理是利用了被攻击网站的身份验证机制不够严格,攻击者可以伪造请求中的身份认证凭证(如cookie),从而欺骗被攻击网站。
预防手段

为了防止 CSRF 攻击,开发人员可以采取以下措施:

  • 使用一次性令牌(CSRF token):在每个表单或请求中包含一个随机生成的令牌,服务器在处理请求时验证该令牌的有效性。
  • 检查 Referer 头信息:服务器可以验证请求的来源是否与被请求页面的域名一致,但这种方法可能不完全可靠,因为 Referer 头信息有时会被篡改或禁用。
  • 使用验证码:在一些敏感操作或数据修改的情况下,要求用户输入验证码,以确保用户的人工参与。
  • 加强身份验证和授权机制:使用强密码策略、双因素身份验证等措施,确保用户的身份验证和访问权限的安全性。

综上所述,对于 Web 应用程序来说,保护用户免受 CSRF 攻击是非常重要的,开发人员应该采取适当的防护措施来防止这种类型的攻击。

XSS攻击

  • XSS(Cross-Site Scripting)攻击,也被称为“跨站脚本攻击”,是指攻击者通过将非法的恶意脚本注入到合法网站中,使其在被访问时在用户的浏览器上执行的一种安全漏洞。
攻击模式

XSS 攻击的方式多样,常见的包括以下几种:

  1. 存储型(Persistent)XSS:攻击者将恶意脚本存储到目标网站的数据库中,当其他用户浏览受影响的页面时,恶意脚本会从服务器上取出并在用户浏览器中执行。
  2. 反射型(Reflected)XSS:攻击者通过构造恶意链接或欺骗用户点击恶意链接,将恶意脚本作为参数传递给目标网站,目标网站将该参数作为响应的一部分返回给用户,用户浏览器执行恶意脚本。
  3. DOM-based XSS:攻击者通过修改浏览器DOM(Document Object Model)中的内容,使得恶意脚本在浏览器中执行。
危害
  • XSS 攻击的危害包括窃取用户敏感信息(例如用户名、密码、Cookie)、篡改页面内容、重定向用户到恶意网站、执行恶意操作等。
预防手段

为了防止 XSS 攻击,开发人员可以采取以下措施:

  • 输入验证与过滤:对用户输入的数据进行验证和过滤,确保输入的数据符合预期格式。例如,对于文本输入,可以使用特殊字符转义或过滤函数来防止恶意注入。
  • 输出转义:在将用户输入展示在页面上时,对特殊字符进行转义或过滤,确保用户输入的内容不会被作为脚本执行。
  • 使用安全的编码和解码:使用安全的编码函数,如将用户输入进行 HTML 实体编码或 URL 编码,以防止特殊字符的执行。
  • 设置 HTTP 头的 Content-Security-Policy(CSP):使用 Content Security Policy 头来指定允许加载和执行的内容源,可以减少 XSS 攻击的风险。
  • 使用浏览器的内置防护机制:现代浏览器通常会提供一些内置的防护机制,如自动过滤或阻止一些可疑的恶意脚本。

综上所述,防止 XSS 攻击是非常重要的,开发人员应该采取适当的防护措施来确保用户数据的安全性。

SpringSecurity预防CSRF攻击

  1. CSRF Token:Spring Security 在处理表单提交时,会生成一个随机的 CSRF Token,并将其包含在表单中或作为请求头的一部分发送到后端。后端校验请求中的 Token 是否与会话中存储的 Token 相匹配,如果不匹配,则拒绝该请求。
  2. SameSite Cookie:在 Spring Security 5.x 版本中,可以通过配置 SameSite 属性来设置 Cookie 的 SameSite 属性为 Strict 或 Lax,以控制 Cookie 是否允许跨站点发送。Strict 模式下,Cookie 只能在同站点请求中发送,Lax 模式下,某些情况下允许跨站点发送,但仅限于 GET 请求。
  3. 验证 HTTP Referer:Spring Security 可以配置验证请求头中的 Referer 字段来检查请求来源是否合法。这种方式需要目标网站的所有请求都来自同一个域名,并且不会存在跨域请求。
  4. 验证 Origin 头:Spring Security 还可以配置验证请求头中的 Origin 字段来检查请求来源是否合法。与 Referer 验证不同,Origin 头是 HTML5 中引入的一种更安全的验证机制。
  5. 避免使用 GET 请求触发敏感操作:将敏感操作(如删除、更新等)使用 POST、PUT 或 DELETE 请求方式发送,避免使用 GET 请求方式。GET 请求可以被浏览器主动预加载、缓存或者通过 URL 地址栏直接触发,容易导致 CSRF 攻击。

综上所述,通过使用 CSRF Token、设置 SameSite Cookie、验证 Referer 或 Origin 头以及避免使用 GET 请求触发敏感操作等机制,Spring Security 提供了有效的防御 CSRF 攻击的手段。开发人员可以根据实际需求选择适合的机制来保护应用程序的安全性。

SpringSecurity预防XSS攻击

Spring Security 本身并不直接提供针对 XSS(Cross-Site Scripting)攻击的防护机制,而是通过一些安全措施和最佳实践来减少 XSS 攻击的潜在风险。下面是一些常见的防止 XSS 攻击的建议:

  1. 输入验证和过滤:对用户输入的数据进行验证和过滤,限制特殊字符、HTML 标签和脚本等,确保用户提供的数据不会被解析为可执行的脚本。
  2. 输出编码:在将用户输入的数据渲染到网页上时,使用合适的编码方式对数据进行转义,确保任何潜在的脚本都被当作文本而不是可执行的代码来处理。
  3. 使用安全的框架和工具:使用安全性较高的框架和工具来处理用户输入和输出,例如,使用 Spring Security 提供的 Thymeleaf、JSTL 或 HTML 转义库,这些库可以自动转义用户输入的数据。
  4. CSP(Content Security Policy):在 HTTP 响应头中设置 Content-Security-Policy,通过限制页面可以加载的资源来源和类型,来减少 XSS 攻击的风险。
  5. XSS 过滤器:可以在应用的过滤器链中添加一个 XSS 过滤器,对请求和响应中的数据进行检查和过滤,以防止潜在的 XSS 攻击。

SpringSecurity与OAuth2的关系

  • OAuth2(Open Authorization 2.0)是一种开放标准的授权协议,允许用户通过授权第三方应用程序来访问他们存储在另一个服务提供者上的资源,而不需要直接提供其凭据。

  • Spring Security与OAuth2的关系是,Spring Security提供了对OAuth2的支持,使得开发者可以使用Spring Security来实现基于OAuth2的认证和授权机制。Spring Security提供了一些内置的OAuth2相关的类和接口,用于处理OAuth2协议的各个环节,如授权服务器、资源服务器、客户端等。

  • 通过Spring Security的OAuth2支持,开发者可以轻松地构建安全的应用程序,并在应用程序中实现OAuth2的各种功能,如提供第三方登录、使用第三方身份验证、保护API资源等。此外,Spring Security还提供了许多可扩展的接口和类,以便开发者可以自定义和扩展OAuth2的行为和细节。

SpringSecurity的核心功能

  • Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,

  • 其核心思想是通过一系列的filter chain来进行拦截过滤,对用户的访问权限进行控制。

-​ spring security 的核心功能主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)
  • 其核心就是一组过滤器链,项目启动后将会自动配置。
  • 最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
    在这里插入图片描述
  • 图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。
  • ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
  • FilterSecurityInterceptor: 负责权限校验的过滤器。

例如:对于Username Password认证过滤器来说,

  • 会检查是否是一个登录请求;
  • 是否包含username 和 password (也就是该过滤器需要的一些认证信息) ;

代码实战

依赖

    <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><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><version>5.1.47</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></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.33</version></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency></dependencies>

定义一个接口

@RestController
@RequestMapping("/order")
public class OrderController {@GetMapping("/list")public ResponseResult list(){return new ResponseResult(200, "订单列表");}
}

Redis工具类

package com.micro.utils;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;/*** @author: zjl* @datetime: 2024/4/26* @desc:*/
@Component
public class RedisKeyUtil {private  StringRedisTemplate redisTemplate;@Autowiredpublic void setRedisTemplate(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}/** -------------------key相关操作--------------------- *//*** 删除key** @param key*/public  void delete(String key) {redisTemplate.delete(key);}/*** 批量删除key** @param keys*/public  void delete(Collection<String> keys) {redisTemplate.delete(keys);}/*** 序列化key** @param key* @return*/public  byte[] dump(String key) {return redisTemplate.dump(key);}/*** 是否存在key** @param key* @return*/public  Boolean hasKey(String key) {return redisTemplate.hasKey(key);}/*** 设置过期时间** @param key* @param timeout* @param unit* @return*/public  Boolean expire(String key, long timeout, TimeUnit unit) {return redisTemplate.expire(key, timeout, unit);}/*** 设置过期时间** @param key* @param date* @return*/public  Boolean expireAt(String key, Date date) {return redisTemplate.expireAt(key, date);}/*** 查找匹配的key** @param pattern* @return*/public  Set<String> keys(String pattern) {return redisTemplate.keys(pattern);}/*** 将当前数据库的 key 移动到给定的数据库 db 当中** @param key* @param dbIndex* @return*/public  Boolean move(String key, int dbIndex) {return redisTemplate.move(key, dbIndex);}/*** 移除 key 的过期时间,key 将持久保持** @param key* @return*/public  Boolean persist(String key) {return redisTemplate.persist(key);}/*** 返回 key 的剩余的过期时间** @param key* @param unit* @return*/public  Long getExpire(String key, TimeUnit unit) {return redisTemplate.getExpire(key, unit);}/*** 返回 key 的剩余的过期时间** @param key* @return*/public  Long getExpire(String key) {return redisTemplate.getExpire(key);}/*** 从当前数据库中随机返回一个 key** @return*/public  String randomKey() {return redisTemplate.randomKey();}/*** 修改 key 的名称** @param oldKey* @param newKey*/public  void rename(String oldKey, String newKey) {redisTemplate.rename(oldKey, newKey);}/*** 仅当 newkey 不存在时,将 oldKey 改名为 newkey** @param oldKey* @param newKey* @return*/public  Boolean renameIfAbsent(String oldKey, String newKey) {return redisTemplate.renameIfAbsent(oldKey, newKey);}/*** 返回 key 所储存的值的类型** @param key* @return*/public  DataType type(String key) {return redisTemplate.type(key);}
}
package com.micro.utils;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** @author: zjl* @datetime: 2024/4/26* @desc:*/
@Component
public class RedisStringUtil {private  StringRedisTemplate redisTemplate;@Autowiredpublic void setRedisTemplate(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}/** -------------------string相关操作--------------------- *//*** 设置指定 key 的值** @param key* @param value*/public void set(String key, String value) {redisTemplate.opsForValue().set(key, value);}/*** 获取指定 key 的值** @param key* @return*/public String get(String key) {return redisTemplate.opsForValue().get(key);}/*** 返回 key 中字符串值的子字符** @param key* @param start* @param end* @return*/public String getRange(String key, long start, long end) {return redisTemplate.opsForValue().get(key, start, end);}/*** 将给定 key 的值设为 value ,并返回 key 的旧值(old value)** @param key* @param value* @return*/public String getAndSet(String key, String value) {return redisTemplate.opsForValue().getAndSet(key, value);}/*** 对 key 所储存的字符串值,获取指定偏移量上的位(bit)* @param key* @param offset* @return*/public Boolean getBit(String key, long offset) {return redisTemplate.opsForValue().getBit(key, offset);}/*** 批量获取** @param keys* @return*/public List<String> multiGet(Collection<String> keys) {return redisTemplate.opsForValue().multiGet(keys);}/*** 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value** @param key   位置* @param value 值,true为1, false为0* @return*/public boolean setBit(String key, long offset, boolean value) {return redisTemplate.opsForValue().setBit(key, offset, value);}/*** 将值 value 关联到 key ,并将 key 的过期时间设为 timeout** @param key* @param value* @param timeout 过期时间* @param unit    时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES*                秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS*/public void setEx(String key, String value, long timeout, TimeUnit unit) {redisTemplate.opsForValue().set(key, value, timeout, unit);}/*** 只有在 key 不存在时设置 key 的值** @param key* @param value* @return 之前已经存在返回false, 不存在返回true*/public  boolean setIfAbsent(String key, String value) {return redisTemplate.opsForValue().setIfAbsent(key, value);}/*** 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始** @param key* @param value* @param offset 从指定位置开始覆写*/public  void setRange(String key, String value, long offset) {redisTemplate.opsForValue().set(key, value, offset);}/*** 获取字符串的长度** @param key* @return*/public  Long size(String key) {return redisTemplate.opsForValue().size(key);}/*** 批量添加** @param maps*/public  void multiSet(Map<String, String> maps) {redisTemplate.opsForValue().multiSet(maps);}/*** 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在** @param maps* @return 之前已经存在返回false, 不存在返回true*/public  boolean multiSetIfAbsent(Map<String, String> maps) {return redisTemplate.opsForValue().multiSetIfAbsent(maps);}/*** 增加(自增长), 负数则为自减** @param key* @return*/public  Long incrBy(String key, long increment) {return redisTemplate.opsForValue().increment(key, increment);}/*** @param key* @return*/public  Double incrByFloat(String key, double increment) {return redisTemplate.opsForValue().increment(key, increment);}/*** 追加到末尾** @param key* @param value* @return*/public  Integer append(String key, String value) {return redisTemplate.opsForValue().append(key, value);}
}

响应类

import com.fasterxml.jackson.annotation.JsonInclude;@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, T data) {this.code = code;this.data = data;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}
}

直接运行

加上数据库配置后直接运行,发现控制台多一句输出,这个就是Security默认生成的一个密码
在这里插入图片描述

  • 访问http://localhost:9911/order/list,会发现有一个登录页面,用户名默认是root
  • 也可以自己配置用户名和密码
    spring:security:user:name: zhangsanpassword: 123456
    
  • 但是以上都不是我们想要自己从数据库进行认证校验的

工具类

package com.micro.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;/*** @author: zjl* @datetime: 2024/6/22* @desc: 复兴Java,我辈义不容辞*/
public class JwtUtil {//有效期为一个小时,可以自定义public static final Long JWT_TTL = 60 * 60 *1000L;//设置秘钥明文,一般用一串随机序列,我这里用随机生成器随机生成的public static final String JWT_KEY = "2CNLZIm61Uq3v7CR";public static String getUUID(){String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成jtw,基于UUID* @param subject token中要存放的数据(json格式)* @return*/public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jtw,基于UUID,设置超时时间* @param subject token中要存放的数据(json格式)* @param ttlMillis token超时时间* @return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid)              //唯一的ID.setSubject(subject)   // 主题  可以是JSON数据.setIssuer("susheng")     // 签发者.setIssuedAt(now)      // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* @param id* @param subject* @param ttlMillis* @return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}/*** 生成加密后的秘钥 secretKey* @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}
}
public class WebUtils
{/*** 将字符串渲染到客户端** @param response 渲染对象* @param string 待渲染的字符串* @return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;}
}

认证业务

  • 自定义一个AccountDetailsServiceImpl,实现import org.springframework.security.core.userdetails.UserDetailsService接口,让SpringSecurity使用自定义的UserDetailsService。

  • AccountDetailsServiceImpl可以从数据库中查询用户名和密码

    package com.micro.service;import com.micro.mapper.AccountMapper;
    import com.micro.pojo.Account;
    import com.micro.pojo.LoginAccount;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;import javax.annotation.Resource;
    import java.util.Objects;
    import org.springframework.stereotype.Service;/*** @author: zjl* @datetime: 2024/6/22* @desc: 复兴Java,我辈义不容辞*/
    @Service
    public class AccountDetailsServiceImpl implements UserDetailsService {@Resourceprivate AccountMapper accountMapper;@Overridepublic UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {Account account = accountMapper.selectAccountByAccountCode(userName);if(Objects.isNull(account)){throw new RuntimeException("用户名或密码错误");}//根据用户查询权限信息 LoginAccount//封装成UserDetails对象返回return new LoginAccount(account);}
    }
    
  • 因为UserDetailsService方法的返回值是UserDetails类型,所以LoginAccount类实现该接口,把用户信息封装在其中。注意构造方法。

    package com.micro.pojo;import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;/*** @author: zjl* @datetime: 2024/6/22* @desc: 复兴Java,我辈义不容辞*/
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class LoginAccount implements UserDetails {private Account account;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return account.getAccountPassword();}@Overridepublic String getUsername() {return account.getAccountCode();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
    }
    

密码加密存储问题

  • 在实际开发中,任何一个系统都不可能把密码直接用明文存储在数据库中
  • 通常默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是一般不会采用这种方式。所以就需要替换PasswordEncoder。
  • 替换策略:一般使用SpringSecurity提供的BCryptPasswordEncoder。
  • 此时只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
  • 然后需要定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
package com.micro.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;/*** @author: zjl* @datetime: 2024/6/22* @desc: 复兴Java,我辈义不容辞*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

登录接口

  • 实现自定义登录接口,登录不能拦截,因此需要让SpringSecurity对这个接口放行,也就是不用登录认证就能访问,否则就死循环了(
    • A:我们需要有工作经验的!
    • B:我需要工作才能有经验!
    • A:你没有经验就不能工作!
    • B:我不工作我哪来的工作经验!)
  • 在接口中通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
  • 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,因此需要把用户信息存入redis,可以把用户id作为key。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
    @Resourceprivate AuthService authService;@PostMapping("/login")public ResponseResult login(String accountCode, String accountPassword){try {return authService.login(accountCode,accountPassword);} catch (JsonProcessingException e) {return new ResponseResult<>(500,"登录失败");}}
    @Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate RedisStringUtil redisStringUtil;@Resourceprivate ObjectMapper objectMapper;public ResponseResult login(String accountCode, String accountPassword) throws JsonProcessingException {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountCode,accountPassword);Authentication authenticate = authenticationManager.authenticate(authenticationToken);if (Objects.isNull(authenticate)) {throw new RuntimeException("用户名或密码错误");}//使用userid生成tokenLoginAccount loginAccount = (LoginAccount) authenticate.getPrincipal();String userId = String.valueOf(loginAccount.getAccount().getId());String jwt = JwtUtil.createJWT(userId);//authenticate存入redisredisStringUtil.set("login:" + id, JSON.toJSONString(loginAccount));//把token响应给前端HashMap<String, String> map = new HashMap<>();map.put("token", jwt);return new ResponseResult(200, "登陆成功", map);}

认证过滤器

  • 需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。
  • 使用userid去redis中获取对应的LoginUser对象。
  • 然后封装Authentication对象存入SecurityContextHolder
package com.micro.filter;import com.fasterxml.jackson.databind.ObjectMapper;
import com.micro.pojo.LoginAccount;
import com.micro.utils.JwtUtil;
import com.micro.utils.RedisStringUtil;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;/*** @author: zjl* @datetime: 2024/6/22* @desc: 复兴Java,我辈义不容辞*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Resourceprivate RedisStringUtil redisStringUtil;@Resourceprivate ObjectMapper objectMapper;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//放行filterChain.doFilter(request, response);return;}//解析tokenString userid;try {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}//从redis中获取用户信息String redisKey = "login:" + userid;JSONObject jsonObject = JSONObject.parseObject(redisStringUtil.get(redisKey));LoginAccount loginAccount = objectMapper.convertValue(jsonObject, LoginAccount.class);if(Objects.isNull(loginAccount)){throw new RuntimeException("用户未登录");}//存入SecurityContextHolder//获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginAccount,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Resourceprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//把token校验过滤器添加到过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

退出登陆

  • 这个简单,只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。
	@Resourceprivate RedisKeyUtil redisKeyUtil;public ResponseResult logout() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginAccount loginAccount = (LoginAccount) authentication.getPrincipal();int userid = loginAccount.getAccount().getId();redisKeyUtil.delete("login:"+userid);return new ResponseResult(200,"退出成功");}

测试

访问:127.0.0.1:9911/order/list
在这里插入图片描述

访问:127.0.0.1:9911/login,输入用户名和密码

在这里插入图片描述

相关文章:

【SpringSecurity】认证与鉴权框架SpringSecurity——认证

目录 SpringSecurity介绍特性CSRF攻击攻击模式攻击原理预防手段 XSS攻击攻击模式危害预防手段 SpringSecurity预防CSRF攻击SpringSecurity预防XSS攻击SpringSecurity与OAuth2的关系SpringSecurity的核心功能 代码实战依赖定义一个接口Redis工具类响应类直接运行工具类认证业务密…...

Video-Infinity:利用多设备加速长视频生成

Video-Infinity基于 VideoCrafter2&#xff0c;利用了分布式推理方法&#xff0c;可以在多个 GPU 上并行生成长视频。在 8 个 Nvidia Ada 6000 GPU 上&#xff0c;只需 5 分钟即可生成 95 秒长的视频&#xff0c;比 Streaming T2V 快 100 倍。...

水下机器人ArduSub 固件常用参数

目前最新版的ArduSub 固件是4.1.2&#xff0c;本文的参数是基于这个版本的固件 SURFACE_DEPTH&#xff1a;水表深度读数 当水下机器人在水面时&#xff0c;水压传感器将读取的深度数据&#xff08;以厘米为单位&#xff09;&#xff0c;这个相当于抵消零偏 单位&#xff1a;…...

AI智能体 | 扣子Coze 工作流中如何嵌入代码,看这一篇就够了

Coze的工作流中除了能嵌入大模型&#xff0c;插件&#xff0c;图像流&#xff0c;其他工作流外&#xff0c;还能嵌入代码。嵌入代码的好处是对一些复杂的返回结果进行二次处理。 Coze的代码支持js和python两种语言。这次用python来做演示介绍 在节点中选择代码 弹出对话框如下…...

Transformer2--Multi-head self-attention

一、背景 为什么是multi-head self attention? 并行的处理多种注意力模式&#xff1a; 每个注意力头使用不同的线性变换&#xff0c;这意味着它们可以从输入序列的不同子空间中学习不同的特征关联。这样一来&#xff0c;模型可以通过多个注意力头同时关注输入序列的不同方面&…...

11. Java线程的状态详解

1. 前言 本节内容主要是对多线程的 6 种状态进行详细讲解&#xff0c;具体内容点如下&#xff1a; 抛开语言&#xff0c;谈操作系统的线程的生命周期及线程 5 种状态&#xff0c;这是我们学习 Java 多线程 6 种状态的基础&#xff1b;掌握 Java 的线程生命周期及 6 种线程状态…...

ubuntu永久换镜像源详细方法

1.查看ubuntu的版本&#xff0c;不同的版本对应的不同的镜像源&#xff08;下面会讲到&#xff0c;先按步骤操作即可&#xff09; cat /etc/issue 2.先备份一个&#xff0c;防止更改错误 cp /etc/apt/sources.list /etc/apt/sources.list.backup 3.备份好之后删除原有的sour…...

docker部署(第一期)(相关命令)

腾讯云-----------镜像centOS 7.9 64位置---------------分配独立公网IP&#xff08;远程连接&#xff09;----------带宽选择最大-------------新建安全组-------------设置服务器密码---------WindTerm远程连接服务器 WindTerm下载&#xff1a;https://github.com/kingToolb…...

华为云L实例云服务器部署前后端分离项目全过程记录

1、配置云服务器 https://blog.csdn.net/modaoshi51991/article/details/139547630 2、上传前端项目并配置nginx 首先在前端项目运行以下代码将项目打包 我遇到node.js和openssl两个版本不兼容问题&#xff0c;更新版本依次解决即可 vue-cli-service build看一下package.json…...

uniapp - 微信小程序 - 自定义底部tabbar

废话不多说&#xff0c;直接行源码 这里需要的底部tabbar的图片在这里 我的资源里面呢 图片是这样的 先看成品吧 首先 - BaseApp\components\Tabbar.vue <script setup>import {ref,nextTick,watch} from "vue"// 核心 - 隐藏uniapp自带的底部tabbaruni.hi…...

每日一题——Python实现PAT乙级1030 完美数列(举一反三+思想解读+逐步优化)五千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 初次尝试 再次尝试 代码结构 时间复杂度分析 空间复杂度分析 总结 我要更强 时…...

【C/C++】this指针的概念和作用

目录 一、this指针的概念 二、this指针的作用 2.1 访问当前对象的成员 2.2 返回对象本身 2.3 区分对象 2.4 在构造函数和析构函数中 2.5 在类的内部调用其他成员函数 2.6 作为参数传递 三、this指针使用 3.1 this指针的使用 3.2 C++ 中this指针使用 一、this…...

Spring Bean 的生命周期

在 Spring 框架中&#xff0c;Bean 的生命周期由 Spring 容器管理&#xff0c;从创建到销毁&#xff0c;Spring 提供了多种方式来定制 Bean 的初始化和销毁过程。本文将详细介绍 Spring Bean 的生命周期&#xff0c;包括 Bean 的初始化和销毁、自定义初始化方法和销毁方法。 一…...

锐起RDV5高性能云桌面

锐起是上海锐起信息技术有限公司旗下品牌。该公司创立于 2001 年&#xff0c;是桌面虚拟化产品和解决方案提供商&#xff0c;专注于桌面管理系统和私有云存储系统的系列软件产品研发&#xff0c;致力于简化 IT 管理、增强系统安全&#xff0c;提供简单、易用、稳定、安全的产品…...

pandas减少dataframe占用内存的若干方法

一、只获取文件需要的列&#xff0c;避免加载整个文件 举例&#xff1a;只获取A.B两列数据 df pd.read_csv(123.csv, usecols[A, B]) 二、使用更准确的数据类型&#xff0c;减少内存空间占用 import pandas as pd import numpy as np # 假设你的CSV文件有三列&#xff0…...

Ubuntu20.04 64位 安装docker(有问题可评论沟通交流)

1、查看系统版本 cat /proc/version 2、卸载可能存在或未安装成功的docker&#xff08;新系统无需操作&#xff09; apt-get remove docker docker-engine docker-ce docker.io 3、更新apt-get apt-get update 4、安装软件包允许apt-get通过 HTTPS 使用存储库 apt-get install …...

【C++PCL】点云处理Kd树和八叉树区别

作者:迅卓科技 简介:本人从事过多项点云项目,并且负责的项目均已得到好评! 公众号:迅卓科技,一个可以让您可以学习点云的好地方 重点:每个模块都有参数如何调试的讲解,即调试某个参数对结果的影响是什么,大家有问题可以评论哈,如果文章有错误的地方,欢迎来指出错误的…...

makefile学习过程

makefile 完美教程 - WittXie - 博客园 (cnblogs.com) Makefile教程&#xff08;绝对经典&#xff0c;所有问题看这一篇足够了&#xff09;-CSDN博客 Makefile入门(超详细一文读懂)-CSDN博客 最实用的Makefile教程 真的很简单&#xff08;搞不明白网上的教程写那么复杂干嘛&…...

Kompas AI数据分析与预测功能对比

一、引言 在现代商业环境中&#xff0c;数据分析与预测是企业制定战略决策的关键工具。通过对大量数据的分析&#xff0c;企业能够识别趋势、预测未来变化&#xff0c;并做出更为明智的决策。本文将对比Kompas AI与其他主要AI产品在数据分析与预测方面的能力&#xff0c;展示K…...

Appium+python自动化(二十五)- 那些让人抓耳挠腮、揪头发和掉头发的事 - 获取控件ID(超详解)

简介 在前边的第二十二篇文章里&#xff0c;已经分享了通过获取控件的坐标点来获取点击事件的所需要的点击位置&#xff0c;那么还有没有其他方法来获取控件点击事件所需要的点击位置呢&#xff1f;答案是&#xff1a;Yes&#xff01;因为在不同的大小屏幕的手机上获取控件的坐…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录

ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录&#xff0c;以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

376. Wiggle Subsequence

376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

微信小程序 - 手机震动

一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注&#xff1a;文档 https://developers.weixin.qq…...

Linux云原生安全:零信任架构与机密计算

Linux云原生安全&#xff1a;零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言&#xff1a;云原生安全的范式革命 随着云原生技术的普及&#xff0c;安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测&#xff0c;到2025年&#xff0c;零信任架构将成为超…...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行&#xff0c;YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID&#xff1a; YW3…...

GitHub 趋势日报 (2025年06月08日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...

leetcodeSQL解题:3564. 季节性销售分析

leetcodeSQL解题&#xff1a;3564. 季节性销售分析 题目&#xff1a; 表&#xff1a;sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)

Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败&#xff0c;具体原因是客户端发送了密码认证请求&#xff0c;但Redis服务器未设置密码 1.为Redis设置密码&#xff08;匹配客户端配置&#xff09; 步骤&#xff1a; 1&#xff09;.修…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...