关于Spring Security框架详解!!!

本章介绍Spring Security框架

  • 一,Spring Security基本概要
  • 二,关于BCrypt算法
  • 三,关于登录的账号
  • 四,关于JWT
    • 1,关于Token
    • 2,关于JWT

一,Spring Security基本概要

Spring Security主要解决了认证与授权的相关问题。
其需要添加的依赖是:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

添加了依赖的默认配置效果有:
1,所有请求都是必须通过认证的
如果未认证,同步请求将自动跳转到 /login,是框架自带的登录页,非跨域的异步请求将响应 403 错误
2,提供了默认的登录信息,用户名为 user,密码是启动项目是随机生成的,在启动日志中可以看到
(1)当登录成功后,会自动重定向到此前访问的URL
(2)当登录成功后,可以执行所有同步请求,所有异步的POST请求都暂时不可用
(3)可以通过 /logout 退出登录

二,关于BCrypt算法

BCrypt算法被设计为是一种慢速运算的算法,可以一定程度上避免或缓解密码被暴力破解(使用循环进行穷举的破解)。

当添加了Spring Security相关的依赖项后,此依赖项中将包含BCryptPasswordEncoder工具类,是一个使用BCrypt算法的密码编码器,它实现了PasswordEncoder接口,并重写了接口中的String encode(String rawPassword)方法,用于对密码原文进行编码(加密),及重写了boolean matches(String rawPassword, String encodedPassword)方法,用于验证密码原文与密文是否对应。

BCrypt算法会自动使用随机的盐值进行加密处理,所以,当反复对同一个原文进行加密处理,每次得到的密文都是不同的,但这并不影响验证密码!

三,关于登录的账号

默认情况下,Spring Security使用user作为用户名,使用随机的UUID作为密码来登录!
如果需要自行指定登录账号,需要自定义一个组件类,实现UserDetailService接口,此接口中定义了User Detail loadUserByUsername(String username),在处理认证时,当用户(使用者)输入了用户名,密码并提交,String Security就会自动使用用户在表单中输入的用户名来调用老大UserByUsername()方法,作为开发者,应该重写此方法,并根据用户名来返回匹配的UserDetails对象,此对象应该包含用户的相关信息,例如密码等,当Spring Security得到调用loadUserByUsername()返回的UserDetails对象后,会自动处理后续的认证过程,例如验证密码是否匹配等。

例如 ,在根包下创建security.UserDetailsServiceImpl类:

package cn.tedu.csmall.passport.security;

import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
        // 暂时使用模拟数据来处理登录认证,假设正确的用户名和密码分别是root和123456
        if ("root".equals(s)) {
            UserDetails userDetails = User.builder()
                    .username("root")
                    .password("$2a$10$nO7GEum8P27F8S0EGEHryel7m89opm/AMdaqMBk.qdsdIpE/SWFwe")
                    .accountExpired(false)
                    .accountLocked(false)
                    .disabled(false)
                    .authorities("权限标识") // 权限,注意,此方法的参数不可以为null,在不处理权限之前,可以写一个随意的字符串值
                    .build();
            log.debug("即将向Spring Security返回UserDetails对象:{}", userDetails);
            return userDetails;
        }
        log.debug("此用户名【{}】不存在,即将向Spring Security返回为null的UserDetails值", s);
        return null;
    }

}

注意!!!!
上述的密码是密文的,这只是一个例子,后面可以根据需要把用户名和密码改为数据库的数据,还有权限,此处只是写的随意一个字符串,后续有专门的权限处理!

再有,上述的UserDetailsServiceImpl中返回的UserDetails接口类型的对象是User类型的,此类型没有id属性,如果需要向JWT(后面会将)中封装id甚至其它属性,必须自定义类,继承自User或实现UserDetails接口,在自定义类中补充声明所需的属性,并在UserDetailsServiceImpl中返回自定义类的对象。

例如创建AdminDetails继承User

@Setter
@Getter
@EqualsAndHashCode
@ToString(callSuper = true)
public class AdminDetails extends User {

    private Long id;

    public AdminDetails(String username, String password, boolean enabled,
                        Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled,
                true, true, true,
                authorities);
    }

}

UserDetailsServiceImpl中,调整为返回AdminDetails类型的对象:

public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
    AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
    log.debug("从数据库查询与用户名【{}】匹配的管理员信息:{}", s, loginInfo);

    if (loginInfo == null) {
        log.debug("此用户名【{}】不存在,即将抛出异常");
        String message = "登录失败,用户名不存在!";
        throw new BadCredentialsException(message);
    }
    
    // ===== 以下是调整的内容 =====
    List<GrantedAuthority> authorities = new ArrayList<>();
    GrantedAuthority authority = new SimpleGrantedAuthority("权限标识");
    authorities.add(authority);

    AdminDetails adminDetails = new AdminDetails(
            loginInfo.getUsername(), loginInfo.getPassword(),
            loginInfo.getEnable() == 1, authorities);
    adminDetails.setId(loginInfo.getId());

    log.debug("即将向Spring Security返回UserDetails接口类型的对象:{}", adminDetails);
    return adminDetails;
}

此处已经把id封装进去了,但是还没对权限进行处理,需要自行去数据库查询,多表联查找出用户的权限信息(此处就不详写了,就是数据库的基本查询),下面把查到的权限封装进AdminDetails,最终结果就能通过 adminDetails返回出来,用户信息都在这里面,下面的permission就是查找出来的权限。

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);
    AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
    log.debug("从数据库查询与用户名【{}】匹配的管理员信息:{}", s, loginInfo);

    if (loginInfo == null) {
        log.debug("此用户名【{}】不存在,即将抛出异常");
        String message = "登录失败,用户名不存在!";
        throw new BadCredentialsException(message);
    }

    // ===== 以下是此次调整的内容 =====
    List<GrantedAuthority> authorities = new ArrayList<>();
    for (String permission : loginInfo.getPermissions()) {
        GrantedAuthority authority = new SimpleGrantedAuthority(permission);
        authorities.add(authority);
    }

    AdminDetails adminDetails = new AdminDetails(
            loginInfo.getUsername(), loginInfo.getPassword(),
            loginInfo.getEnable() == 1, authorities);
    adminDetails.setId(loginInfo.getId());
}

四,关于JWT

1,关于Token

Token机制是目前主流的取代Session用于服务器端识别客户端身份的机制。

Token就类似于现实生活中的“火车票”,当客户端向服务器端提交登录请求时,就类似于“买票”的过程,当登录成功后,服务器端会生成对应的Token并响应到客户端,则客户端就拿到了所需的“火车票”,在后续的访问中,客户端携带“火车票”即可,并且,服务器端有“验票”机制,能够根据客户端携带的“火车票”识别出客户端的身份。

2,关于JWT

JWT的全拼是:JSON Web Token,是使用JSON格式来组织多个属性于值,主要用于Web访问的Token。

JWT的本质就是只一个字符串,是通过算法进行编码后得到的结果。

在项目中,如果需要生成、解析JWT,需要添加以下依赖项:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

JWT主要由三部分组成:Header,Payload,Signature
具体想要详细了解的话,可以参考以下地址:
JWT生成及解析网址
其中Header里面都是固定内容,
Payload里面放的就是主要内容,类似于id,用户名,邮箱,JWT过期时间这些用户信息的都可以放在里面,不同于Session的短时间过期,JWT是可以自定义过期时间的。
Signature里面保存的主要是随机的“盐”(就是一段自己写的随机的字母加数字加符号),这也是保证JWT唯一的凭证。
例如生成及解析如下:

package cn.tedu.csmall.passport;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTests {

    String secretKey = "kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn";

//生成JWT
    @Test
    public void testGenerate() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 9527);
        claims.put("username", "liucangsong");
        claims.put("email", "[email protected]");

        Date expirationDate = new Date(System.currentTimeMillis() + 10 * 60 * 1000);
        System.out.println("过期时间:" + expirationDate);

        String jwt = Jwts.builder()
                // Header
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "JWT")
                // Payload
                .setClaims(claims)
                .setExpiration(expirationDate)
                // Signature
                .signWith(SignatureAlgorithm.HS256, secretKey)
                // 整合
                .compact();
        System.out.println(jwt);

       
    }

//解析JWT
    @Test
    public void testParse() {
        String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OTUyNywiZXhwIjoxNjY1NTY4Mjc1LCJlbWFpbCI6ImxpdWNhbmdzb25nQDE2My5jb20iLCJ1c2VybmFtZSI6ImxpdWNhbmdzb25nIn0.ESPNerLR2uBt1UtUhPwEU_71fcX_Ve-Td6X4Pjvegak";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Integer id = claims.get("id", Integer.class);
        System.out.println("id=" + id);
        String username = claims.get("username", String.class);
        System.out.println("username=" + username);
        String email = claims.get("email", String.class);
        System.out.println("email=" + email);
        String phone = claims.get("phone", String.class);
        System.out.println("phone=" + phone);
    }

}

注意!!!如果生成的JWT超过了过期时间的话,再解析是会出现ExpiredJwtException错误的:

io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-10-12T17:14:41Z. Current time: 2022-10-12T17:39:57Z, a difference of 1516448 milliseconds.  Allowed clock skew: 0 milliseconds.

另外JWT是不安全的,因为在不知道secretKey的情况下,任何JWT都是可以解析出Header、Payload部分的,这2部分的数据并没有做任何加密处理,所以,如果JWT数据被暴露,则任何人都可以从中解析出Header、Payload中的数据!

至于JWT中的secretKey,及生成JWT时使用的算法,是用于对Header、Payload执行签名算法的,JWT中的Signature是用于验证JWT真伪的。

当然,如果你认为有必要的话,可以自行另外使用加密算法,将Payload中应该封装的数据先加密,再用于生成JWT!

另外,如果JWT数据被泄露,他人使用有效的JWT是可以正常使用的!所以,通常,在相对比较封闭的操作系统(例如智能手机的操作系统)中,JWT的有效时间可以设置得很长,但是,不太封闭的操作系统(例如PC端的操作系统)中,JWT的有效时间应该相对较短。

所以,在JWT时,需要注意:

1,根据你所需的安全性,来设置JWT的有效时间
2,不要在JWT中存放敏感数据,例如:手机号码、身份证号码、明文密码
3,如果一定要在JWT中存放敏感数据,应该自行使用加密算法处理过后再用于生成JWT

你可能感兴趣的:(spring,java,spring,boot)