spring-boot-security 一个简单的demo

2022-07-31  本文已影响0人  东南枝下
  1. 引入依赖:使用的依赖版本如下

父依赖

<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";
    }

}

在浏览器中调用这个接口,会跳转到一个登录页面,输入刚才指定的账号密码后,成功调通这个接口。

测试完成后我们再进一步。

  1. 实现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去调用其他接口就好了

上一篇下一篇

猜你喜欢

热点阅读