session 认证流程:
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
session 认证存在的问题
什么是Token? (令牌)
服务器对 Token 的存储方式:
Token特点:
token 的身份验证流程:
每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
注意:
登录时 token 不宜保存在 localStorage,被 XSS 攻击时容易泄露。所以比较好的方式是把 token 写在 cookie 里。为了保证 xss 攻击时 cookie 不被获取,还要设置 cookie 的 http-only。这样,我们就能确保 js 读取不到 cookie 的信息了。再加上 https,能让我们的请求更安全一些。
token认证方式的优缺点
如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
什么是JWT
JWT有什么用
JWT认证方式
头部(Header)
{"typ":"JWT","alg":"HS256"}
//typ(Type):令牌类型,也就是 JWT。
//alg(Algorithm) :签名算法,比如 HS256。
JWT签名算法中,一般有两个选择,一个采用HS256,另外一个就是采用RS256
进行BASE64编码https://base64.us/,编码后的字符串如下:eyJhbGciOiJIUzI1NiJ9
载荷(payload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
{"sub":"1234567890","name":"John Doe","admin":true}
将上面的JSON数据进行base64编码,得到Jwt第二部分: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
字段说明,下面的字段都是由 JWT的标准所定义的
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。
签名(signature)
服务器通过 Payload、Header 和一个密钥(Secret) 使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
Signature 部分是对前两部分的签名,作用是防止 Token(主要是 payload) 被篡改。
这个签名的生成需要用到:
- Header + Payload。
- 存放在服务端的密钥(一定不要泄露出去)。
- 签名算法。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
String encodeString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
String secret = HMACSHA256(encodeString,secret);
签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
最后一步签名的过程,实际上是对头部以及载荷内容进行签名。
一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。
所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用 alg
字段指明了我们的加密算法了。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
Token 和 JWT (JSON Web Token) 都是用来在客户端和服务器之间传递身份验证信息的一种方式。但是它们之间有一些区别。
总的来说,JWT 是一种特殊的 Token,它具有更强的安全性和可靠性。
使用jjwt实现jwt的签发和解析获取payload中的数据.
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0)。
官方文档:https://github.com/jwtk/jjwt
io.jsonwebtoken
jjwt
0.9.1
@SpringBootTest
class SpringsecurityExampleApplicationTests {
@Test
void contextLoads() {
}
@Test
public void testJJWT(){
JwtBuilder builder = Jwts.builder()
.setId("9527") //设置唯一ID
.setSubject("hejiayun_community") //设置主体
.setIssuedAt(new Date()) //设置签约时间
.signWith(SignatureAlgorithm.HS256, "mashibing");//设置签名 使用HS256算法,并设置SecretKey
//压缩成String形式,签名的JWT称为JWS
String jws = builder.compact();
System.out.println(jws);
/**
* eyJhbGciOiJIUzI1NiJ9.
* eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM1ODY2fQ.
* ybkDJLVj1Fsi8m3agyxtyd0wxv7lHDqCWNOLN-eOxC8
*/
}
}
运行打印结果:
eyJhbGciOiJIUzI1NiJ9.
eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM1ODY2fQ.
ybkDJLVj1Fsi8m3agyxtyd0wxv7lHDqCWNOLN-eOxC
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
解析JJWS的方法如下:
Jwts.parser()
方法创建 JwtParserBuilder
实例。setSigningKey()
与builder中签名方法signWith()对应,parser中的此方法拥有与signWith()方法相同的三种参数形式,用于设置JWT的签名key,用户后面对JWT进行解析。parseClaimsJws(String)
用您的jws调用该方法,生成原始的JWS。 @Test
public void parserJWT(){
String JWS = "eyJhbGciOiJIUzI1NiJ9." +
"eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM1ODY2fQ." +
"ybkDJLVj1Fsi8m3agyxtyd0wxv7lHDqCWNOLN-eOxC8";
//claims = 载荷 (payload)
try {
Claims claims = Jwts.parser().setSigningKey("mashibing")
.parseClaimsJws(JWS)
.getBody();
System.out.println(claims);
} catch (Exception e) {
System.out.println("Token验证失败! !");
e.printStackTrace();
}
}
运行打印结果:
{jti=9527, sub=hejiayun_community, iat=1681135866}
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。
sub: jwt所面向的用户
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。
@Test
public void testJJWT2(){
long currentTimeMillis = System.currentTimeMillis();
Date expTime = new Date(currentTimeMillis);
JwtBuilder builder = Jwts.builder()
.setId("9527") //设置唯一ID
.setSubject("hejiayun_community") //设置主体
.setIssuedAt(new Date()) //设置签约时间
.setExpiration(expTime) //设置过期时间
.signWith(SignatureAlgorithm.HS256, "mashibing");//设置签名 使用HS256算法,并设置SecretKey
//压缩成String形式,签名的JWT称为JWS
String jws = builder.compact();
System.out.println(jws);
/**
* eyJhbGciOiJIUzI1NiJ9.
* eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM3MjI0LCJleHAiOjE2ODExMzcyMjR9.
* evc01MRxLjpbksbMLdVPM9sJGYGhpC3UYOfm4-0sMGE
*/
}
打印效果: 异常信息: JWT签名与本地计算的签名不匹配。JWT有效性不能断言,也不应该被信任
Token验证失败! !
io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found:
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
创建测试类,并设置测试方法:
@Test
public void testJJWT3(){
long currentTimeMillis = System.currentTimeMillis()+100000000L;
Date expTime = new Date(currentTimeMillis);
JwtBuilder builder = Jwts.builder()
.setId("9527") //设置唯一ID
.setSubject("hejiayun_community") //设置主体
.setIssuedAt(new Date()) //设置签约时间
.setExpiration(expTime) //设置过期时间
.claim("roles","admin") //设置角色
.signWith(SignatureAlgorithm.HS256, "mashibing");//设置签名 使用HS256算法,并设置SecretKey
//压缩成String形式,签名的JWT称为JWS
String jws = builder.compact();
System.out.println(jws);
/**
* eyJhbGciOiJIUzI1NiJ9.
* eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM3MjI0LCJleHAiOjE2ODExMzcyMjR9.
* evc01MRxLjpbksbMLdVPM9sJGYGhpC3UYOfm4-0sMGE
*/
}
解析TOKEN,打印结果
{jti=9527, sub=hejiayun_community, iat=1681137464, exp=1681237464, roles=admin}
1) AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
的职责也就非常明确: 处理所有HTTP Request和Response对象,并将其封装成AuthenticationMananger可以处理的Authentication。2) AuthenticationManager
ProviderManager
ProviderManager
是Authentication的一个实现,并将具体的认证操作委托给一系列的AuthenticationProvider来完成,从而可以实现支持多种认证方式。3) AbstractUserDetailsAuthenticationProvider
ProviderManager 本身并不直接处理身份认证请求,它会委托给内部配置的Authentication Provider列表providers。该列表会进行循环遍历,依次对比匹配以查看它是否可以执行身份验证
providers集合的泛型是AuthenticationProvider接口,AuthenticationProvider接口有多个实现子类
4) DaoAuthenticationProvider
5) UserDetailsService
6) InMemoryUserDetailsManager
虽然 Spring Security 看似很复杂,但是其核心思想和以前那种简单的认证流程依然是一样的。只不过,Spring Security 将其中的关键部分抽象了处理,又提供了相应的扩展接口。
我们在使用时,便可以实现自己的 UserDetailsService 和 UserDetails 来获取保存用户信息,实现自己的 Authentication 来保存特定的用户认证信息, 实现自己的 AuthenticationProvider 使用自己的 UserDetailsService 和 Authentication 来对用户认证信息进行效验。
登录操作
org.springframework.boot
spring-boot-starter-data-redis
com.alibaba
fastjson
1.2.74
io.jsonwebtoken
jjwt
0.9.1
com.baomidou
mybatis-plus-boot-starter
3.4.1
mysql
mysql-connector-java
8.0.32
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
com.sun.xml.bind
jaxb-core
2.3.0
javax.activation
activation
1.1.1
Spring Data Redis为我们封装了Redis客户端的各种操作,简化使用。
SpringBoot RedisTemplate的序列化问题
Serializable
接口.① 添加序列化工具类,让Redis使用FastJson序列化,提高序列化效率, 将存储在Redis中的value值,序列化为JSON格式便于查看
/**
* Redis使用FastJson进行序列化
* @date 2023/4/10
**/
public class FastJsonJsonRedisSerializer implements RedisSerializer {
@SuppressWarnings("unused")
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonJsonRedisSerializer(Class clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
public void setObjectMapper(ObjectMapper objectMapper)
{
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
protected JavaType getJavaType(Class> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
② 添加Redis配置类
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate
/**
* spring redis 工具类
*/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public T getCacheObject(final String key)
{
ValueOperations operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public long setCacheList(final String key, final List dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public List getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public long setCacheSet(final String key, final Set dataSet)
{
Long count = redisTemplate.opsForSet().add(key, dataSet);
return count == null ? 0 : count;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public Set getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public void setCacheMap(final String key, final Map dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public Map getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public T getCacheMapValue(final String key, final String hKey)
{
HashOperations opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public List getMultiCacheMapValue(final String key, final Collection hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
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;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "mashibing";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @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("sg") // 签发者
.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();
}
public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的秘钥 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();
}
}
JWT工具类使用相关问题
秘钥长度不合理,将秘钥明文长度设置为 6位.
异常信息: Exception in thread "main" java.lang.IllegalArgumentException: Last unit does not have enough valid bits
//设置秘钥明文(长度为6位)
public static final String JWT_KEY = "msbhjy";
1.8 以上版本,需要引入 JAXB API
相关依赖
异常信息: java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
com.sun.xml.bind
jaxb-core
2.3.0
javax.activation
activation
1.1.1
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;
}
}
通过前面的分析,我们得出结论:可以自定义一个UserDetailsService,并让Spring Security使用它。我们的UserDetailsService可以从数据库中获取用户名和密码。
CREATE TABLE `sys_user` (
`user_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` VARCHAR(30) NOT NULL COMMENT '用户昵称',
`password` VARCHAR(100) DEFAULT '' COMMENT '密码',
`phonenumber` VARCHAR(11) DEFAULT '' COMMENT '手机号码',
`sex` CHAR(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`status` CHAR(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户信息表'
com.baomidou
mybatis-plus-boot-starter
3.4.1
mysql
mysql-connector-java
8.0.32
spring:
datasource:
url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class SysUser implements Serializable {
/**
* 主键
*/
@TableId
private Long userId;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 账号状态(0正常 1停用)
*/
private String status;
}
public interface UserMapper extends BaseMapper {
}
@SpringBootApplication
@MapperScan("com.mashibing.springsecurity_example.mapper")
public class SpringsecurityExampleApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringsecurityExampleApplication.class, args);
System.out.println("123456");
}
}
@SpringBootTest
public class MapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testUserMapper(){
List users = userMapper.selectList(null);
System.out.println(users);
}
}
第一步: 编写一个类,实现UserDetailsService接口,并重写其中的loadUserByUsername方法。在该方法中,使用用户名从数据库中检索用户信息。
/**
* 根据用户名检索用户信息
* @date 2023/4/14
**/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUserName,username);
SysUser user = userMapper.selectOne(wrapper);
//如果查询不到数据,抛出异常 给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//方法的返回值是 UserDetails接口类型,需要返回自定义的实现类
return new LoginUser(user);
}
}
第二步 为了将用户信息转换为UserDetails类型的对象,需要创建一个类来实现UserDetails接口,并将用户信息封装在其中。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
/**
* 用于获取用户被授予的权限,可以用于实现访问控制。
*/
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 用于获取用户的密码,一般用于进行密码验证。
*/
@Override
public String getPassword() {
return sysUser.getPassword();
}
/**
* 用于获取用户的用户名,一般用于进行身份验证。
*/
@Override
public String getUsername() {
return sysUser.getUserName();
}
/**
* 用于判断用户的账户是否未过期,可以用于实现账户有效期控制。
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 用于判断用户的账户是否未锁定,可以用于实现账户锁定功能。
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 用于判断用户的凭证(如密码)是否未过期,可以用于实现密码有效期控制。
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 用于判断用户是否已激活,可以用于实现账户激活功能。
*/
@Override
public boolean isEnabled() {
return true;
}
}