【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;/*** @author: zjl* @datetime: 2024/6/22* @desc: 复兴Java,我辈义不容辞*/
    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.getAccountName();}@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:" + userId, objectMapper.writeValueAsString(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;LoginAccount loginAccount = objectMapper.convertValue(redisStringUtil.get(redisKey), 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,输入用户名和密码

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/356678.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VERYCLOUD睿鸿股份亮相亚马逊云科技中国峰会2024

5月30日&#xff0c;为期两天的亚马逊云科技中国峰会在上海世博中心圆满落幕。 多位大咖现场分享&#xff0c;生成式AI时代的数据战略&#xff0c;企业级AI应用&#xff0c;最新技术、产品重磅发布&#xff0c;创新行业解决方案 …… 作为亚马逊云科技的生态合作伙伴&#x…

Android面试题:App性能优化之Java和Kotlin常见的数据结构

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 Java常见数据结构特点 ArrayList ArrayList底层是基于数组实现add、删除元素需要进行元素位移耗性能&#xff0c;但查找和修改块适合不需要频…

GPT-4o的视觉识别能力,将绕过所有登陆的图形验证码

知识星球&#x1f517;除了包含技术干货&#xff1a;《Java代码审计》《Web安全》《应急响应》《护网资料库》《网安面试指南》还包含了安全中常见的售前护网案例、售前方案、ppt等&#xff0c;同时也有面向学生的网络安全面试、护网面试等。 我们来看一下市面上常见的图形验证…

状态压缩DP——AcWing 291. 蒙德里安的梦想

状态压缩DP 定义 状态压缩DP是一种利用二进制数来表示状态的动态规划算法。它通过将状态压缩成一个整数&#xff0c;从而减少状态数量&#xff0c;提高算法效率。 运用情况 状态压缩DP通常用于解决具有状态转移和最优解性质的问题&#xff0c;例如组合优化、图论、游戏等问…

Vue82-组件内路由守卫

一、组件内路由守卫的定义 在一个组件里面去写路由守卫&#xff0c;而不是在路由配置文件index.js中去写。 此时&#xff0c;该路由守卫是改组件所独有的&#xff01; 只有通过路由规则进入的方式&#xff0c;才会调这两个函数&#xff0c;否则&#xff0c;若是只是用<Ab…

LogicFlow 学习笔记——9. LogicFlow 进阶 节点

LogicFlow 进阶 节点&#xff08;Node&#xff09; 连线规则 在某些时候&#xff0c;我们可能需要控制边的连接方式&#xff0c;比如开始节点不能被其他节点连接、结束节点不能连接其他节点、用户节点后面必须是判断节点等&#xff0c;想要达到这种效果&#xff0c;我们需要为…

【经验分享】RT600 serial boot mode测试

【经验分享】RT600 serial boot mode测试 一&#xff0c; 文档描述二&#xff0c; Serial boot mode测试2.1 evkmimxrt685_gpio_led_output 工程测试2.2 evkmimxrt685_dsp_hello_world_usart_cm33工程测试 一&#xff0c; 文档描述 RT600的启动模式共支持4种&#xff1a; 1&am…

C++设计模式——Composite组合模式

一&#xff0c;组合模式简介 真实世界中&#xff0c;像企业组织、文档、图形软件界面等案例&#xff0c;它们在结构上都是分层次的。将系统分层次的方式使得统一管理和添加不同子模块变得容易&#xff0c;在软件开发中&#xff0c;组合模式的设计思想和它们类似。 组合模式是…

数据库设计概述-数据库设计内容、数据库设计方法(基于E-R模型的规范设计方法)

一、引言 如何利用关系数据库理论设计一个满足应用系统需求的数据库 二、数据库设计内容 1、数据库设计是基于应用系统需求分析中对数据的需求&#xff0c;解决数据的抽象、数据的表达和数据的存储结构等问题 2、其目标是设计出一个满足应用要求、简洁、高效、规范合理的数…

Redis 集群 - 数据分片算法

前言 广义的集群&#xff1a;只要是多个机器构成了一个分布式系统&#xff0c;都可以被称为集群。 狭义的集群&#xff1a;redis 的集群模式&#xff0c;这个集群模式下&#xff0c;主要是解决存储空间不足的问题。 Redis 集群 redis 采用主从结构&#xff0c;可以提高系统的可…

「动态规划」如何求最长湍流子数组的长度?

78. 最长湍流子数组https://leetcode.cn/problems/longest-turbulent-subarray/description/ 给定一个整数数组arr&#xff0c;返回arr的最长湍流子数组的长度。如果比较符号在子数组中的每个相邻元素对之间翻转&#xff0c;则该子数组是湍流子数组。更正式地来说&#xff0c;…

从开源EPR产品Odoo学习

前言 一个先进、敏捷、经济高效、可快速扩展的Odoo免费开源企业信息化解决方案&#xff0c;让企业获得适应未来发展的长期创新和增长能力。 Odoo 的免费开源模式 让我们可利用无数开发人员和业务专家&#xff0c;在短短数年内&#xff0c;打造数百款应用。凭借强大的技术基础&…

苹果智能和人工智能最大化

苹果智能和人工智能最大化 除了苹果公司&#xff0c;还没有人真正使用过苹果的智能功能。它要到秋天才会分阶段发布&#xff0c;即使到那时&#xff0c;它也无法在80%或90%的iPhone安装基础上运行&#xff0c;因为它需要只有iPhone 15 Pro才能使用的设备上处理功能。没有什么能…

现在这个行情,又又又要开始准备面试了~~

亲爱的程序员朋友们: 这些资料曾经帮助过许多有志之士顺利拿下抖音、快手、阿里等大厂的Offer&#xff0c;现在也希望它们能为你的面试旅程助力&#xff01; 关注【程序员世杰】回复【1024】惊喜等你来拿&#xff01; 截图 关注【程序员世杰】回复【1024】惊喜等你来拿&#xf…

车辆轨迹预测系列 (三):nuScenes数据集详细介绍-1

车辆轨迹预测系列 (三)&#xff1a;nuScenes数据集详细介绍-1 文章目录 车辆轨迹预测系列 (三)&#xff1a;nuScenes数据集详细介绍-1一、数据集准备1、解压2、安装nuscenes-devkit3、介绍 二、架构内容解释1、category 类别2、attribute 属性3、visibility 可见性4、instance …

包含网关的概念及案例演示

包容网关 知识点讲解 包容网关可以看作排他网关和并行网关的结合体。与排他网一样&#xff0c;可以在外出顺序流上定义条件&#xff0c;但与排他网关不同的是&#xff0c; 进行决策判读时&#xff0c;包容网关所有条件为true的后继分支都会被依次执行。如果所有分支条件都为fa…

IMU用于飞行坐姿校正

为了提升长途飞行的舒适度并预防乘客因不良坐姿导致的身体不适&#xff0c;来自荷兰上海两所大学的研究团队携手开发出一种创新的“舒适穿戴”设备&#xff0c;专为识别飞行中的坐姿设计。 研究团队制作了两种原型设备&#xff1a;一种追求极致舒适&#xff0c;另一种为紧身设…

干货!!SSAS模型刷新步骤

白茶在上一篇文章PowerBI迁移到SSAS向小伙伴们介绍了如何将已经开发好的PowerBI模型迁移到SSAS整个操作过程&#xff0c;与此同时也带来了新的问题&#xff0c;那就是SSAS的模型该如何刷新呢&#xff1f; 配套工具 SSMS Visual Studio SSIS SSIS[1]的全称是SQL Server Inte…

桂电人工智能学院大数据实验,使用 Docker 搭建 hadoop 集群

桂电人工智能学院大数据实验&#xff0c;使用 Docker 搭建 hadoop 集群 第一步 安装 Docker, Windows 上可以使用 Docker Desktop 下载地址&#xff1a;https://www.docker.com/products/docker-desktop/ 安装过程自行谷歌 安装好的标志&#xff1a;打开终端 运行docker p…

JetBrains PyCharm 2024 mac/win版编程艺术,智慧新篇

JetBrains PyCharm 2024是一款功能强大的Python集成开发环境(IDE)&#xff0c;专为提升开发者的编程效率和体验而设计。这款IDE不仅继承了前代版本的优秀特性&#xff0c;还在多个方面进行了创新和改进&#xff0c;为Python开发者带来了全新的工作体验。 JetBrains PyCharm 20…