Spring Securtiy 认证流程(源码分析)
当用 Spring Security 框架进行认证时,你可能会遇到这样的问题:
你输入的用户名或密码不管是空还是错误,它的错误信息都是 Bad credentials。
那么如果你想根据不同的情况给出相应的错误提示该怎么办呢?
这个时候我们只有了解 Spring Securiy 认证的流程才能知道如何修改代码。
好啦,来看下面的例子,大部分人的 WebSecurityConfig 的 configure 代码都类似于下:
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin().loginPage("/signin")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/signin")
.and()
.csrf().disable();
}
相信以上代码大家都知道什么意思:任何请求信息都允许,也就是不需要身份认证。
登录页面请求为 /signin,用户名和密码参数的name属性分别是 username,password。登录页面 form 的 action 请求为 /signin。
当然这个 action 不必和登录页面请求一样。最后的那个是禁止跨站请求伪造。
这段代码和登录认证联系较大的应该是从 loginPage() 到 loginProcessingUrl() 里的方法。
咱先从 loginPage 看起,鼠标左键拖动覆盖 loginPage,然后右键 Open Declaration 就进入到了 FormLoginConfigurer 类。
这个类里值得注意的方法有两个:构造方法和 loginPage 方法。
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
} public FormLoginConfigurer<H> loginPage(String loginPage) {
return super.loginPage(loginPage);
}
构造方法中使用了一个用户名密码认证过滤器类,这一看就和认证有关系。
loginPage 方法大家可以自行按照这个步骤查看,现在直接看 UsernamePasswordAuthenticationFilter 类。
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
} public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
这只是其中一部分代码,其他的可以自己看。该类中定义的两个字符串和构造方法定义了默认的登录方式。
登录 action 请求为以 POST 方式的 /login,用户名及密码分别以 username,password 属性值获取。
该类的父类的父类 GenericFilterBean 实现了 InitializingBean 接口,也就是会初始化为一个 Bean。
当看到 attemptAuthentication 时,就知道他是认证的方法啦。
这里咱直接看到 new UsernamePasswordAuthenticationToken(username, password);
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
从这里可以知道,它把用户名和密码分别存在了 principal,credentials 里。
现在我们只需要记住登录信息存在了 authRequest 里。现在来看下setDetails,虽然我不感兴趣。
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
它调用了一个 buildDetails 方法,实际上是调用的:(追根溯源可以看到)
/**
* Records the remote address and will also set the session Id if a session already
* exists (it won't create one).
*
* @param request that the authentication request was received from
*/
public WebAuthenticationDetails(HttpServletRequest request) {
this.remoteAddress = request.getRemoteAddr(); HttpSession session = request.getSession(false);
this.sessionId = (session != null) ? session.getId() : null;
}
从源码注释可以看到,它是记录远程地址并且会设置一个会话 ID,这里我们不管它了。
直接看这一句:return this.getAuthenticationManager().authenticate(authRequest);
它调用的是一个实现了 AuthenticationManager 接口的类的 authenticate 方法。
从源码中我们找不到它用的是哪个实现类,网上说是 ProviderManager 类,我们来看一下该类。
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
} if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
} try {
result = provider.authenticate(authentication); if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
} if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
} if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
} // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
} // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
} throw lastException;
}
}
这里我只给出该类的声明和 authenticate 方法,从类的声明可以看出来它也会初始化为一个 Bean,咱找不到很正常对吧。
authenticate 方法会遍历所有的 AuthenticationProvider ,然后调用 provider 的 authenticate 方法。
如果认证结果不为空的话将会保存到 result 中,并且擦除认证信息再返回 result。
为空的话一般是没有提供 AuthenticationProvider,会报 ProviderNotFoundException 错误。
现在我们来看下 provider 的 authenticate 方法。
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new CustomAuthenticationProvider();
provider.setMessageSource(messageSource);
provider.setUserDetailsService(userService);
provider.setPasswordEncoder(new BCryptPasswordEncoder());
return provider;
}
这个是我写的一个 AuthenticationProvider,只不过我重写了一个类继承了 DaoAuthenticationProvider。
这里我们来看 DaoAuthenticationProvider 类:(这个类里面并没有发现 authenticate 方法,那先从它的父类找)
父类是 AbstractUserDetailsAuthenticationProvider,它也实现了 InitializingBean 接口,也是初始化为一个 Bean。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported")); // Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName(); boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username); if (user == null) {
cacheWasUsed = false; try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
} Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
} try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
} postAuthenticationChecks.check(user); if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
} Object principalToReturn = user; if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
} return createSuccessAuthentication(principalToReturn, authentication, user);
}
在这段代码中可以知道:如果 authentication.getPrincipal() 为空的话,username 将会为 NONE_PROVIDED。
不为空的话将会得到 authentication.getPrincipal(),也就是用户名,只是这种类型不是 String 类型,但可以强制转换。
代码中是 authentication.getName(),这种和上面基本一样,只不过该类型是 String 类型的。
然后定义一个 user,先尝试从缓存中获取 user,没获取到的话就通过 retrieveUser 获取。
该类中 retrieveUser 是一个抽象方法,我们现在来看 DaoAuthenticationProvider 类里的方法。
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
从代码中可以看到是通过我们之前写的 UserDetailsService 方法获取用户。
接下来我们看后面的代码,这部分异常代码我们等会再看。
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
这两句代码是对用户进行检查的,第一行代码调用的其实是这部分的:
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
logger.debug("User account is locked"); throw new LockedException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"));
} if (!user.isEnabled()) {
logger.debug("User account is disabled"); throw new DisabledException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.disabled",
"User is disabled"));
} if (!user.isAccountNonExpired()) {
logger.debug("User account is expired"); throw new AccountExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.expired",
"User account has expired"));
}
}
}
可以看到并不是检查密码的,只是对用户状态进行检查。那么我们不管它了,看下一行代码:
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
} String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
这里有个获取密码的操作:authentication.getCredentials()。
然后如果密码不为空的话就通过 passwordEncoder.matches(presentedPassword, userDetails.getPassword() 检查是否匹配。
如果匹配成功的话,嗯,这部分结束了,我们回到 AbstractUserDetailsAuthenticationProvider 类里的 authenticate 方法。
return createSuccessAuthentication(principalToReturn, authentication, user);
它会返回一个创建成功认证方法的返回值。这里我们就不管了。
现在我们先回到AbstractUserDetailsAuthenticationProvider 类的错误处理上:
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
这个是用户找不到引起的错误,我们看下 messages.getMessage():
public String getMessage(String code, String defaultMessage) {
String msg = this.messageSource.getMessage(code, null, defaultMessage, getDefaultLocale());
return (msg != null ? msg : "");
}
再来看下这个里面的 getMessage():
它是一个接口类里的方法:根据 code 返回 messageSource 里的字符串,如果不存在这个 code,就返回 defaultMessage。
既然是个接口类,那我们看下它的实现类,回到 messageSource,查看一下它:
public class SpringSecurityMessageSource extends ResourceBundleMessageSource {
// ~ Constructors
// =================================================================================================== public SpringSecurityMessageSource() {
setBasename("org.springframework.security.messages");
} // ~ Methods
// ======================================================================================================== public static MessageSourceAccessor getAccessor() {
return new MessageSourceAccessor(new SpringSecurityMessageSource());
}
}
原来是从这个路径里找数据源。
其他的错误处理也是一样,这里就省略了。那我们如何获取错误信息呢?
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin().loginPage("/signin")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/signin")
.failureHandler(authenticationFailureHandler)
.and()
.csrf().disable();
看到那个 failureHandler 没,这个是登录失败处理器,这里加上只是看一下里面源码:AbstractAuthenticationFilterConfigurer
/**
* Specifies the {@link AuthenticationFailureHandler} to use when authentication
* fails. The default is redirecting to "/login?error" using
* {@link SimpleUrlAuthenticationFailureHandler}
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} to use
* when authentication fails.
* @return the {@link FormLoginConfigurer} for additional customization
*/
public final T failureHandler(
AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return getSelf();
}
从注释中可以看出默认的失败处理器是 SimpleUrlAuthenticationFailureHandler:
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException { if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error"); response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
else {
saveException(request, exception); if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl); request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}
else {
logger.debug("Redirecting to " + defaultFailureUrl);
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
因为默认的 defaultFailureUrl 为 /login?error,从 AbstractAuthenticationFilterConfigurer 类里可以看出来。
登录失败后,会调用 saveException(request, exception); 保存错误信息。
protected final void saveException(HttpServletRequest request,
AuthenticationException exception) {
if (forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
else {
HttpSession session = request.getSession(false); if (session != null || allowSessionCreation) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
exception);
}
}
}
由于该类中 forwardToDestination 为 false,它将执行 else 里的语句。
将错误信息保存到会话的 WebAttributes.AUTHENTICATION_EXCEPTION 属性中:
public static final String AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";
所有我们可以通过会话的这个属性来获取错误信息。(thymeleaf)
(注意:signin.html 不能放在 static 目录下,不然获取不到错误信息。)
<p th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>
好啦,都介绍完了,可以看下我的 CustomAuthenticationProvider:
package security.config; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert; public class CustomAuthenticationProvider extends DaoAuthenticationProvider { @Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// TODO Auto-generated method stub String presentedPassword = authentication.getCredentials().toString();
if (!getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage(
"UNameOrPwdIsError","Username or Password is not correct"));
}
} @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// TODO Auto-generated method stub
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported")); if("".equals(authentication.getPrincipal())) {
throw new BadCredentialsException(messages.getMessage(
"UsernameIsNull","Username cannot be empty"));
}
if("".equals(authentication.getCredentials())) {
throw new BadCredentialsException(messages.getMessage(
"PasswordIsNull","Password cannot be empty"));
} String username = (String) authentication.getPrincipal();
boolean cacheWasUsed = true;
UserDetails user = this.getUserCache().getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"UNameOrPwdIsError","Username or Password is not correct"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
getPreAuthenticationChecks().check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
getPreAuthenticationChecks().check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
} getPostAuthenticationChecks().check(user); if (!cacheWasUsed) {
this.getUserCache().putUserInCache(user);
} Object principalToReturn = user; if (isForcePrincipalAsString()) {
principalToReturn = user.getUsername();
} return createSuccessAuthentication(principalToReturn, authentication, user);
} }
这里值得注意的是 "".equals(authentication.getPrincipal()),"".equals(authentication.getCredentials())
因为如果按照那个 AbstractUserDetailsAuthenticationProvider 类来写的话,发现这一步永不为 null。
我通过加入代码 System.out.println(username); 才知道的,应该是个坑吧。
项目代码可供大家参考:
链接:https://pan.baidu.com/s/13fc6P9NV49aRRBctr3MjNQ
提取码:4qgu
Spring Securtiy 认证流程(源码分析)的更多相关文章
- Django rest framework 的认证流程(源码分析)
一.基本流程举例: urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^users/', views.HostView.as_view() ...
- Django-restframework 源码之认证组件源码分析
Django-restframework 源码之认证组件源码分析 一 前言 之前在 Django-restframework 的流程分析博客中,把最重要的关于认证.权限和频率的方法找到了.该方法是 A ...
- spring boot 2.0 源码分析(一)
在学习spring boot 2.0源码之前,我们先利用spring initializr快速地创建一个基本的简单的示例: 1.先从创建示例中的main函数开始读起: package com.exam ...
- Spring JPA实现逻辑源码分析总结
1.SharedEntityManagerCreator: entitymanager的创建入口 该类被EntityManagerBeanDefinitionRegistrarPostProcesso ...
- spring boot 2.0 源码分析(四)
在上一章的源码分析里,我们知道了spring boot 2.0中的环境是如何区分普通环境和web环境的,以及如何准备运行时环境和应用上下文的,今天我们继续分析一下run函数接下来又做了那些事情.先把r ...
- Spring中Bean命名源码分析
Spring中Bean命名源码分析 一.案例代码 首先是demo的整体结构 其次是各个部分的代码,代码本身比较简单,不是我们关注的重点 配置类 /** * @Author Helius * @Crea ...
- Spring Cloud 学习 之 Spring Cloud Eureka(源码分析)
Spring Cloud 学习 之 Spring Cloud Eureka(源码分析) Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 ...
- Spring Boot 自动配置 源码分析
Spring Boot 最大的特点(亮点)就是自动配置 AutoConfiguration 下面,先说一下 @EnableAutoConfiguration ,然后再看源代码,到底自动配置是怎么配置的 ...
- spring boot 2.0 源码分析(二)
在上一章学习了spring boot 2.0启动的大概流程以后,今天我们来深挖一下SpringApplication实例变量的run函数. 先把这段run函数的代码贴出来: /** * Run the ...
随机推荐
- MYSQL结构修改
mysql改表结构主要是5大操作 ADD 添加字段 MODIFY 修改字段类型 CHANGE 修改字段名(也可以修改字段名) DROP 删除字段 RENAME 修改表名 ADD添加新字段:(新字段默认 ...
- 「刷题」可怜与STS
又是一道假期望,我们发现一共有$ C_{2n}^m $种情况. 而$ \frac{(2n)!}{m!(2n-m)!}=C_{2n}^m $ 其实结果就是各个情况总伤害. 1.10分算法,爆搜10分. ...
- Python文字转换语音,让你的文字会「说话」,抠脚大汉秒变撒娇萌妹
作者 | pk 哥 来源公众号 | Python知识圈(ID:PythonCircle) APP 也有文字转换为语音的功能,虽然听起来很别扭,但是基本能解决长辈们看不清文字或者眼睛疲劳,通过文字转换为 ...
- AndroidOS体系结构
首先上图一张 对照着图,我们再来看Android 系统的体系结构就爽多了.我们从底层向上进行分析. 一.Linux 内核层 Linux Kernel 基于linux2.6.其核心系统服务如安全性.内存 ...
- 正睿OI集训游记
什么嘛....就是去被虐的... 反正就是难受就是了.各种神仙知识点,神仙题目,各式各样的仙人掌..... 但是还是学会了不少东西...... 应该是OI生涯最后一次集训了吧.... 这次的感言还是好 ...
- 零基础Linux入门之《Linux就该这么学》
本书是由全国多名红帽架构师(RHCA)基于最新Linux系统共同编写的高质量Linux技术自学教程,极其适合用于Linux技术入门教程或讲课辅助教材,目前是国内最值得去读的Linux教材,也是最有价值 ...
- Fiddler 原理及iPhone的配置
原理: 首先Fiddler运行在自己的PC上,Fiddler运行的时候会在PC的8888端口开启一个代理服务,这个服务实际上是一个HTTP/HTTPS的代理. 确保手机和PC在同一个局域网内,我们可以 ...
- len、is、==、可变于不可变类型
a="asdfghjkl;'iuygb" b="小米" c=['a','b','c'] d= {'name':1,'age':24} # len统计字符或元素的 ...
- Jumpserver v2.0.0 使用说明
官方文档:http://www.jumpserver.org/ — 登录脚本 — 1.1 使用paramiko原生ssh协议登录后端主机(原来版本使用pexpect模拟登录) 1.2 新增使用别名或备 ...
- nyoj 33-蛇形填数 (循环,模拟)
33-蛇形填数 内存限制:64MB 时间限制:3000ms Special Judge: No accepted:15 submit:38 题目描述: 在n*n方陈里填入1,2,...,n*n,要求填 ...