- 引入依赖:使用的依赖版本如下
父依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.12</version>
</parent>
依赖版本随父依赖指定,重点是spring-boot-starter-security
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
</dependencies>
其实这个时候就可以写一个接口来测试了
先在配置文件application.yml
中加上这一段,在spring.security
中指定一个账号密码,当然实际情况这样做不太好,测完了把它删掉
spring:
application:
name: salt-security
security:
user:
name: admin
password: 123456
然后写个测试接口用的
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping()
public String security(){
return "hello spring security";
}
}
在浏览器中调用这个接口,会跳转到一个登录页面,输入刚才指定的账号密码后,成功调通这个接口。
测试完成后我们再进一步。
- 实现UserDetailsService,重写loadUserByUsername
这个是为了自定义账户的获取,用户将用户名和密码传过来认证,我们这边要使用用户传过来的用户名去数据源(数据库 等)中找到对应的账户信息,才能和用户传递过来的信息做对比
这里就意思意思直接指定密码了
package com.jenson.oauth.security.custom;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里应该是从数据源中根据用户名查找用户信息,如果用户信息查询不到则抛出用户不存在的异常
String password = "123456";
UserDetails userDetails = new User(username, password, Collections.emptyList());
return userDetails;
}
}
光实现这个查询出用户还不行,会报错
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
需要实现一下PasswordEncoder
matches
方法就是用来判断密码是否匹配的,匹配就会返回true
package com.jenson.oauth.security.custom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class CustomBCryptPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return null;
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
}
这个时候再测试一下刚才的 /test
,还是一样的,可以认证成功。
但是现在密码是明文的,要是数据库泄露就完蛋了,所以改一下CustomBCryptPasswordEncoder
,对密码简单的加个密
package com.jenson.oauth.security.custom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class CustomBCryptPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
// 简单加密,生成一个salt
String salt = BCrypt.gensalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword != null && encodedPassword != null && encodedPassword.length() != 0) {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
} else {
log.warn("Empty encoded password");
return false;
}
}
}
可以写个单元测试测试下这个类
package com.jenson.oauth.security.custom;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class CustomBCryptPasswordEncoderTest {
@Autowired
private CustomBCryptPasswordEncoder customBCryptPasswordEncoder;
@Test
void encode() {
String password = customBCryptPasswordEncoder.encode("123456");
System.out.println("password = " + password);
boolean success1 = customBCryptPasswordEncoder.matches("123456", password);
boolean success2 = customBCryptPasswordEncoder.matches("123456", "$2a$10$Z1OKl3clWu5FD2WMGNa.KOAiMn4QTk4CFfozKAC86s4Fw6aPmWbri");
boolean success3 = customBCryptPasswordEncoder.matches("1234567", password);
System.out.println("success1 = " + success1 + "\n"
+ "success2 = " + success2 + "\n"
+ "success3 = " + success3);
}
}
运行结果如下
password = $2a$10$o.DUpIgkmWS12DCee8.5z.YwFqA65pp/CzrI4Xj6eR/l2Rj5I8s9W
success1 = true
success2 = true
success3 = false
可以看出,BCrypt.checkpw
可以用一段未加密的字符串和已加密的字符串做比较,如果与已加密字符串的原字符串能匹配上,就会返回true。
这样的话数据库里就不用保存明文的密码了,安全性大大提高
再修改下loadUserByUsername
,密码换成加密后的"123456",再测试一下/test
接口,发现没有问题,依然能正常登录
可以发现同样是"123456"这个字符串,BCrypt.hashpw("123456", salt)
加密后的字符串并不相同,但是依然能和原字符串匹配成功
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里应该是从数据源中根据用户名查找用户信息,如果用户信息查询不到则抛出用户不存在的异常
String password = "$2a$10$wETX0LQUnOJ8iJG9M.m4w.ofrD2RVkZ7udPqRXgonHILTKYgizg0e";
UserDetails userDetails = new User(username, password, Collections.emptyList());
return userDetails;
}
但是这样调用接口需要重定向到登录页面就很麻烦,特别是前后端分离的场景,如果是通过一个接口获取token,再使用此token去调用其他接口就好了
网友评论