目录
- 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 攻击的方式多样,常见的包括以下几种:
- 存储型(Persistent)XSS:攻击者将恶意脚本存储到目标网站的数据库中,当其他用户浏览受影响的页面时,恶意脚本会从服务器上取出并在用户浏览器中执行。
- 反射型(Reflected)XSS:攻击者通过构造恶意链接或欺骗用户点击恶意链接,将恶意脚本作为参数传递给目标网站,目标网站将该参数作为响应的一部分返回给用户,用户浏览器执行恶意脚本。
- DOM-based XSS:攻击者通过修改浏览器DOM(Document Object Model)中的内容,使得恶意脚本在浏览器中执行。
危害
- XSS 攻击的危害包括窃取用户敏感信息(例如用户名、密码、Cookie)、篡改页面内容、重定向用户到恶意网站、执行恶意操作等。
预防手段
为了防止 XSS 攻击,开发人员可以采取以下措施:
- 输入验证与过滤:对用户输入的数据进行验证和过滤,确保输入的数据符合预期格式。例如,对于文本输入,可以使用特殊字符转义或过滤函数来防止恶意注入。
- 输出转义:在将用户输入展示在页面上时,对特殊字符进行转义或过滤,确保用户输入的内容不会被作为脚本执行。
- 使用安全的编码和解码:使用安全的编码函数,如将用户输入进行 HTML 实体编码或 URL 编码,以防止特殊字符的执行。
- 设置 HTTP 头的 Content-Security-Policy(CSP):使用 Content Security Policy 头来指定允许加载和执行的内容源,可以减少 XSS 攻击的风险。
- 使用浏览器的内置防护机制:现代浏览器通常会提供一些内置的防护机制,如自动过滤或阻止一些可疑的恶意脚本。
综上所述,防止 XSS 攻击是非常重要的,开发人员应该采取适当的防护措施来确保用户数据的安全性。
SpringSecurity预防CSRF攻击
- CSRF Token:Spring Security 在处理表单提交时,会生成一个随机的 CSRF Token,并将其包含在表单中或作为请求头的一部分发送到后端。后端校验请求中的 Token 是否与会话中存储的 Token 相匹配,如果不匹配,则拒绝该请求。
- SameSite Cookie:在 Spring Security 5.x 版本中,可以通过配置 SameSite 属性来设置 Cookie 的 SameSite 属性为 Strict 或 Lax,以控制 Cookie 是否允许跨站点发送。Strict 模式下,Cookie 只能在同站点请求中发送,Lax 模式下,某些情况下允许跨站点发送,但仅限于 GET 请求。
- 验证 HTTP Referer:Spring Security 可以配置验证请求头中的 Referer 字段来检查请求来源是否合法。这种方式需要目标网站的所有请求都来自同一个域名,并且不会存在跨域请求。
- 验证 Origin 头:Spring Security 还可以配置验证请求头中的 Origin 字段来检查请求来源是否合法。与 Referer 验证不同,Origin 头是 HTML5 中引入的一种更安全的验证机制。
- 避免使用 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 攻击的建议:
- 输入验证和过滤:对用户输入的数据进行验证和过滤,限制特殊字符、HTML 标签和脚本等,确保用户提供的数据不会被解析为可执行的脚本。
- 输出编码:在将用户输入的数据渲染到网页上时,使用合适的编码方式对数据进行转义,确保任何潜在的脚本都被当作文本而不是可执行的代码来处理。
- 使用安全的框架和工具:使用安全性较高的框架和工具来处理用户输入和输出,例如,使用 Spring Security 提供的 Thymeleaf、JSTL 或 HTML 转义库,这些库可以自动转义用户输入的数据。
- CSP(Content Security Policy):在 HTTP 响应头中设置 Content-Security-Policy,通过限制页面可以加载的资源来源和类型,来减少 XSS 攻击的风险。
- 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
,输入用户名和密码