Spring Security和OpenID Connect_springsecurity openid-程序员宅基地

技术标签: spring  spring boot  java  系统安全  后端  Spring Security OAuth2  

Spring Security和OpenID Connect

概述

OpenID Connect 是一个开放标准,由 OpenID 基金会于 2014 年 2 月发布。它定义了一种使用 OAuth 2.0 执行用户身份认证的互通方式。OpenID Connect 直接基于 OAuth 2.0 构建,并保持与它兼容。

当授权服务器支持 OIDC 时,它有时被称为身份提供者(Idp),因为它向客户端提供有关资源所有者的信息。而客户端映射为OpenID Connect 流程中登录依赖方(RP)。在本文中我们将授权服务称为身份提供者,客户端称为登录依赖方进行陈述。

OpenID Connect 流程看起来与 OAuth 相同。主要区别是,在授权请求中,使用了一个特定的范围openid,而在获取token中,登录依赖方(RP)同时接收到一个访问令牌和一个ID 令牌(经过签名的 JWT)。ID令牌与访问令牌不同的是,ID 令牌是发送给 RP 的,并且要被它解析。

本文您将学到

  • 配置授权服务支持OpenID Connect
  • 自定义ID令牌
  • 登录依赖方通过OAuth2UserService实现权限映射

先决条件:

  • java 8+
  • mysql

使用Spring Authorization Server搭建身份提供服务(IdP)

本节中我们将使用Spring Authorization Server搭建身份提供服务,并通过OAuth2TokenCustomizer实现自定义ID Token。

maven 依赖项
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-authorization-server</artifactId>
  <version>0.3.1</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.6.7</version>
</dependency>
配置

首先我们配置身份提供服务端口8080:

server:
  port: 8080

接下来我们创建AuthorizationServerConfig配置类,在此类中我们配置OAuth2及OICD相关Bean。我们首先注册一个客户端:

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
    
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("relive-client")
                .clientSecret("{noop}relive-client")
                .clientAuthenticationMethods(s -> {
    
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
                })
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-client-oidc")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .scope(OidcScopes.EMAIL)
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true)
                        .requireProofKey(false)
                        .build())
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) 
                        .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                        .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
                        .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
                        .reuseRefreshTokens(true)
                        .build())
                .build();
        return new InMemoryRegisteredClientRepository(registeredClient);
    }

我们正在配置的属性是:

  • clientId – Spring Security将使用它来识别哪个客户端正在尝试访问资源
  • clientSecret——客户端和服务器都知道的一个秘密,它提供了两者之间的信任
  • 客户端验证方式——在我们的例子中,我们将支持basic和post身份验证方式
  • 授权类型——允许客户端生成授权码和刷新令牌
  • 重定向 URI – 客户端将在基于重定向的流程中使用它
  • scope——此参数定义客户端可能拥有的权限。在我们的例子中,我们将拥有所需的OidcScopes.OPENID和用来获取额外的身份信息OidcScopes.PROFILEOidcScopes.EMAIL

OpenID Connect 使用一个特殊的权限范围值 openid 来控制对 UserInfo 端点的访问。 OpenID Connect 定义了一组标准化的 OAuth 权限范围,对应于用户属性的子集profile、email、 phone、address,参见表格:

权限范围 声明
openid sub
profile Name、family_name、given_name、middle_name、nickname、preferred_username、profile、 picture、website、gender、birthdate、zoneinfo、locale、updated_at
email email、email_verified
address address,是一个 JSON 对象、包含 formatted、street_address、locality、region、postal_code、country
phone phone_number、phone_number_verified

让我们根据上述规范定义OidcUserInfoService,用于扩展/userinfo用户信息端点响应:

public class OidcUserInfoService {
    

    public OidcUserInfo loadUser(String name, Set<String> scopes) {
    
        OidcUserInfo.Builder builder = OidcUserInfo.builder().subject(name);
        if (!CollectionUtils.isEmpty(scopes)) {
    
            if (scopes.contains(OidcScopes.PROFILE)) {
    
                builder.name("First Last")
                        .givenName("First")
                        .familyName("Last")
                        .middleName("Middle")
                        .nickname("User")
                        .preferredUsername(name)
                        .profile("http://127.0.0.1:8080/" + name)
                        .picture("http://127.0.0.1:8080/" + name + ".jpg")
                        .website("http://127.0.0.1:8080/")
                        .gender("female")
                        .birthdate("2022-05-24")
                        .zoneinfo("China/Beijing")
                        .locale("zh-cn")
                        .updatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
            }
            if (scopes.contains(OidcScopes.EMAIL)) {
    
                builder.email(name + "@163.com").emailVerified(true);
            }
            if (scopes.contains(OidcScopes.ADDRESS)) {
    
                JSONObject address = new JSONObject();
                address.put("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"));
                builder.address(address.toJSONString());
            }
            if (scopes.contains(OidcScopes.PHONE)) {
    
                builder.phoneNumber("13728903134").phoneNumberVerified("false");
            }
        }
        return builder.build();
    }
}


接下来,我们将配置一个 bean 以应用默认 OAuth 安全性。使用上述OidcUserInfoService配置OIDC中UserInfoMapper;oauth2ResourceServer()配置资源服务器使用JWT验证,用来保护Spring Security 提供的/userinfo端点;对于未认证请求我们会将它重定向到/login 登录页:

注意:有时“授权服务器”和“资源服务器”是同一台服务器。

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();

        //自定义用户映射器
        Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = (context) -> {
    
            OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
            JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
            return userInfoService.loadUser(principal.getName(), context.getAccessToken().getScopes());
        };
        authorizationServerConfigurer.oidc((oidc) -> {
    
            oidc.userInfoEndpoint((userInfo) -> userInfo.userInfoMapper(userInfoMapper));
        });

        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

        return http.requestMatcher(endpointsMatcher).authorizeRequests((authorizeRequests) -> {
    
            ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) authorizeRequests.anyRequest()).authenticated();
        }).csrf((csrf) -> {
    
            csrf.ignoringRequestMatchers(new RequestMatcher[]{
    endpointsMatcher});
        }).apply(authorizationServerConfigurer)
                .and()
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .exceptionHandling(exceptions -> exceptions.
                        authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
                .apply(authorizationServerConfigurer)
                .and()
                .build();
    }

每个授权服务器都需要其用于令牌的签名密钥,让我们生成一个 2048 字节的 RSA 密钥:

@Bean
public JWKSource<SecurityContext> jwkSource() {
    
  RSAKey rsaKey = Jwks.generateRsa();
  JWKSet jwkSet = new JWKSet(rsaKey);
  return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

static class Jwks {
    

  private Jwks() {
    
  }

  public static RSAKey generateRsa() {
    
    KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    return new RSAKey.Builder(publicKey)
      .privateKey(privateKey)
      .keyID(UUID.randomUUID().toString())
      .build();
  }
}

static class KeyGeneratorUtils {
    

  private KeyGeneratorUtils() {
    
  }

  static KeyPair generateRsaKey() {
    
    KeyPair keyPair;
    try {
    
      KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
      keyPairGenerator.initialize(2048);
      keyPair = keyPairGenerator.generateKeyPair();
    } catch (Exception ex) {
    
      throw new IllegalStateException(ex);
    }
    return keyPair;
  }
}

然后我们将使用带有@EnableWebSecurity注释的配置类启用 Spring Web 安全模块:

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class DefaultSecurityConfig {
    

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin(withDefaults());
        return http.build();
    }

    //...
}

这里我们使用Form表单认证方式,所以我们还需要为登录认证提供用户名和密码:

    @Bean
    UserDetailsService users() {
    
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

至此,我们服务配置完成,但是用于给客户端传递权限信息,我们将更改ID Token claim,添加用户角色属性:

@Configuration(proxyBeanMethods = false)
public class IdTokenCustomizerConfig {
    

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
    
        return (context) -> {
    
            if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
    
                context.getClaims().claims(claims ->
                        claims.put("role", context.getPrincipal().getAuthorities()
                                .stream().map(GrantedAuthority::getAuthority)
                                .collect(Collectors.toSet())));
            }
        };
    }
}

登录依赖方服务(RP)实现

本节中我们将使用Spring Security搭建登录依赖方服务,并设计相关数据库表结构表达身份提供方服务与登录依赖方服务权限关系,通过OAuth2UserService实现权限映射。

本节中部分代码涉及JPA相关知识,如果您并不了解也没有关系,您可以通过Mybatis进行替换。

maven 依赖项
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
  <version>2.6.7</version>  
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jdbc</artifactId>
  <version>2.6.7</version>  
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
  <version>2.6.7</version>  
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
  <version>2.6.7</version>  
</dependency>

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>  
</dependency>
相关数据库表结构

这是我们本文中RP服务使用的相关数据库表,涉及相关创建表及初始化数据的SQL语句可以从这里获取。
在这里插入图片描述

配置

首先我们通过application.yml文件中配置服务端口和数据库连接信息:

server:
  port: 8070
  servlet:
    session:
      cookie:
        name: CLIENT-SESSION

spring:
  datasource:
    druid:
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/oidc_login?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: <<root>> # 修改用户名
      password: <<password>> # 修改密码

接下来我们将启用Spring Security安全配置。使用Form认证方式;并使用oauth2Login()定义OAuth2登录默认配置:

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
        http.authorizeHttpRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin(from -> {
    
                    from.defaultSuccessUrl("/home");
                })
                .oauth2Login(Customizer.withDefaults())
                .csrf().disable();
        return http.build();
    }

下面我们将配置OAuth2客户端基于MySql数据库的存储方式,你也可以从Spring Security 持久化OAuth2客户端了解详细信息。

    /**
     * 定义JDBC 客户端注册存储库
     *
     * @param jdbcTemplate
     * @return
     */
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
    
        return new JdbcClientRegistrationRepository(jdbcTemplate);
    }

    /**
     * 负责{@link org.springframework.security.oauth2.client.OAuth2AuthorizedClient}在 Web 请求之间进行持久化
     *
     * @param jdbcTemplate
     * @param clientRegistrationRepository
     * @return
     */
    @Bean
    public OAuth2AuthorizedClientService authorizedClientService(
            JdbcTemplate jdbcTemplate,
            ClientRegistrationRepository clientRegistrationRepository) {
    
        return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
    }

    /**
     * OAuth2AuthorizedClientRepository 是一个容器类,用于在请求之间保存和持久化授权客户端
     *
     * @param authorizedClientService
     * @return
     */
    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository(
            OAuth2AuthorizedClientService authorizedClientService) {
    
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }

我们不在使用基于内存的用户名密码,在初始化数据库时我们已经将用户名密码添加到user表中,所以我们需要实现UserDetailsService接口用于Form认证时获取用户信息:

@RequiredArgsConstructor
public class JdbcUserDetailsService implements UserDetailsService {
    

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
        com.relive.entity.User user = userRepository.findUserByUsername(username);
        if (ObjectUtils.isEmpty(user)) {
    
            throw new UsernameNotFoundException("user is not found");
        }
        if (CollectionUtils.isEmpty(user.getRoleList())) {
    
            throw new UsernameNotFoundException("role is not found");
        }
        Set<SimpleGrantedAuthority> authorities = user.getRoleList().stream().map(Role::getRoleCode)
                .map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}

这里UserRepository继承了JpaRepository,提供user表的CRUD,详细代码可以在文末链接中获取。


现在我们将要解决如何将IdP服务用户角色映射为RP服务已有的角色,在前面文章中曾使用GrantedAuthoritiesMapper映射角色。在本文中我们将使用OAuth2UserService添加角色映射策略,它与GrantedAuthoritiesMapper相比更加灵活:

public class OidcRoleMappingUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
    
    private OidcUserService oidcUserService;
    private final OAuth2ClientRoleRepository oAuth2ClientRoleRepository;

    //...

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
    
        OidcUser oidcUser = oidcUserService.loadUser(userRequest);

        OidcIdToken idToken = userRequest.getIdToken();
        List<String> role = idToken.getClaimAsStringList("role");
        Set<SimpleGrantedAuthority> mappedAuthorities = role.stream()
                .map(r -> oAuth2ClientRoleRepository.findByClientRegistrationIdAndRoleCode(userRequest.getClientRegistration().getRegistrationId(), r))
                .map(OAuth2ClientRole::getRole).map(Role::getRoleCode).map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
        oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

        return oidcUser;
    }
}

最后我们将创建HomeController,通过控制页面中展示内容使测试效果视觉上更加显著,我们将根据角色展示不同信息,使用thymeleaf模版引擎渲染。

@Controller
public class HomeController {
    

    private static Map<String, List<String>> articles = new HashMap<>();

    static {
    
        articles.put("ROLE_OPERATION", Arrays.asList("Java"));
        articles.put("ROLE_SYSTEM", Arrays.asList("Java", "Python", "C++"));
    }

    @GetMapping("/home")
    public String home(Authentication authentication, Model model) {
    
        String authority = authentication.getAuthorities().iterator().next().getAuthority();
        model.addAttribute("articles", articles.get(authority));
        return "home";
    }
}

完成配置后,我们可以访问 http://127.0.0.1:8070/login 进行测试。

结论

在本文中分享了Spring Security对于OpenID Connect的支持。如果您有任何问题,请在下面发表评论。

与往常一样,本文中使用的源代码可在 GitHub 上获得。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/new_ord/article/details/126108524

智能推荐

Redis为什么变慢了?一文讲透如何排查Redis性能问题 | 万字长文_redis 3.0.5 内存超5g后写入速度慢-程序员宅基地

文章浏览阅读396次。Redis 作为优秀的内存数据库,其拥有非常高的性能,单个实例的 OPS 能够达到 10W 左右。但也正因此如此,当我们在使用 Redis 时,如果发现操作延迟变大的情况,就会与我们的预期不符。你也许或多或少地,也遇到过以下这些场景:在 Redis 上执行同样的命令,为什么有时响应很快,有时却很慢?为什么 Redis 执行 SET、DEL 命令耗时也很久?为什么我的 Redis 突然慢了一波,之后又恢复正常了?为什么我的 Redis 稳定运行了很久,突然从某个时间点开始变慢了?如果你并不清楚 _redis 3.0.5 内存超5g后写入速度慢

提高对C++的认识_对c++的认知-程序员宅基地

文章浏览阅读1k次。C++中有很多 "东西":C,重载,面向对象,模板,例外,名字空间。这么多东西,有时让人感到不知所措。怎么弄懂所有这些东西呢?C++之所以发展到现在这个样子,在于它有自己的设计目标。理解了这些设计目标,就不难弄懂所有这些东西了。C++最首要的目标在于:· 和C的兼容性。很多很多C还存在,很多很多C程序员还存在。C++利用了这一基础,并建立在 ---- 我是指 "平衡在" ---- 这一基础_对c++的认知

软件项目管理课程设计-数字化校园学工信息系统-程序员宅基地

文章浏览阅读2w次,点赞67次,收藏594次。数字化校园学工信息系统小组成员 分工明细:目录一、引言 31.1编写目的 31.2 背景 31.3定义 41.4 参考资料 4二、项目概述 42.1 项目目标 42.2产品目..._软件项目管理课程设计

计算机操作系统实验报告:进程同步与互斥_进程同步与互斥实验总结-程序员宅基地

文章浏览阅读1.1k次,点赞3次,收藏10次。进程同步与互斥。_进程同步与互斥实验总结

vue-实现一个购物车结算页面_vue写购物车界面选择不同尺码对应的价格-程序员宅基地

文章浏览阅读1.8k次。这是路由之间的跳转,传递值最好采用传参,而不是用$emit和$on,不起作用如果实在一个页面中的兄弟组件,可以使用$emit和$on中间件,eventBus.js 放在components目录下面图片路径 static/img模拟数据/static/data.json{"status":1,"result":{"totalMoney":59,"list":[{"pr..._vue写购物车界面选择不同尺码对应的价格

【bandgap】无运放带隙基准电路_不用运放的基准-程序员宅基地

文章浏览阅读605次。M6,M7,Q3支路为M3,M4提供偏置电压,同时起负反馈作用,使节点①电压等于节点②电压。假设节点③电压不等于节点⑤电压,如果V⑤>V③,由VBE1<VBE3得到I1<I5,而由VGS3>VGS7得到I1>I5,与前面得到的结论相矛盾,所以,V⑤=V③,I1=I5,VGS1=VGS6,从而得到节点①电压等于节点②电压。因为M3和M4传输同样的电流,漏极电压又相等,它们接在同一个栅极电压上,所以,V④=V③。刚接通电源时,节点⑥为低电平。图2中,M1,M2,M5,M6宽长比的比例为2∶1∶1∶2。_不用运放的基准

随便推点

第二类斯特林数_n等于2时第二类斯特林数这么用啊-程序员宅基地

文章浏览阅读870次。第二类斯特林数定义:第二类Stirling数实际上是集合的一个拆分,表示将n个不同的元素拆分成m个集合的方案数,记为 或者 。和第一类Stirling数不同的是,集合内是不考虑次序的,而圆排列是有序的。常常用于解决组合数学中几类放球模型。描述为:将n个不同的球放入m个无差别的盒子中,要求盒子非空,有几种方案?递推式第二类Stirling数的推导和第一类Stirling数类似,可_n等于2时第二类斯特林数这么用啊

Excel中插入柱状图以及在图下方显示数据表_柱状图下面怎么带一个表格-程序员宅基地

文章浏览阅读2.1w次。在Sheet中插入柱状图在Excel 中, 有如下数据:上面的数据是学生学科分数的统计,第一行是学科第一列是姓名要插入上面数据的柱状图的步骤如下:选中数据单元格, 包含标头点击 “INSERT” (插入)标签页找到柱状图,点击选择需要的样式, 这里选择堆积柱状图, 也就是可以直观看到每个学生的总分统计。插入的柱状图的效果如下:柱状图的样式有很多种, 除了堆积柱状图之外, 还有簇型、百分比以及三维等, 详细的图的类型有。图表标题鼠标点击图表标题可以直接进行编辑,如下图_柱状图下面怎么带一个表格

log4j升级至log4j2_log4j转log4j2-程序员宅基地

文章浏览阅读1.3w次,点赞5次,收藏24次。本文主要内容包含:实现log4j升级至log4j2,并实现日志自动删除的操作步骤以及注意事项。一、升级原因:log4j存在天然缺陷:log4j采用同步输出模式,当遇到高并发&amp;日志输出过多情况,可能导致线程阻塞,消耗时间过大log4j无法实现自动删除按照日期产生的日志,现有项目都采用定时脚本删除日志。通过调研,log4j2采用异步输出,并且能通过配置实现自动删除日志..._log4j转log4j2

快速恢复Windows 2000/XP遗忘的管理员密码 _windowsadministrator删除了怎么恢复-程序员宅基地

文章浏览阅读881次。  一、删除SAM文件,清除Administrator账号密码  Windows 2000所在的Winnt/System32/Config目录下有个SAM文件(即账号密码数据库文件),它保存了Windows 2000中所有的用户名和密码。当你登录的时候,系统就会把你键入的用户名和密码,与SAM文件中的加密数据进行校对,如果两者完全符合,则会顺利进入系统,否则将无法登录,因此我们可以使用删除SAM文_windowsadministrator删除了怎么恢复

前端工作经验总结以及技术分享_前端技术分享-程序员宅基地

文章浏览阅读8.6k次,点赞74次,收藏324次。uniapp H5 公众号微信自定义分享qq,微信带图片标题内容__揽的博客-程序员宅基地js获取当前日期,并将其格式化为YYYY-MM-DD,并且自定义返回__揽的博客-程序员宅基地_js获取当前日期并格式化(成功最详细版本,自定义传参失败,跳转出现空白页面,校验文件失败)微信小程序扫码跳转小程序指定页面保姆级教程__揽的博客-程序员宅基地_小程序二维码校验文件(巨好使,详细,解析URl,URl自定义传参)js URl中快速自定义传参,并且实现参数解析 修改 和新增,替换功能__揽的博客-程序员宅基地。_前端技术分享

android webview访问本地文件_android webview 本地文件-程序员宅基地

文章浏览阅读3.1k次。android webview访问本地文件_android webview 本地文件

推荐文章

热门文章

相关标签