9_springboot_shiro_jwt_多端认证鉴权_整合jwt

news2024/11/19 8:33:34

1. Shiro框架回顾

到目前为之,Shiro框架本身的知识点已经介绍完了。web环境下,整个框架从使用的角度我们需要关注的几个点:

  1. 要使用Shiro框架,就要创建核心部件securityManager 对象。 SpringBoot项目中,引入shiro-spring-boot-web-starter 组件后,其自动配置会自动创建securityManager 对象,默认创建的是org.apache.shiro.web.mgt.DefaultWebSecurityManager , 一般项目web项目使用它已经够用,如果有特殊要求我们自己可以定制

  2. securityManager 对象要能够正常工作,我们需要为它准备:

    • subjectDAO 一般情况下,我们无需配置这个对象,默认使用的是DefaultSubjectDAO, 看名字好像是Subject对象的存储,其实Subject对象每次访问都会创建一个新的subject对象,并会绑定到当前线程中。其实它所做的事情并不是存储Subject,而是将当前Subject中的身份信息:principalauthenticated 是否校验成功放入到session 中去而已。

      subjectDAO 工作的时候,需要为它提供SessionStorageEvaluator 默认提供的是 DefaultWebSessionStorageEvaluator, 它决定了subjectDAO 是否要将subject中的信息写入到session中。

      如果确实不需要写入session,比如某些场景下每次都要验证,就没有必要写入到session中了,此时可以自定义类,继承DefaultWebSessionStorageEvaluator 类,重写 isSessionStorageEnabled 方法,在这个方法中根据情况返回是否开启session存储操作。

      本章代码中自定义了一个 SessionStorageEvaluator 让api请求认证完毕后,不存入session。

    • subjectFactory : 创建subject对象的工厂类,默认是DefaultWebSubjectFactory, 创建的是 WebDelegatingSubject, 如果项目中确实需要,也可以定制

    • authenticator 认证器,默认为:ModularRealmAuthenticator, ModularRealmAuthenticator 默认的认证策略是AtLeastOneSuccessfulStrategy. 无特殊需求,一般也无需定制,使用默认即可

    • authorizer 授权器,默认为: ModularRealmAuthorizer

    • realm 绝大多数情况下,我们需要定义自己的realm ,可以定义多个

    • sessionManager Session管理器,前面代码中我们进行了定制

    • cacheManager 缓存管理器,前面代码中我们使用了redis作为缓存

  3. realm 配置realm的时候关注是否开启缓存,缓存的key是什么

  4. sessionManager 为他配置sessionDAO. 前面我们自定义了sessionDAO,将session数据保存到了redis中。

  5. 过滤器相关

    shiroFilterFactoryBean 实际类型为 ShiroFilterFactoryBean, 它的作用就是创建了一个Filter,并将这个Filter注册到Servlet容器中。同时它也管理着系统默认的Filter和自定义的Filter。 它需要:

    1. securityManager
    2. filterChainDefinitionMap : 即URL与过滤器名称之间的映射
    3. filters 即自定义过滤器,名称和过滤器实例对象之间的映射

2. 目标

本节先了解一些jwt 的基本知识,然后讨论一下 将JWT整合到shiro中的必要性,最后看如何通过代码来将两者整合到一起。

3. JWT介绍

JSON web token (JWT)是认证的一种方式,相比于基于Session认证,在系统中并不存储任何关于用户信息。

3.1 JWT格式

一个JWT实际上就是一个字符串,它由三部分组成,头部载荷签名,每个部分用 . 进行分割

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ.2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54
  • 头部 Header

    第一部分是头部(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9)。头部信息是生成签名的算法,这部分非常标准,对于任何使用相同算法的JWT都是一样的。

  • 载荷 Payload

    第二部分是有效载荷(payload)(eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ),它包含特定应用程序信息(在这个示例中是用户名),以及关于token有效期和有效性的信息。

  • 签名 Signature

    第三部分是签名(2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54)。它是通过将前两部分与一个密钥合并和散列而生成的。

是头部和有效载荷是不加密的.仅使用base64进行编码,意味着任何人都能解码看到内容,

Linux中使用命令可以查看:

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d
{"alg":"HS256","typ":"JWT"}

在这里插入图片描述

3.2 jwt中三个部分相关字段

  • Header(头部):描述JWT的基本元数据,包括使用的加密算法(用于签名或加密)以及JWT的类型,相关字段:

    • alg(algorithm)

      指定签名或加密算法,如 HS256(HMAC with SHA-256)、RS256(RSA with SHA-256)、ES256(ECDSA with SHA-256)等。这个字段告知接收方如何对JWT的Signature部分进行验证.

    • typ (type)

      声明数据类型,对于JWT而言,通常固定为 JWT,表明这是一个JSON Web Token。

    示例:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  • Payload(载荷):承载实际要传输的数据,即声明(claims)。这些声明是关于JWT所代表实体(如用户、设备、服务等)的一系列键值对,包含身份验证、授权和其他相关信息。相关字段包含

    • 标准(Standard) 或 公开(Public) claim:
      • iss(issuer):颁发者,标识JWT的签发主体。
      • sub(subject):主题,标识JWT所代表的主体,即其所有者。
      • aud(audience):受众,标识JWT预期的接受者。
      • exp(expiration time):过期时间,定义JWT的有效期截止时间。
      • iat(issued at):发行时间,记录JWT的创建时间。
      • jti(JWT ID):JWT唯一标识符,有助于防止重放攻击。
    • 私有(Private)claim:自定义的键值对,用于承载特定业务相关的数据,如用户ID、角色、权限、个人信息等。

    示例:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022,
      "custom_claim": "example_value"
    }
    
  • Signature(签名): 用于验证JWT完整性和防篡改,通过对Header和Payload进行签名来保证其内容未被更改。Signature的计算过程基于Header中指定的加密算法.Signature本身不是一个JSON对象,而是通过加密算法生成的一个字节序列,经过Base64URL编码后作为JWT的第三个部分。

    示例:

    HmacSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
    

3.3 认证步骤

  1. 登录成功生成Token,发送给客户端。 生成的时候根据实际场景需要自定义 Payload内容
  2. 客户端放入到请求头Authorization-Token 中。 客户端可以解析Payload内容(将base64格式的 payload还原成字符串即可)
  3. 在需要身份的URL上携带请求头Authorization-Token 发送到服务端
  4. 服务端请求到来时,先被中间件进行拦截,获取到Authorization-Token 请求头中的token信息后,解析这个token,解析其实就是取出 Header部分和Payload部分,然后用秘钥进行SHA256进行签名,将签名的结果和Token中的Signature进行比较,一致则签名通过,放行到Controller进行处理,不一致则认证失败,直接给出认证结果响应到客户端。

Token失效有两种方案:

  1. 设置Token过期时间,如果过期,则认证不能通过。
  2. 为每个用户设置一个秘钥,并与用户一起保存到数据库或缓存中。如果需要让用户重新登录,比如退出登录或者修改密码或者重置密码后,直接更改数据库中的秘钥,即重新生成一个秘钥。这样 在验证token的时候,由于秘钥发生变化,那么验签的结果就会与原来不同,从而判定token失效

4. Shiro使用JWT的必要性

  1. 无状态服务需求

    每个服务实例应能独立处理请求,不依赖于共享的服务器端会话状态。JWT提供了这种能力,它将用户的认证信息以加密的JSON对象形式存储在客户端(通常是浏览器的Cookie或HTTP头部),每次请求时携带此Token。Shiro与JWT整合后,可以基于Token直接进行身份验证,无需在服务器端维护会话,从而适应分布式、水平扩展的场景。

  2. 跨域支持

    如果应用涉及多个子域、前端与后端分离,或者需要与其他第三方系统进行安全交互,JWT的跨域特性非常有用。由于Token包含所有必要的认证信息,可以在不同域名或接口之间自由传递,客户端只需在请求时添加Token即可,不受同源策略限制。整合Shiro与JWT,可以使系统轻松支持跨域认证与授权。

  3. 移动端友好

    在移动应用(如原生App或React Native、Flutter等跨平台应用)中,维持会话状态可能较为复杂。JWT因其无状态特性,特别适合移动端应用,客户端只需妥善保管Token并在每次请求时发送,避免了复杂的会话同步问题。与Shiro整合,可为移动应用提供一致且高效的认证授权机制。

  4. 长会话与单点登录(SSO)支持

    JWT支持设置较长的有效期,允许用户在一段时间内无需重新登录即可访问受保护资源。此外,通过在Token中嵌入用户标识和相关权限信息,可以实现单点登录(SSO)功能,用户在一个系统登录后,携带同一Token访问其他关联系统时无需再次认证。整合Shiro与JWT,可以方便地构建SSO解决方案,提升用户体验。

由于JWT将认证信息存储在客户端,服务器端验证Token时只需进行签名验证和过期检查,无需查询数据库或缓存(除非需要实时更新权限)。这减少了对后端服务的压力,特别是当用户基数大、并发请求高时,有利于提高系统的整体性能和可伸缩性。

在下面的案例中,会为每个用户都分配一个秘钥,这个秘钥是存放在数据库中的,服务端验证Token的时候,会先取得Realm中的认证信息(其中包含了秘钥),而这个认证信息是缓存在Redis中的。当更改了用户的秘钥后,要清除这个缓存,从数据库中获取,此时已经生成的JWT Token也就失效了

5. Shiro整合JWT步骤

在这个案例中,允许了一些情况下需要用到session,一些情况下需要禁用session。使用JWT不需要用到session,因为用户的信息都已经存放到JWT中了,根据情况来创建session,前面分析过,Shiro中的 subjectDAO 中有个 SessionStorageEvaluator ,它来决定是否要写入到session中。所以本节会自定义SessionStorageEvaluator 加入逻辑来适配各种情况。

5.1 自定义AuthenticationToken

使用自定义的AuthenticationToken 来封装JWT。 后面的过滤器中会创建这个JWT token。 两种场景下会创建Token:

  • 用户登录认证,需要创建JWT token
  • 认证登录成功后,会为客户端签发 JWT token,客户端请求后需要验证这个 JWT Token 签名是否正确,是否过期。

每种情况下,token中的数据是不一样的,但是有一个数据必须是存在的,那就是用户身份的唯一标识。因为登录认证需要知道谁谁在登录,而 JWT 的签名验证需要用到秘钥,而这里设计的是每个用户都有自己的秘钥,必须要有用户的唯一身份标识才能取到秘钥。所以在过滤器中创建 JWT token的时候,必须要能获取到用户唯一身份标识,例子中用的是用户名,实际场景中可以换成别的。

public class JwtAuthenticationToken implements AuthenticationToken {
    private String  jwt;
    // 是否是登录请求,realm中要使用它来判断是登录还是验证
    private boolean isLoginRequest;
    private String  userName;
    private String  password;

    // 如果是登录,使用这个构造方法
    public JwtAuthenticationToken(String username, String password) {
        this.userName = username;
        this.password = password;
        isLoginRequest = true;
    }

    // 如果是jwt验证,使用这个构造方法
    public JwtAuthenticationToken(String jwt) {
        this.jwt = jwt;
        isLoginRequest = false;
        //从jwt中解析出用户名
        this.userName = JwtUtil.getClaimFiled(jwt, "account");
    }

    public boolean isLoginRequest() {
        return this.isLoginRequest;
    }

    /**
     * 身份信息
     *
     * @return
     */
    @Override
    public Object getPrincipal() {
        return userName;
    }

    public String getJwt() {
        //获取jwt
        return jwt;
    }

    /**
     * 只用在登录中获取提交的凭证
     *
     * @return
     */
    @Override
    public Object getCredentials() {
        return password;
    }
}

5.2 过滤器

在过滤器要完成的功能:

  1. 预处理

    通过这个过滤器的请求支持跨域请求。在JWT的场景下,不需要创建session,而其它场景下又需要创建sesssion。所以是否创建session是根据场景条件来决定。所以向reqeust attribute中放一个是否需要创建session的标识,后面我们自定义的SessionStorageEvaluator 会读取这个标识,根据条件来决定是否创建session

  2. 跨域处理

    因为添加了自定义的非标准的请求头,浏览器会认为是非简单请求,此时浏览器就会发送一个OPTION

public class JwtAuthenticationFilter extends AuthenticatingFilter{
    ...
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        // 重要,不需要创建Session,就在这里指定 ConditionalSessionStorageEvaluator 中会用到
        httpServletRequest.setAttribute("SESSION_CREATION_ENABLED", false);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
    ...
}
  1. 创建AuthenticationToken

    此时需要是不是登录请求,如果是登录请求,创建的token中是没有jwt token信息的。前面说过用户的身份标识是一定要有的,如果缺失,后续代码是无法执行的。所以这里抛出了异常。在下面执行认证的时候,会抓取这个异常响应给客户端。

    @Slf4j
    public class JwtAuthenticationFilter extends AuthenticatingFilter {    
       @Override
        protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
            // 登录请求,就验证用户名密码
            if (isLoginRequest(request, response)) {
                // 用户名,参数名称为 username,根据实际情况修改
                String userName = WebUtils.getCleanParam(request, "username");
                // 密码,根据实际情况进行修改
                String password = WebUtils.getCleanParam(request, "password");
                return new JwtAuthenticationToken(userName, password);
            } else {
    
                // 非登录请求,就验证JWT, 请求头名称为 X-Access-Token 根据实际情况修改
                String jwtHeader = WebUtils.toHttp(request).getHeader("X-Access-Token");
                if (StringUtils.isBlank(jwtHeader)) {
                    throw new JwtVerificationException("JWT token 不存在");
                }
                return new JwtAuthenticationToken(jwtHeader);
            }
        }
    }
    
  2. 执行认证

    因为需要禁用掉session,所以当请求到来时,是无法关联上session的。也就是说只要是请求都需要认证一遍,可能是登录请求,也可能是提交了JWT token的请求。因为从 AuthenticatingFilter 继承,此时一定会调用 onAccessDenied ,所以只需要重写这个方法,让它去执行登录,进而执行 Subject.login ,这样就会调用Reaml进行认证。

    本来对于 JWT token的签名验证和过期判断是可以在Filter中完成的,如果是统一的秘钥,当然可以在这里完成,不必在Reaml中做验证。但是这里每个用户的秘钥都是不一样,而秘钥在realm中能获取到,所以就把所有的认证工作交给realm 来做。

    realm是带缓存的,不必每次都去查询用户的认证信息和授权信息。所以性能上几乎没什么损失。

    @Slf4j
    public class JwtAuthenticationFilter extends AuthenticatingFilter {
        ...
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            // 去执行 Subject.login(); 这样才会调用 realm,由realm中的匹配器来决定是执行登录验证还是 jwt验证。
            return executeLogin(request, response);
        }
        
        // 执行登录
         @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
            AuthenticationToken token = null;
            try {
                // 创建token
                token = createToken(request, response);
                // 执行登录,交给reaml去认证
                Subject subject = getSubject(request, response);
                subject.login(token);
                return onLoginSuccess(token, subject, request, response);
            } catch (JwtVerificationException e) {
                Map<String, ?> result = Map.of("code", 401, "msg", e.getMessage());
                responseJsonResult(result, response);
                return false;
            } catch (AuthenticationException e) {
                return onLoginFailure(token, e, request, response);
            }
        }
        ...
    }
    
    // 如果是登陆失败,让请求到达Controller输出错误信息。
    // 如果是JWT验证失败,直接输出错误消息
     protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                         ServletRequest request, ServletResponse response) {
            if (isLoginRequest(request, response)) {
                setFailureAttribute(request, e);
                return true; // 
            } else { // JWT验证失败
                Map<String, ?> result = Map.of("code", 401, "msg", e.getMessage());
                responseJsonResult(result, response);
                return false;
            }
        }
    
     // 将shbiro中的错误保存到 request attribute中,Controller中获取后输出错误消息
        protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
            String className = ae.getClass().getName();
            request.setAttribute(DEFAULT_ERROR_KEY_ATTRIBUTE_NAME, className);
        }
    
     // 向客户端输出信息。
        private void responseJsonResult(Map<String, ?> result, ServletResponse response) {
            if (response instanceof HttpServletResponse res) {
                res.setContentType("application/json;charset=UTF-8");
                res.setStatus(200);
                res.setCharacterEncoding("UTF-8");
                try {
                    // 输出JSON 数据
                    res.getWriter().write(JSON.toJSONString(result));
                    res.getWriter().flush();
                    res.getWriter().close();
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
        }
    
    

5.3 Realm

Realm 是用来获取用户认证信息和授权信息的,这里先定义一下用户对象。

5.3.1 准备数据

这里的用户对象中包含了 :账号,密码密文,秘钥,盐值,jwt唯一标识,角色和权限。

因为jwt token应用,不能让它一直有效。虽然有过期时间,但如果token已经颁发了,没到有效期,但此时如果存在泄露的风险,需要立即让它失效,所以它需要进行管理。所以这里设计了一个 jwtId,专门用于管理jwt token

@Data
@ToString
@Builder
public class JwtAccount implements Serializable {
    private String             account;//账号
    private String             pwdEncrypt;//密码密文
    private String             secretKey;// jwt 秘钥
    private String             salt;// 对密码加密的时候使用的salt值
    private String             jwtId;// jwt唯一标识
    private Collection<String> roles;// 角色
    private Collection<String> permissions;//权限

}

Realm中指定匹配的 AuthenticationToken 然后再初始化一些数据:(实际项目是放到数据库中的)

注意这里的秘钥必须是 32字符,也就是 256字节。因为 JWT生成签名的时候,用的是HmacSHA256 算法

@Slf4j
public class JwtAuthenticationRealm extends AuthorizingRealm {
     // 模拟数据库中的账号信息 key为账号
    private Map<String, JwtAccount>  jwtAccountMap = new HashedMap();
    // 一个账号可以拥有多种角色
    private Map<String, Set<String>> roles         = Map.of(
            "administrator", Set.of("admin"),//管理员
            "zhangsan", Set.of("normal") // 普通用户
    );
    // 角色权限
    private Map<String, Set<String>> permissions   = Map.of(
            "admin", Set.of("*", "*:*"), //所有权限
            "normal", Set.of("employee:write", "employee:read") //执行查看
    );
    public JwtAuthenticationRealm() {
        // 指定密码匹配器
        super(new JwtCredentialsMatcher());
        jwtAccountMap.put("administrator", JwtAccount.builder()
                .account("administrator")
                .pwdEncrypt("0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb")
                .secretKey("cbce2d1aad0867f8317e7ebeb3427333")
                .salt("55ae2b2c63ddd6d4763e0c57bda9078e")
                .build());
        jwtAccountMap.put("zhangsan", JwtAccount.builder()
                .account("zhangsan")
                .pwdEncrypt("3bff14c4279f01892165b96afed9b40ec7f14a9de55d9564c088bad3e04d6411")
                .secretKey("cbce2d1aad0867f8317e7ebeb3427555")
                .salt("cbce2d1aad0867f8317e7ebeb3427999")
                .build());
    }
    
    // 声明它只支持 JwtAuthenticationToken
    @Override
    public boolean supports(AuthenticationToken token) {
        return token != null && JwtAuthenticationToken.class.isAssignableFrom(token.getClass());
    }  
    
    ...
}

5.3.2 匹配器

上面构造方法中指定了一个 匹配器。在这个匹配器中需要完成两种情况的匹配。一种情况是登录认证,一种情况是JWT Token 验签。这里需要用到JWT相关的库。所以项目中需要引入:

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.4.0</version>
        </dependency>

再定义一个 jwt的工具类,用来完成jwt 的生成,验证

@Slf4j
public class JwtUtil {
    public static final String HEADER = "X-Access-Token";

    // 默认过期时间,单位分钟, 即一周过期
    private static final long EXPIRE = 60 * 24 * 7;

    /**
     * 生成jwt token
     */
    public static String generateToken(JwtAccount jwtAccount) {
        // 签名算法
        Algorithm ALGORITHM = Algorithm.HMAC256(jwtAccount.getSecretKey());
        //过期时间
        LocalDateTime tokenExpirationTime = LocalDateTime.now().plusMinutes(EXPIRE);
        return JWT.create()
                .withHeader(Map.of("typ", "JWT", "alg", "HS256"))
                .withSubject(jwtAccount.getAccount())  //账号为JWT所代表的主体,即其所有者
                .withExpiresAt(Timestamp.valueOf(tokenExpirationTime)) //过期时间
                .withIssuedAt(Timestamp.valueOf(LocalDateTime.now())) //发行时间
                .withJWTId(jwtAccount.getJwtId())
                .withClaim("account", jwtAccount.getAccount())
                .withArrayClaim("roles", jwtAccount.getRoles().toArray(new String[0]))
                .withArrayClaim("permissions", jwtAccount.getPermissions().toArray(new String[0]))
                .sign(ALGORITHM);
    }

    /**
     * @param token
     * @param secretKey
     * @return
     * @throws JWTVerificationException SignatureVerificationException – 签名失败.
     *                                  TokenExpiredException – 已过期.
     *                                  MissingClaimException – 确实Claim.
     *                                  IncorrectClaimException – claim不正确
     */
    public static DecodedJWT verifyJWTSignature(String token, String secretKey) throws JWTVerificationException {
        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        JWTVerifier verifier = JWT.require(algorithm)
                .build();
        return verifier.verify(token);
    }


    /**
     * 获得token中的自定义信息,一般是获取token的username,无需secret解密也能获得
     *
     * @param token
     * @param filed
     * @return
     */
    public static String getClaimFiled(String token, String filed) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(filed).asString();
        } catch (JWTDecodeException e) {
            log.error("JwtUtil getClaimFiled error: ", e);
            return null;
        }
    }

}

匹配器:

@Slf4j
public class JwtCredentialsMatcher extends CodecSupport implements CredentialsMatcher {
    // 这里要区分是登录还是 jwt验证
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        // 是 jwtAuthenticationToken 时候才验证
        if (token instanceof JwtAuthenticationToken jwtAuthenticationToken) {
            if (jwtAuthenticationToken.isLoginRequest()) {
                return matchLogin(jwtAuthenticationToken, info);
            } else {
                return matchJwt(jwtAuthenticationToken, info);
            }
        }
        return false;
    }

    // 登录匹配
    private boolean matchLogin(JwtAuthenticationToken token, AuthenticationInfo info) {

        // 取出真实身份信息
        Object primaryPrincipal = info.getPrincipals().getPrimaryPrincipal();
        // 如果身份信息是 SystemAccount 对象
        // 此时要注意,Realm 中要将 JwtAccount 对象放入到 AuthenticationInfo 中

        JwtAccount account    = (JwtAccount) primaryPrincipal;
        String     accountPwd = account.getPwdEncrypt();
        // 获取盐值
        String accountSalt = account.getSalt();
        // JwtAuthenticationToken 中凭证就是密码
        String tokenPwd = token.getCredentials().toString();
        //进行散列
        String tokenPwdSha = new Sha256Hash(tokenPwd, accountSalt, 2).toHex();
        return accountPwd.equals(tokenPwdSha);

    }

    // 验证JWT
    private boolean matchJwt(JwtAuthenticationToken token, AuthenticationInfo info) {

        // 取出真实身份信息
        Object primaryPrincipal = info.getPrincipals().getPrimaryPrincipal();
        // 此时要注意,Realm 中要将 SystemAccount 对象放入到 AuthenticationInfo 中
        JwtAccount account = (JwtAccount) primaryPrincipal;
        try {
            JwtUtil.verifyJWTSignature(token.getJwt(), account.getSecretKey());
            return true;
        } catch (Exception e) {
            throw new JwtVerificationException("验证签名失败,或Token已过期");
        }
    }
}

5.3.3 Realm认证

doGetAuthenticationInfo 中只需要从数据库中取得用户正确的认证信息,它的任务就完成了,并且会进行缓存。至于是否成功,这是由 匹配器来决定的。

这里我们希望的是用户登录完成之后,将用户的权限和角色信息写入到 JWT 后返回给客户端。由于Subject 中不能直接获取到授权信息,所以这里重写了 assertCredentialsMatch 方法,这个方法其实就是在调用匹配器完成验证工作,这里添加了一段逻辑: 如果是登录验证,而且登录成功了,就取出授权信息(调用getAuthorizationInfo 会将授权信息缓存),放到了 jwtAccount 中了,这样从subject 中获取的主体就是 jwtAccount ,此时它包含了授权信息。

@Slf4j
public class JwtAuthenticationRealm extends AuthorizingRealm {    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 1.从传过来的认证Token信息 是 JwtAuthenticationToken, 它里面定义了 username作为账号
        String account = token.getPrincipal().toString();
        // 2.通过用户名到数据库中获取整个用户对象
        JwtAccount jwtAccount = jwtAccountMap.get(account);
        if (StringUtils.isBlank(account) || jwtAccount == null) {
            throw new UnknownAccountException();
        }
        // 3. 创建认证信息,即用户正确的用户名和密码。
        // 四个参数:
        // 第一个参数为主体,第二个参数为凭证,第三个参数为Realm的名称
        // 因为上面将凭证信息和主体身份信息都保存在 SystemAccount中了,所以这里直接将 SystemAccount对象作为主体信息即可

        // 第二个参数表示凭证,匹配器中会从 SystemAccount中获取盐值,密码登凭证信息,所以这里直接传null。

        // 第三个参数,表示盐值,这里使用了自定义的SaltSimpleByteSource,之所以在这里new了一个自定义的SaltSimpleByteSource,
        // 是因为开启redis缓存的情况下,序列化会报错

        // 第四个参数表示 Realm的名称
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                jwtAccount,
                null,
                new SaltSimpleByteSource(jwtAccount.getSalt()),
                getName()
        );
        return authenticationInfo;
    }
    
    
    // 调用匹配器完成匹配工作
    @Override
    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        // 登录成功,补充 用户角色和权限信息
        JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) token;
        // 一旦没有匹配上会抛出异常
        CredentialsMatcher cm = getCredentialsMatcher();

        if (!cm.doCredentialsMatch(token, info)) {
            //not successful - throw an exception to indicate this:
            String msg = "认证错误";
            throw new IncorrectCredentialsException(msg);
        }
        // 如果登录成功,则将用户角色和权限信息补充到主体中
        if (jwtToken.isLoginRequest()) {
            JwtAccount jwtAccount = (JwtAccount) info.getPrincipals().getPrimaryPrincipal();
            // 获取身份信息
            SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection(Set.of(jwtAccount), this.getName());
            // 调用父类方法,可以走缓存
            AuthorizationInfo authorizationInfo = super.getAuthorizationInfo(simplePrincipalCollection);
            // 补充角色
            jwtAccount.setRoles(authorizationInfo.getRoles());
            // 补充权限信息
            jwtAccount.setPermissions(authorizationInfo.getStringPermissions());
        }

    }
    
}

5.3.4 Realm 授权

@Slf4j
public class JwtAuthenticationRealm extends AuthorizingRealm {
    ...
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 1. 获取用户信息
        JwtAccount account = (JwtAccount) principals.getPrimaryPrincipal();
        // 2. 获取用户角色
        Set<String> accountRoles = roles.get(account.getAccount());
        // 3. 获取角色拥有的权限
        Set<String> accountPermissions = new HashSet<>();
        accountRoles.forEach(role -> {
            accountPermissions.addAll(permissions.get(role));
        });
        // 4. 授权信息
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //指定角色
        authorizationInfo.setRoles(accountRoles);
        // 权限字符串
        authorizationInfo.setStringPermissions(accountPermissions);
        return authorizationInfo;
    }
    ...
}

5.4 Controller

提供了三个请求

  1. 登录。 在这里生成jwt返回给客户端,也可以获取错误信息

    @RestController
    @Slf4j
    @RequestMapping("/jwt")
    public class JwtAuthenticateController {
        @PostMapping("/login")
        public Map<String, String> login(HttpServletRequest req) {
            Subject             subject = SecurityUtils.getSubject();
            Map<String, String> map     = new HashMap<>();
            if (subject.isAuthenticated()) {
                // 主体的标识,可以有多个,但是需要具备唯一性。比如:用户名,手机号,邮箱等。
                PrincipalCollection principalCollection = subject.getPrincipals();
                JwtAccount          account             = (JwtAccount) principalCollection.getPrimaryPrincipal();
                map.put("code", "0000");
                map.put("message", "登录成功");
                map.put("token", JwtUtil.generateToken(account));
    
            } else {
                String exceptionClassName = (String) req.getAttribute(JwtAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
                log.error("signinError:{}", exceptionClassName);
                String error = null;
                if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
                    error = "用户名/密码错误";
                } else if (IncorrectCredentialsException.class.getName().equals(exceptionClassName)) {
                    error = "用户名/密码错误";
                } else if (ExcessiveAttemptsException.class.getName().equals(exceptionClassName)) {
                    error = "登录次数过多";
                } else if (exceptionClassName != null) {
                    error = "其他错误:" + exceptionClassName;
                }
                map.put("code", "9999");
                map.put("message", error);
            }
            return map;
        }
    }
    
  2. 退出登录

    @RestController
    @Slf4j
    @RequestMapping("/jwt")
    public class JwtAuthenticateController {
            @PostMapping("/logout")
        public Map<String, String> logout() {
    
            Subject subject = SecurityUtils.getSubject();
            // 主体的标识,可以有多个,但是需要具备唯一性。比如:用户名,手机号,邮箱等。
            PrincipalCollection principalCollection = subject.getPrincipals();
            String              name                = principalCollection.getPrimaryPrincipal().toString();
            // 退出登录
            subject.logout();
            Map<String, String> resultMap = new HashMap<>();
            resultMap.put("name", name);
            resultMap.put("message", "退出登录成功");
            return resultMap;
        }
    }
    

    特别注意:退出登录后,应该让JWT失效了,此时要通过代码更换用户的秘钥,或者 jwtId等手段让旧的jwt发送过来验证的时候,不能通过验证。

  3. 首页授权访问

    @RestController
    @Slf4j
    @RequestMapping("/jwt/home")
    public class JwtHomeController {
        // 需要管理员角色才能访问
        @RequiresRoles("admin")
        @GetMapping()
        public Map<String, String> home() {
            // 现在将 subject理解成当前用户
            Subject subject = SecurityUtils.getSubject();
            // 用户凭证,简单理解成用户名
            PrincipalCollection principalCollection = subject.getPrincipals();
            String              account             = principalCollection.getPrimaryPrincipal().toString();
            // 返回结果
            return Map.of("account", account);
        }
    }
    

5.5 配置

5.5.1 自定义 ModularRealmAuthenticator

系统中的 自定义token和自定义的realm基本上是一一对应的,默认的ModularRealmAuthenticator 使用了 至少一个认证成功的策略。这里改写它是为了不使用默认测策略,不进行一个一个探测,什么样的token就交给它对应的realm去处理好了。

public class ExactMatchRealmAuthenticator extends ModularRealmAuthenticator {

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Realm realm = matchRealm(authenticationToken);
        if (realm != null) {
            return doSingleRealmAuthentication(realm, authenticationToken);
        }
        throw new AuthenticationException("无法找到匹配的Realm");
    }

    private Realm matchRealm(AuthenticationToken token) {
        Collection<Realm> realms = getRealms();

        for (Realm realm : realms) {
            if (realm.supports(token)) {
                return realm;
            }
        }
        return null;
    }
}

配置:

@Configuration
@Slf4j
public class ShiroConfiguration {
    ...
     @Bean
    protected Authenticator authenticator() {
        return new ExactMatchRealmAuthenticator();
    }
    ...
}

5.5.2 自定义SessionStorageEvaluator

前面在filter中,向request属性中放置了一个 SESSION_CREATION_ENABLED 表示是否创建session,所以这里从写DefaultWebSessionStorageEvaluator 是因为需要根据条件来决定是否创建session

public class ConditionalSessionStorageEvaluator extends DefaultWebSessionStorageEvaluator {

    // 在不需要创建session的时候,在Filter向 request中加入
    // request.getAttribute("SESSION_CREATION_ENABLED",false)
    // 禁用session创建,以提高性能
    public boolean isSessionCreationEnabled(ServletRequest request) {
        if (request != null) {
            Object val = request.getAttribute("SESSION_CREATION_ENABLED");
            if (val != null && val instanceof Boolean) {
                return (Boolean) val;
            }
        }
        //by default
        return true;
    }

    @Override
    public boolean isSessionStorageEnabled(Subject subject) {
        boolean result = super.isSessionStorageEnabled(subject);
        if (result) {
            if (subject instanceof WebSubject webSubject) {
                //web subject:
                result = isSessionCreationEnabled(webSubject.getServletRequest());
            }
        }
        return result;
    }
}

配置:

@Configuration
@Slf4j
public class ShiroConfiguration {
    ...
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        return new ConditionalSessionStorageEvaluator();
    }
    ...
}

5.5.3 配置Realm

@Configuration
@Slf4j
public class ShiroConfiguration {
    ...
    @Bean
    public Realm jwtRealm() {
        JwtAuthenticationRealm realm = new JwtAuthenticationRealm();
        realm.setCachingEnabled(true);
        realm.setAuthenticationCachingEnabled(true);
        // 认证缓存的名字,不设置也可以,默认
        realm.setAuthenticationCacheName("shiro:authentication:jwtCache");
        realm.setAuthorizationCachingEnabled(true);
        realm.setAuthorizationCacheName("shiro:authorization:apiCache");
        return realm;
    }
    ...
}

5.5.4 配置过滤器

@Configuration
@Slf4j
public class ShiroConfiguration {
    ...
    private ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        chainDefinition.addPathDefinition("/api/**", "apiAuthc");
        chainDefinition.addPathDefinition("/sys/**", "sysAuthc");
        chainDefinition.addPathDefinition("/jwt/**", "jwtAuthc");
        return chainDefinition;
    }
    
    private Map<String, Filter> getCustomerShiroFilter() {
        // API 认证过滤器
        ApiAuthenticationFilter apiAuthcFilter = new ApiAuthenticationFilter();

        // 用户名,密码认证过滤器
        AuthenticationFilter sysAuthcFilter = new AuthenticationFilter();
        sysAuthcFilter.setLoginUrl("/sys/login");

        // jwt 认证过滤器
        JwtAuthenticationFilter jwtAuthcFilter = new JwtAuthenticationFilter();
        //需要指定登录地址
        jwtAuthcFilter.setLoginUrl("/jwt/login");

        Map<String, Filter> filters = new HashMap<>();
        filters.put("apiAuthc", apiAuthcFilter);
        filters.put("sysAuthc", sysAuthcFilter);
        filters.put("jwtAuthc", jwtAuthcFilter);
        return filters;
    }
    
    ...
}

5.6 测试

5.6.1 登录

请求报文

POST /jwt/login HTTP/1.1
Host: 127.0.0.1:8080
Accept: */*
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded

username=administrator&password=admin

相应报文:

{
    "code": "0000",
    "message": "登录成功",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTIzMzI0ODUsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE3MTI5MzcyODUsImFjY291bnQiOiJhZG1pbmlzdHJhdG9yIiwicGVybWlzc2lvbnMiOlsiKjoqIiwiKiJdLCJyb2xlcyI6WyJhZG1pbiJdfQ.-xY6PA6bI1_YmIUcNwp5Bhz3WdylOtjRYn2hswFJwio"
}

5.6.2 访问home页

请求报文:将上面返回的token放入到 X-Access-Token 请求头中。

GET /jwt/home HTTP/1.1
Host: 127.0.0.1:8080
X-Access-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTIzMzI0ODUsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE3MTI5MzcyODUsImFjY291bnQiOiJhZG1pbmlzdHJhdG9yIiwicGVybWlzc2lvbnMiOlsiKjoqIiwiKiJdLCJyb2xlcyI6WyJhZG1pbiJdfQ.-xY6PA6bI1_YmIUcNwp5Bhz3WdylOtjRYn2hswFJwio
Accept: */*
Host: 127.0.0.1:8080
Connection: keep-alive

响应报文:

{
    "account": "JwtAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, secretKey=cbce2d1aad0867f8317e7ebeb3427333, salt=55ae2b2c63ddd6d4763e0c57bda9078e, jwtId=null, roles=null, permissions=null)"
}

5.6.3 退出

请求报文:

POST /jwt/logout HTTP/1.1
Host: 127.0.0.1:8080
X-Access-Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTIzMzI0ODUsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE3MTI5MzcyODUsImFjY291bnQiOiJhZG1pbmlzdHJhdG9yIiwicGVybWlzc2lvbnMiOlsiKjoqIiwiKiJdLCJyb2xlcyI6WyJhZG1pbiJdfQ.-xY6PA6bI1_YmIUcNwp5Bhz3WdylOtjRYn2hswFJwio
User-Agent: Apifox/1.0.0 (https://apifox.com)
Accept: */*
Host: 127.0.0.1:8080
Connection: keep-alive

响应报文:

{
    "name": "JwtAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, secretKey=cbce2d1aad0867f8317e7ebeb3427333, salt=55ae2b2c63ddd6d4763e0c57bda9078e, jwtId=null, roles=null, permissions=null)",
    "message": "退出登录成功"
}

6. 总结

本节是整个系列的最终一篇文章。本节中回顾了Shiro框架的流程,使得我们对Shiro的定制化开发更加游刃有余。

首先详细的介绍了什么是jwt,我们在shiro中使用 jwt的必要性。然后通过代码完成了整合。

本章还对 SessionStorageEvaluator 做了扩展,因为JWT不再需要使用session,而同一个项目中其它认证方式可能还需要session,这就需要根据情况来决定是否创建session。处理方式是在过滤器的预处理方法中,向reqeust中提前放入SESSION_CREATION_ENABLED 这个标识,然后在SessionStorageEvaluator 中获取这个标识来决定是否创建session

还对ModularRealmAuthenticator 做了扩展,不再使用默认的策略,而是什么样的token,就使用什么样的realm ,这样如果realm多了,每一个都试探效率会变低。

代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 9_springboot_shiro_jwt_多端认证鉴权_整合jwt.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1573183.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python接入AI 实现微信自动回复

import numpy as np # 引入numpy库&#xff0c;目的是将读取的数据转换为列表 import pandas as pd # 引入pandas库&#xff0c;用来读取csv数据 from uiautomation import WindowControl # 引入uiautomation库中的WindowControl类&#xff0c;用来进行图像识别和模拟操作 i…

go | 上传文件分析 | http协议分析 | 使用openssl 实现 https 协议 server.key、server.pem

是这样的&#xff0c;现在分析抓包数据 test.go package mainimport ("fmt""log""github.com/gin-gonic/gin" )func main() {r : gin.Default()// Upload single filer.MaxMultipartMemory 8 << 20r.POST("/upload", func(c *g…

Apache Log4j2 Jndi RCE CVE-2021-44228漏洞原理讲解

Apache Log4j2 Jndi RCE CVE-2021-44228漏洞原理讲解 一、什么是Log4j2二、环境搭建三、简单使用Log4j2四、JDNI和RMI4.1、启动一个RMI服务端4.2、启动一个RMI客户端4.3、ldap 五、漏洞复现六、Python批量检测 参考视频&#xff1a;https://www.bilibili.com/video/BV1mZ4y1D7K…

基于Socket简单的TCP网络程序

⭐小白苦学IT的博客主页 ⭐初学者必看&#xff1a;Linux操作系统入门 ⭐代码仓库&#xff1a;Linux代码仓库 ❤关注我一起讨论和学习Linux系统 TCP单例模式的多线程版本的英汉互译服务器 我们先来认识一下与udp服务器实现的不同的接口&#xff1a; TCP服务器端 socket()&…

【C++初阶】String在OJ中的使用(一):仅仅反转字母、字符串中的第一个唯一字母、字符串最后一个单词的长度、验证回文串、字符串相加

前言&#xff1a; &#x1f3af;个人博客&#xff1a;Dream_Chaser &#x1f388;博客专栏&#xff1a;C &#x1f4da;本篇内容&#xff1a;仅仅反转字母、字符串中的第一个唯一字母、字符串最后一个单词的长度、验证回文串、字符串相加 目录 917.仅仅反转字母 题目描述&am…

【stm32】软件I2C读写MPU6050

软件I2C读写MPU6050(文章最后附上源码) 编码 概况 首先建立通信层的.c和.h模块 在通信层里写好I2C底层的GPIO初始化 以及6个时序基本单元 起始、终值、发送一个字节、接收一个字节、发送应答、接收应答 写好I2C通信层之后&#xff0c;再建立MPU6050的.c和.h模块 基于I2C通…

软考116-上午题-【计算机网络】-LINUX命令

一、真题 真题1&#xff1a; 真题2&#xff1a; 权限通常分为三类&#xff1a; 读&#xff08;r&#xff09;&#xff1a;允许读取文件内容或列出目录内容。写&#xff08;w&#xff09;&#xff1a;允许修改文件内容或在目录中创建/删除文件。执行&#xff08;x&#xff09;&…

stm32开发之threadx使用记录(主逻辑分析)

前言 threadx的相关参考资料 论坛资料、微软官网本次使用的开发板为普中科技–麒麟&#xff0c;核心芯片为 stm32f497zgt6开发工具选择的是stm32cubemx(代码生成工具)clion(代码编写工具)编译构建环境选择的是arm-none-gcc编译 本次项目结构 CMakeList对应的配置 set(CMAKE_…

SD-WAN国际网络专线:高效、合规且可靠的跨境连接解决方案

在数字化时代&#xff0c;企业对跨境网络连接的需求日益增长。SD-WAN技术作为一种新兴的解决方案&#xff0c;正逐渐成为构建跨境网络连接的首选。本文将探讨SD-WAN国际网络专线的发展现状、合规性要求以及选择时需要考虑的关键因素。 SD-WAN技术&#xff1a;跨境网络连接的新…

如何在没有备份的情况下从 iPad 恢复照片?

有很多操作都可能导致iPad照片丢失&#xff0c;包括误删除、出厂设置、iPad的iOS更新等。如果没有备份&#xff0c;似乎没有办法找回它们。然而&#xff0c;即使您将备份保留在 iCloud 或iTunes上&#xff0c;这些方式也需要您的 iPad 首先重置&#xff0c;从而用备份内容覆盖当…

堆排序解读

在算法世界中&#xff0c;排序算法一直是一个热门话题。推排序&#xff08;Heap Sort&#xff09;作为一种基于堆这种数据结构的有效排序方法&#xff0c;因其时间复杂度稳定且空间复杂度低而备受青睐。本文将深入探讨推排序的原理、实现方式&#xff0c;以及它在实际应用中的价…

lua学习笔记5(分支结构和循环的学习)

print("*****************分支结构和循环的学习******************") print("*****************if else语句******************") --if 条件 then end a660 b670 --单分支 if a<b thenprint(a) end --双分支 if a>b thenprint("满足条件")…

机器学习模型——逻辑回归

https://blog.csdn.net/qq_41682922/article/details/85013008 https://blog.csdn.net/guoziqing506/article/details/81328402 https://www.cnblogs.com/cymx66688/p/11363163.html 参数详解 逻辑回归的引出&#xff1a; 数据线性可分可以使用线性分类器&#xff0c;如果…

c# wpf LiveCharts 简单试验

1.概要 1.1 说明 1.2 环境准备 NuGet 添加插件安装 2.代码 <Window x:Class"WpfApp3.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"…

WindowsPowerShell安装配置Vim的折腾记录

说明 vim一直以来都被称为编辑器之神一样的存在。但用不用vim完全取决于你自己&#xff0c;但是作为一个学计算机的同学来说&#xff0c;免不了会和Linux打交道&#xff0c;而大部分的Linux操作系统都预装了vim作为编辑器&#xff0c;如果是简单的任务&#xff0c;其实vim只要会…

电商技术揭秘八:搜索引擎中的SEO内部链接建设与外部推广策略

文章目录 引言一、 内部链接结构优化1.1 清晰的导航链接1. 简洁明了的菜单项2. 逻辑性的布局3. 避免深层次的目录结构4. 使用文本链接5. 突出当前位置6. 移动设备兼容性 1.2 面包屑导航1. 显示当前页面位置2. 可点击的链接3. 简洁性4. 适当的分隔符5. 响应式设计6. 避免重复主页…

图像分割-RSPrompter

文章目录 前言1. 自动化提示器1.1 多尺度特征增强器1.2 RSPrompterAnchor-based PrompterQuery-based Prompter 2. SAM的扩展3. 结果WHU数据集NWPU数据集SSDD数据集 前言 《RSPrompter: Learning to prompt for remote sensing instance segmentation based on visual foundati…

Linux--03---虚拟机网络配置、拍摄快照和克隆

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1.虚拟机网络配置1.虚拟机的联网模式模式1 仅主机模式特点模式2 桥接模式特点模式3 NAT模式特点关于模式的选择 2. 修改网络配置信息3.修改虚拟机ens33网卡的网络配…

「 典型安全漏洞系列 」12.OAuth 2.0身份验证漏洞

在浏览网页时&#xff0c;你肯定会遇到允许你使用社交媒体帐户登录的网站。此功能一般是使用流行的OAuth 2.0框架构建的。本文主要介绍如何识别和利用OAuth 2.0身份验证机制中发现的一些关键漏洞。 1. OAuth产生背景 为了更好的理解OAuth&#xff0c;我们假设有如下场景&#…

分享一个基于Multi-SLAM+3DGS的新一代三维内容生产技术

基于智能空间计算&#xff0c;新一代超逼真三维内容生成技术。 可自动化生成超逼真的大场景三维模型&#xff0c;并在各类终端和空间计算设备中&#xff0c;实现前所未有的沉浸式体验。 更可接入专业三维软件和应用平台&#xff0c;进行深度的模型开发与场景落地。 支持超大复杂…