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

news2024/12/23 18:56:32

目录

  • 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;
    @Autowired
    public 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;
    @Autowired
    public 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: zhangsan
          password: 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 {
        @Resource
        private AccountMapper accountMapper;
        @Override
        public 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;
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return null;
        }
    
        @Override
        public String getPassword() {
            return account.getAccountPassword();
        }
    
        @Override
        public String getUsername() {
            return account.getAccountName();
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public 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 {
    @Bean
    public 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 {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
    @Resource
    private AuthService authService;
    @PostMapping("/login")
    public ResponseResult login(String accountCode, String accountPassword){
        try {
            return authService.login(accountCode,accountPassword);
        } catch (JsonProcessingException e) {
            return new ResponseResult<>(500,"登录失败");
        }
    }
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private RedisStringUtil redisStringUtil;
    @Resource
    private 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生成token
        LoginAccount loginAccount = (LoginAccount) authenticate.getPrincipal();
        String userId = String.valueOf(loginAccount.getAccount().getId());
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisStringUtil.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 {

    @Resource
    private RedisStringUtil redisStringUtil;
    @Resource
    private ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        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 {

    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Override
    protected 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
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

退出登陆

  • 这个简单,只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。
	@Resource
    private 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.coloradmin.cn/o/1850476.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

简易智能家居系统

文章目录 摘要一、系统设计要求及总体设计方案1.1 设计要求1.2 总体设计方案 二、终端结点的设计及实现2.1单片机最小系统2.2 LED灯的控制与工作状态的显示2.2.1 硬件设计2.2.2 软件设计 2.3 温度的测量与显示2.4 火灾的监测与报警2.5 串口的显示与控制 三、网络传输与控制3.1 …

three.js 第十一节 - uv坐标

// ts-nocheck // 引入three.js import * as THREE from three // 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls // 导入lil.gui import { GUI } from three/examples/jsm/libs/lil-gui.module.min.js // 导入tween import * as T…

计算机网络(概述)

该笔记为湖科大计算机网络相关笔记、教材参考计算机网络第六版 湖科大计算机网络 计算机网络概述 因特网概述 Internet和internet的区别 internet&#xff1a;只要是计算机与计算机连接&#xff0c;形成了网络&#xff0c;就可以叫internet Internet&#xff1a;泛指全世界的…

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

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

【数据分享】《中国法律年鉴》1987-2022

而今天要免费分享的数据就是1987-2022年间出版的《中国法律年鉴》并以多格式提供免费下载。&#xff08;无需分享朋友圈即可获取&#xff09; 数据介绍 自1987年起&#xff0c;《中国法律年鉴》作为一部全面记录中国法律发展进程的重要文献&#xff0c;见证了中国法治建设的每…

C语言| 数组的折半查找

数组的折半查找 折半查找&#xff1a;在已经排好序的一组数据中快速查找数据。 先排序&#xff0c;再使用折半查找。 【折半查找的运行过程】 1 存储数组下标 low最小的下标&#xff0c;mid中间的下标&#xff0c; high最大的下标 2 key存放查找的值&#xff0c;每一次对比后…

2024年,业绩大爆发的企业,都做对了一件事

作为新质生产力之一的AI技术&#xff0c;已经完成了从实验室到场景应用的“惊险一跃”&#xff0c;这背后离不开云计算、大数据技术的日趋成熟。与此同时&#xff0c;大模型、柔性计算等创新的云基础设施解决方案&#xff0c;为企业降本增效、快速高质量地发展&#xff0c;提供…

电影数据集关联分析及FP-Growth实现

&#xff08;1&#xff09;数据预处理 我们先对数据集进行观察&#xff0c;其属性为’movieId’ ‘title’ ‘genres’&#xff0c;其中’movieId’为电影的序号&#xff0c;但并不完整&#xff0c;‘title’为电影名称及年份&#xff0c;‘genres’为电影的分类标签。…

【从0实现React18】 (一) 项目初始化

Multi-repo 和 Mono-repo 由于需要同时管理多个包&#xff0c;如React、React-dom等&#xff0c;所以选择**Mono-repo** 选择使用pnpm-workspace搭建Mono-repo环境的原因 依赖安装快更规范 Pnpm初始化 npm install -g pnpm pnpm init配置pnpm-workspace.yml文件 pnpm-work…

【C++提高编程-11】----C++ STL常用集合算法

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

vscode插件path-intellisense失效原因

很可能是因为设置中的自动补全部分除了问题。 问题 作者自身是因为使用了copilot之后&#xff0c;感觉vscod自带的自动补全(设置里面叫"建议"&#xff0c;或者inttelisense)就没必要了&#xff0c;然后一通改设置把建议关掉之后发现插件path-intellisense也不能用了…

CATIA_DELMIA_V5R2019安装包下载及安装教程破解

以下为V5-6R2019安装说明 1.将两卷安装文件解压到同一目录内&#xff0c;互相覆盖即可 &#xff08;按用户需要下载 CATIA 或者DELMIA&#xff09; 以上为 CATIA 的安装包 以上为 DELMIA 的安装包 两者合并到一起&#xff0c;同一目录 2.解压后运行setup.exe 如遇到报错&…

[信号与系统]关于LTI系统的转换方程、拉普拉斯变换和z变换

前言 本文还是作为前置知识。 LTI系统的传递函数 LTI系统的传递函数 H ( z ) H(z) H(z) 是输出信号的z变换 Y ( z ) Y(z) Y(z) 与输入信号的z变换 X ( z ) X(z) X(z) 的比值&#xff1a; H ( z ) Y ( z ) X ( z ) H(z) \frac{Y(z)}{X(z)} H(z)X(z)Y(z)​ 多项式比值表…

【实战分享】雷池社区版助力构建高可用、安全的Web应用架构

引言 在日益复杂的网络环境中&#xff0c;构建坚不可摧的安全防线成为每一位网站守护者的重要使命。本文将深入剖析一套集CDN加速、高效Nginx代理与雷池WAF深度防护于一体的现代网站安全架构设计&#xff0c;特别强调雷池WAF在此架构中的核心作用及其对整体安全性的提升策略。…

作为一名程序员,怎么才能写出简洁实用还漂亮的代码楼呢?这25个超惊艳的Python代码写法,你一定要学会!

前言 Python可以用于复杂的数据分析和Web开发项目&#xff0c;还能以极少的代码行数完成令人惊叹的任务。本文将分享25个简短的Python代码示例&#xff0c;用来展示Python编程语言的魅力和效率。 1.列表推导式 Python的列表推导式提供了一种优雅的方法来创建列表。 # 将一个…

渗透测试-若依框架的杀猪交易所系统管理后台

前言 这次是带着摸鱼的情况下简单的写一篇文章&#xff0c;由于我喜欢探究黑灰产业&#xff0c;所以偶尔机遇下找到了一个加密H币的交易所S猪盘&#xff0c;我记得印象是上年的时候就打过这一个同样的站&#xff0c;然后我是通过指纹查找其它的一些站&#xff0c;那个站已经关…

高通安卓12-在源码中查找应用的方法

1.通过搜索命令查找app 一般情况下&#xff0c;UI上看到的APP名称会在xml文件里面定义出来&#xff0c;如 搜索名字为WiGig的一个APP 执行命令 sgrep "WiGig" 2>&1|tee 1.log 将所有的搜索到的内容打印到log里面 Log里面会有一段内容 在它的前面是这段内…

针对 AI 优化数据湖仓一体:使用 MinIO 仔细了解 RisingWave

RisingWave 是现代数据湖仓一体处理层中的开源流数据库&#xff0c;专为性能和可扩展性而构建。RisingWave 旨在允许开发人员在流数据上运行 SQL。鉴于 SQL 是数据工程的通用语言&#xff0c;此功能非常重要。它具有强大的架构&#xff0c;包括计算节点、元节点和压缩器节点&am…

Docker可视化web工具

docker run --restart always --name docker.ui -d -v /var/run/docker.sock:/var/run/docker.sock -p 8989:8999 joinsunsoft/docker.ui #--restart always&#xff1a;重启策略&#xff0c;只要关闭就会重启 http://192.168.10.51:8989 账号&#xff1a;ginghan 密码&#xf…

ruoyi添加自己的菜单

先把自己自定义的view填写好 在菜单管理模块 因为我已经新增过&#xff0c;所以就看看我填的啥就行了 我发现一个问题&#xff0c;路由地址可以填index2或者scooldemo/index2都可以&#xff08;这个包含了文件夹路径&#xff09;&#xff0c;反正组件路径一定要填对就可以了。 …