草庐IT

Spring Security 6.0(spring boot 3.0) 下认证配置流程

银之石 2023-04-09 原文

目录

前提

强烈建议在学习完 2.x 版本的配置流程之后再阅读本文

推荐一个:视频教程

将要实现的功能

  1. 使用用户名+密码+验证码+记住我功能进行登陆
  2. CSRF校验
  3. 将Session交给Redis管理,将记住我功能持久化到数据库

依赖(POM)

数据库操作部分省略了

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>3.0.0</version>
	<relativePath/>
</parent>
<dependencies>
     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <optional>true</optional>
    </dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<!--redis-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	<!--session-redis-->
	<dependency>
		<groupId>org.springframework.session</groupId>
		<artifactId>spring-session-data-redis</artifactId>
	</dependency>
	<!--验证码-->
	<dependency>
		<groupId>com.github.penggle</groupId>
		<artifactId>kaptcha</artifactId>
		<version>2.3.2</version>
	</dependency>	
	<!--springdoc -->
	<dependency>
		<groupId>org.springdoc</groupId>
		<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
		<version>2.0.0</version>
	</dependency>
	<!--knife4j - 接口文档UI-->
	<dependency>
		<groupId>com.github.xiaoymin</groupId>
		<artifactId>knife4j-springdoc-ui</artifactId>
		<!--在引用时请在maven中央仓库搜索3.X最新版本号-->
		<version>3.0.3</version>
	</dependency>
</dependencies>

注:结尾包含了 springdoc+knife4j 生成接口文档,示例代码中也包含了springdoc提供的注解。

示例代码

基础组件

验证码

生成配置(与视频教程中一致)

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer kaptcha() {
        final Properties properties = new Properties();
        //高度
        properties.setProperty("kaptcha.image.width", "150");
        //宽度
        properties.setProperty("kaptcha.image.height", "50");
        //可选字符串
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        //验证码长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");

        final DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(new Config(properties));
        return defaultKaptcha;
    }
}

接口

生成验证码,保存到SessionAttribute中,后续验证时也从这里取出,两个接口返回不同格式的验证码数据。

@Controller
@RequestMapping("/sys/verifyCode")
@RequiredArgsConstructor
@Tag(name = "验证码接口")
public class VerifyCodeController {
    public static final String VERIFY_CODE_KEY = "vc";
    private final Producer producer;

    @GetMapping("/base64")
    @Operation(summary = "Base64格式")
    @ResponseBody
    public Res<String> base64(@Parameter(hidden = true) HttpSession httpSession) throws IOException {
        //生成验证码
        final BufferedImage image = createImage(httpSession);
        //响应图片
        final FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        ImageIO.write(image, "jpeg", os);
        //返回 base64
        return Res.of(Base64.encodeBase64String(os.toByteArray()));
    }

    @GetMapping("/image")
    @Operation(summary = "图片格式")
    public void image(@Parameter(hidden = true) HttpServletResponse response, @Parameter(hidden = true) HttpSession httpSession) throws IOException {
        final BufferedImage image = createImage(httpSession);
        //响应图片
        response.setContentType(MimeTypeUtils.IMAGE_JPEG_VALUE);
        ImageIO.write(image, "jpeg", response.getOutputStream());
    }

    private BufferedImage createImage(HttpSession httpSession) {
        //生成验证码
        final String verifyCode = producer.createText();
        //保存到 session 中(或redis中)
        httpSession.setAttribute(VERIFY_CODE_KEY, verifyCode);
        //生成图片
        return producer.createImage(verifyCode);
    }
}

MyUserDetailsServiceImpl(认证/权限信息)

  • 这里没什么特别的,根据用户名查询并返回用户的认证信息,SystemUserService提供数据库访问接口
  • 由于我们实现了UserDetailsPasswordServiceSpringSecurity如果发现用户的密码加密方法过时或明文,将会自动修改密码。
  • createUser方法是调用了SpringSecurity提供的User.UserBuilder构造了一个UserDetails
  • 因为尚未涉及到鉴权部分,这里在权限处直接给了一个空列表,这里如果不写会报错。
  • @Service直接注册到容器
@Service
@RequiredArgsConstructor
public class MyUserDetailsServiceImpl implements UserDetailsService, UserDetailsPasswordService {

    private final SystemUserService systemUserService;

    /**
     * 当前用户
     * @return 当前用户
     */
    public SystemUser currentUser() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        final String username = ((UserDetails) authentication.getPrincipal()).getUsername();
        return systemUserService.getByUsername(username);
    }

    /**
     * 根据用户名查询用户的认证授权信息
     * @param username 用户名
     * @return org.springframework.security.core.userdetails.UserDetails
     * @throws UsernameNotFoundException 异常
     * @since 2022/12/6 15:03
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        final SystemUser systemUser = systemUserService.getByUsername(username);
        if (systemUser == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return systemUser.createUser()
                .authorities(new ArrayList<>())
                .build();
    }

    /**
     * 修改密码
     * @param user        用户
     * @param newPassword 新密码
     * @return UserDetails
     */
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        final SystemUser systemUser = systemUserService.getByUsername(user.getUsername());
        systemUser.setPassword(newPassword);
        systemUserService.updateById(systemUser);
        return systemUser.createUser()
                .authorities(new ArrayList<>())
                .build();
    }
}

MyAuthenticationHandler(Handler)

因为这些接口里都只有一个方法,且都需要类似的处理,我把它集中到一起实现,流程几乎是一致的:

  1. 设置Content-Typeapplication/json;charset=UTF-8
  2. 根据情况设置状态码
  3. 将返回结果写入到response

唯一需要注意的地方是,登陆成功后需要清理已使用过的验证码

注意:我们有两个地方需要用到这个对象,所以直接注册到容器中方便注入

@Component
public class MyAuthenticationHandler implements AuthenticationSuccessHandler
        , AuthenticationFailureHandler
        , LogoutSuccessHandler
        , SessionInformationExpiredStrategy
        , AccessDeniedHandler, AuthenticationEntryPoint {

    public static final String APPLICATION_JSON_CHARSET_UTF_8 = "application/json;charset=UTF-8";
    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    /**
     * 认证失败处理
     * @param request       that resulted in an <code>AuthenticationException</code>
     * @param response      so that the user agent can begin authentication
     * @param authException that caused the invocation
     * @throws IOException      异常
     * @throws ServletException 异常
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        String detailMessage = e.getClass().getSimpleName() + " " + e.getLocalizedMessage();
        if (e instanceof InsufficientAuthenticationException) {
            detailMessage = "请登陆后再访问";
        }
        response.setContentType(APPLICATION_JSON_CHARSET_UTF_8);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().println(OBJECT_MAPPER.writeValueAsString(Res.of(detailMessage, "认证异常")));
    }

    /**
     * 权限不足时的处理
     * @param request               that resulted in an <code>AccessDeniedException</code>
     * @param response              so that the user agent can be advised of the failure
     * @param accessDeniedException that caused the invocation
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        String detailMessage = null;
        if (accessDeniedException instanceof MissingCsrfTokenException) {
            detailMessage = "缺少CSRF TOKEN,请从表单或HEADER传入";
        } else if (accessDeniedException instanceof InvalidCsrfTokenException) {
            detailMessage = "无效的CSRF TOKEN";
        } else if (accessDeniedException instanceof CsrfException) {
            detailMessage = accessDeniedException.getLocalizedMessage();
        } else if (accessDeniedException instanceof AuthorizationServiceException) {
            detailMessage = AuthorizationServiceException.class.getSimpleName() + " " + accessDeniedException.getLocalizedMessage();
        }
        response.setContentType(APPLICATION_JSON_CHARSET_UTF_8);
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().println(OBJECT_MAPPER.writeValueAsString(Res.of(detailMessage, "禁止访问")));
    }

    /**
     * 认证失败时的处理
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType(APPLICATION_JSON_CHARSET_UTF_8);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().println(OBJECT_MAPPER.writeValueAsString(Res.of(exception.getLocalizedMessage(), "登陆失败")));
    }

    /**
     * 认证成功时的处理
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType(APPLICATION_JSON_CHARSET_UTF_8);
        response.setStatus(HttpStatus.OK.value());
        // SecurityContext在设置Authentication的时候并不会自动写入Session,读的时候却会根据Session判断,所以需要手动写入一次,否则下一次刷新时SecurityContext是新创建的实例。
        //  https://yangruoyu.blog.csdn.net/article/details/128276473
        request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
        response.getWriter().println(OBJECT_MAPPER.writeValueAsString(Res.of(MyUserDetails.of(authentication), "登陆成功")));
        //清理使用过的验证码
        request.getSession().removeAttribute(VERIFY_CODE_KEY);
    }

    /**
     * 会话过期处理
     * @throws IOException      异常
     * @throws ServletException 异常
     */
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        String message = "该账号已从其他设备登陆,如果不是您自己的操作请及时修改密码";
        final HttpServletResponse response = event.getResponse();
        response.setContentType(APPLICATION_JSON_CHARSET_UTF_8);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().println(OBJECT_MAPPER.writeValueAsString(Res.of(event.getSessionInformation(), message)));
    }

    /**
     * 登出成功处理
     * @param request        请求
     * @param response       响应
     * @param authentication 认证信息
     * @throws IOException      异常
     * @throws ServletException 异常
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType(APPLICATION_JSON_CHARSET_UTF_8);
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().println(OBJECT_MAPPER.writeValueAsString(Res.of(MyUserDetails.of(authentication), "注销成功")));
    }
}

MyRememberMeServices(记住我)

记住我功能,规定了:

  1. requestAttribute中获取rememberMe字段
  2. 当字段值为TRUE_VALUES表的成员时认为需要开启记住我功能

构造函数中

  1. PersistentTokenRepository会在后续提供
  2. UserDetailsService已在前文提供

注意:我们有两个地方需要用到这个对象,所以直接注册到容器中方便注入

@Component
public class MyRememberMeServices extends PersistentTokenBasedRememberMeServices {
    public static final String REMEMBER_ME_KEY = "rememberMe";
    public static final List<String> TRUE_VALUES = List.of("true", "yes", "on", "1");

    public MyRememberMeServices(UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(UUID.randomUUID().toString(), userDetailsService, tokenRepository);
    }

    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        final String rememberMe = (String) request.getAttribute(REMEMBER_ME_KEY);
        if (rememberMe != null) {
            for (String trueValue : TRUE_VALUES) {
                if (trueValue.equalsIgnoreCase(rememberMe)) {
                    return true;
                }
            }
        }
        return super.rememberMeRequested(request, parameter);
    }
}

核心组件

MyLoginFilter(登陆过滤器)

  1. 构造方法的参数都可以从容器获取,所以这里也直接注册到容器自动构造
  2. 继承了UsernamePasswordAuthenticationFilter,后续我们要用它替换默认的UsernamePasswordAuthenticationFilter
  3. 构造函数中,指定了:
    1. 登陆成功和失败时的处理方法
    2. 记住我功能的组件
    3. 登陆使用的路径
  4. attemptAuthentication方法中规定了登陆流程:
    1. 如果Content-Type是Json,则从Body中获取请求参数,否则从Form表单中获取
    2. SessionAttribute中获取之前保存的验证码,和用户提供的验证码进行比对
    3. 把用户提供的rememberMe字段放到requestAttribute中,供后续MyRememberMeServices获取
    4. 结尾部分来自父类,照抄过来的。
@Component
public class MyLoginFilter extends UsernamePasswordAuthenticationFilter {
    private final ObjectMapper objectMapper = new ObjectMapper();

    public MyLoginFilter(AuthenticationManager authenticationManager,
                         MyAuthenticationHandler authenticationHandler,
                         MyRememberMeServices rememberMeServices) throws Exception {
        super(authenticationManager);
        setAuthenticationFailureHandler(authenticationHandler);
        setAuthenticationSuccessHandler(authenticationHandler);
        //rememberMe
        setRememberMeServices(rememberMeServices);
        //登陆使用的路径
        setFilterProcessesUrl("/sys/user/login");
    }

    private static boolean isContentTypeJson(HttpServletRequest request) {
        final String contentType = request.getContentType();
        return APPLICATION_JSON_CHARSET_UTF_8.equalsIgnoreCase(contentType) || MimeTypeUtils.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType);
    }
  
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        if (!HttpMethod.POST.name().equalsIgnoreCase(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = null;
        String password = null;
        String verifyCode = null;
        String rememberMe = null;
        if (isContentTypeJson(request)) {
            try {
                Map<String, String> map = objectMapper.readValue(request.getInputStream(), new TypeReference<>() {
                });
                username = map.get(getUsernameParameter());
                password = map.get(getPasswordParameter());
                verifyCode = map.get(VERIFY_CODE_KEY);
                rememberMe = map.get(MyRememberMeServices.REMEMBER_ME_KEY);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            username = obtainUsername(request);
            password = obtainPassword(request);
            verifyCode = request.getParameter(VERIFY_CODE_KEY);
            rememberMe = request.getParameter(MyRememberMeServices.REMEMBER_ME_KEY);
        }
        //校验验证码
        final String vc = (String) request.getSession().getAttribute(VERIFY_CODE_KEY);
        if (vc == null) {
            throw new BadCredentialsException("验证码不存在,请先获取验证码");
        } else if (verifyCode == null || "".equals(verifyCode)) {
            throw new BadCredentialsException("请输入验证码");
        } else if (!vc.equalsIgnoreCase(verifyCode)) {
            throw new BadCredentialsException("验证码错误");
        }

        //将 rememberMe 状态存入 attr中
        if (!ObjectUtils.isEmpty(rememberMe)) {
            request.setAttribute(MyRememberMeServices.REMEMBER_ME_KEY, rememberMe);
        }

        username = (username != null) ? username.trim() : "";
        password = (password != null) ? password : "";
        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

MySecurityConfig(核心配置)

  1. @Bean authenticationManager 提供了MyLoginFilter需要的AuthenticationManager
  2. @Bean daoAuthenticationProvider提供了MyRememberMeServices需要的PersistentTokenRepository,其中setCreateTableOnStartup方法在首次运行的时候需要解开注释让它自动建表
  3. @Bean securityFilterChain核心中的核心,2.x版本中对HttpSecurity http的配置都需要移动到这里,这里我们配置了:
    1. 路径配置,这里把接口文档和验证码的路径进行了放行,其他请求都需要认证。登陆请求并不受它影响不需要专门配置。
    2. 用自定义的MyLoginFilter替换了默认的UsernamePasswordAuthenticationFilter,注意原本的http.formLogin()不要再写了,否则将可以通过/login绕过验证码登陆
    3. 登出配置,指定了路径,和成功登出的处理方法
    4. csrf验证,注意这里比2.x版本需要多写一句.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
    5. 会话管理,配置了只允许一个端登陆,不需要配置sessionRegistry了,会自动注入,当然手动配置也是可以的,但是容器里不会自动创建了,需要手动传一个new SpringSessionBackedSessionRegistry<>(new RedisIndexedSessionRepository(redisTemplate)),其中redisTemplate需要为RedisTemplate<String,Object>
    6. 记住我功能,注意,这里和MyLoginFilter里的两次配置缺一不可。
    7. 权限不足时的处理
@Configuration
@RequiredArgsConstructor
public class MySecurityConfig {
    /**
     * 接口文档放行
     */
    public static final List<String> DOC_WHITE_LIST = List.of("/doc.html", "/webjars/**", "/v3/api-docs/**");
    /**
     * 测试接口放行
     */
    public static final List<String> TEST_WHITE_LIST = List.of("/test/**");
    /**
     * 验证码放行
     */
    public static final List<String> VERIFY_CODE_WHITE_LIST = List.of("/sys/verifyCode/**");

    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 允许抛出用户不存在的异常
     * @param myUserDetailsService myUserDetailsService
     * @return DaoAuthenticationProvider
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(MyUserDetailsServiceImpl myUserDetailsService) {
        final DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(myUserDetailsService);
        provider.setUserDetailsPasswordService(myUserDetailsService);
        provider.setHideUserNotFoundExceptions(false);
        return provider;
    }

    /**
     * 自定义RememberMe服务token持久化仓库
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(DataSource datasource) {
        final JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //设置数据源
        tokenRepository.setDataSource(datasource);
        //第一次启动的时候建表
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                   MyLoginFilter loginFilter,
                                                   MyAuthenticationHandler authenticationHandler,
                                                   MyRememberMeServices rememberMeServices
    ) throws Exception {
        //路径配置
        http.authorizeHttpRequests()
                .requestMatchers(HttpMethod.GET, DOC_WHITE_LIST.toArray(new String[0])).permitAll()
                .requestMatchers(HttpMethod.GET, VERIFY_CODE_WHITE_LIST.toArray(new String[0])).permitAll()
//                .requestMatchers(HttpMethod.GET, TEST_WHITE_LIST.toArray(new String[0])).permitAll()
                .anyRequest().authenticated()
        ;

        //登陆
        http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);

        //配置自定义登陆流程后需要关闭 否则可以使用原有登陆方式

        //登出
        http.logout().logoutUrl("/sys/user/logout").logoutSuccessHandler(authenticationHandler);

        //禁用 csrf
//        http.csrf().disable();

        //csrf验证 存储到Cookie中
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
        ;

        //会话管理
        http.sessionManagement()
                .maximumSessions(1)
                .expiredSessionStrategy(authenticationHandler)
        //引入redis-session依赖后已不再需要手动配置 sessionRegistry
//                .sessionRegistry(new SpringSessionBackedSessionRegistry<>(new RedisIndexedSessionRepository(RedisConfig.createRedisTemplate())))
        //禁止后登陆挤下线
//               .maxSessionsPreventsLogin(true)
        ;

        //rememberMe
        http.rememberMe().rememberMeServices(rememberMeServices);

        // 权限不足时的处理
        http.exceptionHandling()
                .accessDeniedHandler(authenticationHandler)
                .authenticationEntryPoint(authenticationHandler)
        ;

        return http.build();
    }
}

完成

有关Spring Security 6.0(spring boot 3.0) 下认证配置流程的更多相关文章

  1. ruby-on-rails - 独立 ruby​​ 脚本的配置文件 - 2

    我有一个在Linux服务器上运行的ruby​​脚本。它不使用rails或任何东西。它基本上是一个命令行ruby​​脚本,可以像这样传递参数:./ruby_script.rbarg1arg2如何将参数抽象到配置文件(例如yaml文件或其他文件)中?您能否举例说明如何做到这一点?提前谢谢你。 最佳答案 首先,您可以运行一个写入YAML配置文件的独立脚本:require"yaml"File.write("path_to_yaml_file",[arg1,arg2].to_yaml)然后,在您的应用中阅读它:require"yaml"arg

  2. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  3. Vscode+Cmake配置并运行opencv环境(Windows和Ubuntu大同小异) - 2

    之前在培训新生的时候,windows环境下配置opencv环境一直教的都是网上主流的vsstudio配置属性表,但是这个似乎对新生来说难度略高(虽然个人觉得完全是他们自己的问题),加之暑假之后对cmake实在是爱不释手,且这样配置确实十分简单(其实都不需要配置),故斗胆妄言vscode下配置CV之法。其实极为简单,图比较多所以很长。如果你看此文还配不好,你应该思考一下是不是自己的问题。闲话少说,直接开始。0.CMkae简介有的人到大二了都不知道cmake是什么,我不说是谁。CMake是一个开源免费并且跨平台的构建工具,可以用简单的语句来描述所有平台的编译过程。它能够根据当前所在平台输出对应的m

  4. 神州数码无线产品(AC+AP)配置 - 2

    注意:本文主要掌握DCN自研无线产品的基本配置方法和注意事项,能够进行一般的项目实施、调试与运维AP基本配置命令AP登录用户名和密码均为:adminAP默认IP地址为:192.168.1.10AP默认情况下DHCP开启AP静态地址配置:setmanagementstatic-ip192.168.10.1AP开启/关闭DHCP功能:setmanagementdhcp-statusup/downAP设置默认网关:setstatic-ip-routegeteway192.168.10.254查看AP基本信息:getsystemgetmanagementgetmanaged-apgetrouteAP配

  5. hadoop安装之保姆级教程(二)之YARN的配置 - 2

    1.1.1 YARN的介绍 为克服Hadoop1.0中HDFS和MapReduce存在的各种问题⽽提出的,针对Hadoop1.0中的MapReduce在扩展性和多框架⽀持⽅⾯的不⾜,提出了全新的资源管理框架YARN. ApacheYARN(YetanotherResourceNegotiator的缩写)是Hadoop集群的资源管理系统,负责为计算程序提供服务器计算资源,相当于⼀个分布式的操作系统平台,⽽MapReduce等计算程序则相当于运⾏于操作系统之上的应⽤程序。 YARN被引⼊Hadoop2,最初是为了改善MapReduce的实现,但是因为具有⾜够的通⽤性,同样可以⽀持其他的分布式计算模

  6. Ruby 默认将 IRB 配置为 Pretty_Inspect - 2

    我是ruby​​的新手,正在配置IRB。我喜欢pretty-print(需要'pp'),但总是输入pp来漂亮地打印它似乎很麻烦。我想做的是默认情况下让它漂亮地打印出来,所以如果我有一个var,比如说,'myvar',然后键入myvar,它会自动调用pretty_inspect而不是常规检查。我从哪里开始?理想情况下,我将能够向我的.irbrc文件添加一个自动调用的方法。有什么想法吗?谢谢! 最佳答案 irb中默认pretty-print对象正是hirb被迫去做。Theseposts解释hirb如何将几乎所有内容转换为ascii表。虽

  7. ruby - 是否可以将 IRB 提示配置为动态更改? - 2

    我想在IRB中浏览文件系统并让提示更改以反射(reflect)当前工作目录,但我不知道如何在每个命令后进行提示更新。最终,我想在日常工作中更多地使用IRB,让bash溜走。我在我的.irbrc中试过这个:require'fileutils'includeFileUtilsIRB.conf[:PROMPT][:CUSTOM]={:PROMPT_N=>"\e[1m:\e[m",:PROMPT_I=>"\e[1m#{pwd}>\e[m",:PROMPT_S=>"FOO",:PROMPT_C=>"\e[1m#{pwd}>\e[m",:RETURN=>""}IRB.conf[:PROMPT_MO

  8. ruby - 如何配置 Ruby Mechanize 代理以通过 Charles Web 代理工作? - 2

    我正在使用Ruby/Mechanize编写一个“自动填写表格”应用程序。它几乎可以工作。我可以使用精彩CharlesWeb代理以查看服务器和我的Firefox浏览器之间的交换。现在我想使用Charles查看服务器和我的应用程序之间的交换。Charles在端口8888上代理。假设服务器位于https://my.host.com。.一件不起作用的事情是:@agent||=Mechanize.newdo|agent|agent.set_proxy("my.host.com",8888)end这会导致Net::HTTP::Persistent::Error:...lib/net/http/pe

  9. ruby - token 认证 - 2

    简单代码require'net/http'url=URI.parse('getjson/otherdatahere[link]')req=Net::HTTP::Get.new(url.to_s)res=Net::HTTP.start(url.host,url.port){|http|http.request(req)}putsres.body只是想知道如何在phpcURL中放置身份验证token,我是这样做的    curl_setopt($ch,CURLOPT_HTTPHEADER,array('Authorization:Bearerxxx'));//Bearertokenfora

  10. ruby - HTTParty 摘要认证 - 2

    谁能提供一个使用HTTParty和digestauth的例子?我在网上找不到例子,希望有人能提供一些帮助。谢谢。 最佳答案 您可以在定义类时使用digest_auth方法设置用户名和密码classFooincludeHTTPartydigest_auth'username','password'end 关于ruby-HTTParty摘要认证,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questi

随机推荐