spring-security的使用

1,189 阅读4分钟

1. 背景

在做通用权限系统的时候,我使用了spring-security来控制权限系统,现在就最基本的使用做个总结

2. 使用demo

2.1 一些基本的概念

Spring Security的安全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)

Spring Security登录认证主要涉及两个重要的接口 UserDetailService和UserDetails接口。 UserDetailService接口主要定义了一个方法 loadUserByUsername(String username)用于完成用户信息的查询,其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接 口,完成数据库查询,该接口返回UserDetail。

loadUserByUsername 用户返回的是UserDetails,我们自己的User实现UserDetails

UserDetail主要用于封装认证成功时的用户信息,即UserDetailService返回的用户信息,可以用Spring 自己的User对象,但是最好是实现UserDetail接口,自定义用户对象

认证成功返回token是什么

Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户 名和密码

基本的依赖

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

2.2 Spring Security认证步骤

  1. 自定UserDetails的实现类User:当实体对象字段不满足时需要自定义UserDetails,一般都要自定义 UserDetails

  2. 自定义UserDetailsService类,主要用于从数据库查询用户信息。

    将User类实现UserDetails接口

    将原有的isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired和isEnabled属性修改成boolean类型,同时添加authorities属性、permisssion属性

  3. 创建登录认证成功处理器,认证成功后需要返回JSON数据,菜单权限等。

    LoginSuccessHandler implements AuthenticationSuccessHandler

    实现onAuthenticationSuccess方法

    生成token、创建登录结果对象、获取输出流、把生成的token存到redis

  4. 创建登录认证失败处理器,认证失败需要返回JSON数据,给前端判断。

    LoginFailureHandler implements AuthenticationFailureHandler,重写onAuthenticationFailure()方法

    String result =
            JSON.toJSONString(Result.error().code(code).message(message));
    outputStream.write(result.getBytes(StandardCharsets.UTF_8));
    outputStream.flush();
    outputStream.close();
    
  5. 创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示JSON。

    CustomerAccessDeniedHandler implements AccessDeniedHandler

  6. 创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示JSON。

    AnonymousAuthenticationHandler implements AuthenticationEntryPoint

  7. 配置Spring Security配置类,把上面自定义的处理器交给Spring Security。

3. JWT 的原理和使用

典型的时间换空间的设计模式

3.1 JWT的优势

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快

  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库

  • 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。

  • 不需要在服务端保存会话信息,特别适用于分布式微服务。

3.2.令牌组成

  • 1.标头(Header)

    标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分

  • 2.有效载荷(Payload)

  • 3.签名(Signature)

    前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过

3.3 JWT 加密解密的全过程

@Test
void contextLoads() {
    Calendar instance = Calendar.getInstance();
    instance.add(Calendar.SECOND,100);
    String token = JWT.create()
            .withClaim("userid",12)
            .withClaim("username", "xiaochen")//payload
            .withExpiresAt(instance.getTime())//指定令牌过期时间
            .sign(Algorithm.HMAC256("!Q@W#E$R"));//签名
    System.out.println(token);
}

@Test
public void test(){
    //创建验证对象
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!Q@W#E$R")).build();

    DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTYxMjg1MjMsInVzZXJpZCI6MTIsInVzZXJuYW1lIjoieGlhb2NoZW4ifQ.WhsEsCcf6c4hKr1eyhy6psQsaVIPr3Ibfqo3vM8sHKA\n");

    System.out.println(verify.getClaim("userid").asInt());
    System.out.println(verify.getClaim("username").asString());
    System.out.println("过期时间: "+verify.getExpiresAt());
}

3.4 JWT 常见异常信息

  • SignatureVerificationException:签名不一致异常

  • TokenExpiredException:令牌过期异常

  • AlgorithmMismatchException:算法不匹配异常

  • InvalidClaimException: 失效的payload异常

3.5 JWT 封装工具类

public class JWTUtils {

    private static final String SIGN = "!Q@W3e4r%T^Y";

    /**
     * 生成token  header.payload.sign
     */
    public static String getToken(Map<String,String> map){

        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,7);
        JWTCreator.Builder builder = JWT.create();
        map.forEach(builder::withClaim);
        return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SIGN));
    }

    /**
     * 验证token 合法性, 不抛异常就是验证通过
     * 获取token中payload
     *
     */
    public static DecodedJWT verify(String token){
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    }

}