2021-01-12

本文对Spring-Security源码进行解读。先介绍使用组合键查找UsernamePasswordAuthenticationFilter过滤器处理用户认证逻辑,包括默认登录请求url、获取用户名和密码方式等。还阐述了AuthenticationManager管理AuthenticationProvider,通过ProviderManager实现类进行认证处理的流程。
  • Spring-Security源码解读:

    1.使用ctrl+shift+n组合键查找UsernamePasswordAuthenticationFilter过滤器,该过滤器是用来处理用户认证逻辑的,进入后如图:

  •  

  • (1)可以看到它默认的登录请求url是"/login",并且只允许POST方式的请求

    (2)obtainUsername()方法点进去发现它默认是根据参数名为"username"和"password"来获取用户名和密码的

    (3)通过构造方法实例化一个UsernamePasswordAuthenticationToken对象,此时调用的是UsernamePasswordAuthenticationToken的两个参数的构造函数,如图:

  • 其中super(null)调用的是父类的构造方法,传入的是权限集合,因为目前还没有认证通过,所以不知道有什么权限信息,这里设置为null,然后将用户名和密码分别赋值给principal和credentials,同样因为此时还未进行身份认证,所以setAuthenticated(false)

    (4)setDetails(request, authRequest)是将当前的请求信息设置到UsernamePasswordAuthenticationToken中

    (5)通过调用getAuthenticationManager()来获取AuthenticationManager,通过调用它的authenticate方法来查找支持该token(UsernamePasswordAuthenticationToken)认证方式的provider,然后调用该provider的authenticate方法进行认证

    2.AuthenticationManager是用来管理AuthenticationProvider的接口,通过查找后进入,然后使用ctrl+H组合键查看它的继承关系,找到ProviderManager实现类,它实现了AuthenticationManager接口,查看它的authenticate方法,它里面有段这样的代码:

  • for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
        ...
        try {
                result = provider.authenticate(authentication);
        ...
        }
    }

    通过for循环遍历AuthenticationProvider对象的集合,找到支持当前认证方式的AuthenticationProvider,找到之后调用该AuthenticationProvider的authenticate方法进行认证处理:

  • result = provider.authenticate(authentication);
    3.AuthenticationProvider接口,就是进行身份认证的接口,它里面有两个方法:authenticate认证方法和supports是否支持某种类型token的方法,通过ctrl+h查看继承关系,找到AbstractUserDetailsAuthenticationProvider抽象类,它实现了AuthenticationProvider接口,它的supports方法如下:
    
        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
            }
    说明它是支持UsernamePasswordAuthenticationToken类型的AuthenticationProvider
    
    再看它的authenticate认证方法,其中有一段这样的代码:
    
    boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
     
        if (user == null) {
            cacheWasUsed = false;
     
            try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
        ...
        }
    如果从缓存中没有获取到UserDetails,那么它调用retrieveUser方法来获取用户信息UserDetails,这里的retrieveUser是抽象方法,等一会我们看它的子类实现。
    
    用户信息UserDetails是个接口,我们进入查看,它包含以下6个接口方法:
    
    Collection<? extends GrantedAuthority> getAuthorities();//获取权限集合
    String getPassword();  //获取密码
    String getUsername();   //获取用户名
    boolean isAccountNonExpired(); //账户未过期
    boolean isAccountNonLocked();   //账户未锁定
    boolean isCredentialsNonExpired(); //密码未过期
    boolean isEnabled();    //账户可用
    查看它的继承关系发现User类实现了该接口,并实现了该接口的所有方法
    
    接着AbstractUserDetailsAuthenticationProvider往下看,找到下面的代码:
    
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
    preAuthenticationChecks预检查,在最下面的内部类DefaultPreAuthenticationChecks中可以看到,它会检查上面提到的三个boolean方法,即检查账户未锁定、账户可用、账户未过期,如果上面的方法只要有一个返回false,就会抛出异常,那么认证就会失败。
    
    additionalAuthenticationChecks是附加检查,是个抽象方法,等下看子类的具体实现。
    
    下面还有个postAuthenticationChecks.check(user)后检查,在最下面的DefaultPostAuthenticationChecks内部类中可以看到,它会检查密码未过期,如果为false就会抛出异常
    
    如果上面的检查都通过并且没有异常,表示认证通过,会调用下面的方法:
    
    createSuccessAuthentication(principalToReturn, authentication, user);
    跟进发现此时通过构造方法实例化对象UsernamePasswordAuthenticationToken时,调用的是三个参数的构造方法:
    
        public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }
    此时会调用父类的构造方法设置权限信息,并调用父类的setAuthenticated(true)方法,到这里就表示认证通过了。
    
    下面我们看看AbstractUserDetailsAuthenticationProvider的子类,同ctrl+h可查看继承关系,找到DaoAuthenticationProvider
    
    4.DaoAuthenticationProvider类
    
    (1)查看additionalAuthenticationChecks附加检查方法,它主要是检查用户密码的正确性,如果密码为空或者错误都会抛出异常
    
    (2)获取用户信息UserDetails的retrieveUser方法,主要看下面这段代码:
    
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    它是调用了getUserDetailsService先获取到UserDetailsService对象,通过调用UserDetailsService对象的loadUserByUsername方法获取用户信息UserDetails
    
    找到UserDetailsService,发现它是一个接口,查看继承关系,有很多实现,都是spring-security提供的实现类,并不满足我们的需要,我们想自己制定获取用户信息的逻辑,所以我们可以实现这个接口。比如从我们的数据库中查找用户信息
    
    5.SecurityContextPersistenceFilter过滤器
    
    那么用户认证成功之后,又是怎么保存认证信息的呢,在下一次请求过来是如何判断该用户是否已经认证了呢?
    
    请求进来时会经过SecurityContextPersistenceFilter过滤器,进入SecurityContextPersistenceFilter过滤器并找到以下代码:
    
    SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
    从session中获取SecurityContext对象,如果没有就实例化一个SecurityContext对象
    
    SecurityContextHolder.setContext(contextBeforeChainExecution);
    将SecurityContext对象设置到SecurityContextHolder中
    
    chain.doFilter(holder.getRequest(), holder.getResponse());
    表示放行,执行下一个过滤器
    
    执行完后面的过滤并经过servlet处理之后,响应给浏览器之前再次经过此过滤器。查看以下代码:
    
    SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
    SecurityContextHolder.clearContext();
    this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
    通过SecurityContextHolder获取SecurityContext对象,然后清除SecurityContext,最后将获取的SecurityContext对象放入session中
    
    其中SecurityContextHolder是与ThreadLocal绑定的,即本线程内所有的方法都可以获得SecurityContext对象,而SecurityContext对象中包含了Authentication对象,即用户的认证信息,spring-security判断用户是否认证主要是根据SecurityContext中的Authentication对象来判断。Authentication对象的详细信息如图:

    最后整个过程的流程大致如下图:

  •  

<think>好的,我现在需要解决用户的问题,关于如何将SQLite数据库中的日期字段从'2021/1/12'格式转换为'2021-01-12'。首先,我需要回顾用户之前的提问和我的回答,确保解决方案的一致性。 用户之前提到日期显示为1899-12-30的问题,主要是因为存储格式不正确或者转换错误。现在的问题是日期格式需要统一为ISO8601标准,即YYYY-MM-DD。根据引用[3],SQLite提供了日期函数如strftime来处理格式转换,这可能对当前问题有帮助。 首先,我需要确认用户的数据存储类型。根据之前的回答,SQLite使用类型亲缘性(引用[5]),日期可能存储为TEXT。如果字段是TEXT类型,那么直接处理字符串即可。但用户现在的日期格式是'2021/1/12',即月份和日为单数字,需要转换为双数字格式。 接下来,我需要考虑如何分割原始字符串并重新组合。用户给出的示例日期是'2021/1/12',可能还有其他类似的情况,比如'2021/12/1',所以需要处理不同部分的长度。可以使用substr函数来提取年、月、日,然后使用printf或CASE语句来补零。 引用[2]中提到的datetime函数可能不适用,因为用户的数据是文本格式,需要先转换为正确的日期格式。或者,可以先将字符串分割,再重新格式化为标准格式。 另外,要注意原始数据中是否存在不同的分隔符或格式,例如是否有日期是'2021-1-12'或者其他变体。需要确保SQL语句能够处理所有情况。 步骤可能包括: 1. 使用substr提取年、月、日部分。 2. 检查月和日的长度,如果为1位,前面补零。 3. 组合成YYYY-MM-DD格式。 4. 执行UPDATE语句来修改表中的数据。 需要注意的是,如果字段中存在其他格式的日期,如'2022/10/5',也需要正确处理。可能需要更灵活的处理方式,例如使用INSTR查找分隔符的位置,或者使用REPLACE函数替换斜杠为横杠,但这样可能无法处理单数字月份和日期的情况。 因此,最佳方法可能是分割字符串,并分别处理月、日的补零。例如,使用SUBSTR和INSTR来找到斜杠的位置,从而确定各部分的位置。然后,使用printf('%02d', month)来补零。 不过,SQLite的字符串处理函数有限,可能需要嵌套多个SUBSTR和INSTR来定位。例如: UPDATE表 SET date_field = printf('%04d-%02d-%02d', SUBSTR(date_field, 1, 4), CASE WHEN LENGTH(SUBSTR(date_field, INSTR(date_field, '/')+1, INSTR(SUBSTR(date_field, INSTR(date_field, '/')+1), '/')-1)) = 1 THEN '0' || SUBSTR(...) ELSE SUBSTR(...) END, ...); 这可能会比较复杂,需要多次定位斜杠的位置。或者,如果日期格式统一为YYYY/MM/DD或YYYY/M/D,可以分三次分割。 另一种方法是使用REPLACE将斜杠替换为横杠,然后使用strftime重新格式化,但需要确保月和日已经是两位数。不过,如果原数据中的月份和日是单数字,直接替换斜杠并不能补零,所以必须处理这部分。 例如,将'2021/1/12'转换为'2021-01-12',需要将月份补零。可以先用SUBSTR和INSTR分割各部分,然后分别处理月和日。 可能需要这样的步骤: - 第一个斜杠的位置:pos1 = INSTR(date_field, '/') - 年份:SUBSTR(date_field, 1, pos1-1) - 剩余部分:SUBSTR(date_field, pos1+1) - 第二个斜杠的位置:pos2 = INSTR(剩余部分, '/') - 月份:SUBSTR(剩余部分, 1, pos2-1) - 日:SUBSTR(剩余部分, pos2+1) 然后,对月份和日分别补零,使用printf('%02d', month)或者CASE WHEN LENGTH(month)=1 THEN '0' || month ELSE month END。 但SQLite中可能没有INSTR函数,或者不同版本的SQLite支持不同。根据引用[3],SQLite支持strftime,但字符串处理函数可能有限。例如,INSTR可能不可用,需要查找替代方法。 查阅SQLite的字符串函数,发现SQLite有instr函数,语法为instr(string, substring),返回子字符串的位置。例如,INSTR('2021/1/12', '/') 返回5。因此,可以定位第一个斜杠的位置,然后提取年份,剩下的部分再次分割月份和日。 例如: UPDATE expert SET date_field = printf( '%s-%02d-%02d', SUBSTR(date_field, 1, INSTR(date_field, '/') - 1), SUBSTR( SUBSTR(date_field, INSTR(date_field, '/') + 1), 1, INSTR(SUBSTR(date_field, INSTR(date_field, '/') + 1), '/') - 1 ), SUBSTR( SUBSTR(date_field, INSTR(date_field, '/') + 1), INSTR(SUBSTR(date_field, INSTR(date_field, '/') + 1), '/') + 1 ) ) WHERE date_field LIKE '%/%/%'; 但这里的问题是如何处理月份和日的数值转换,因为SUBSTR返回的是字符串,需要用CAST转为整数,然后再用printf的%02d格式来补零。例如: SUBSTR部分得到月份为'1',然后CAST('1' AS INTEGER)得到1,printf('%02d', 1)得到'01'。因此,可能需要将SUBSTR的部分转换为整数,再格式化。 因此,完整的UPDATE语句可能如下: UPDATE expert SET date_field = printf( '%s-%02d-%02d', SUBSTR(date_field, 1, INSTR(date_field, '/') - 1), CAST(SUBSTR( SUBSTR(date_field, INSTR(date_field, '/') + 1), 1, INSTR(SUBSTR(date_field, INSTR(date_field, '/') + 1), '/') - 1 ) AS INTEGER), CAST(SUBSTR( SUBSTR(date_field, INSTR(date_field, '/') + 1), INSTR(SUBSTR(date_field, INSTR(date_field, '/') + 1), '/') + 1 ) AS INTEGER) ) WHERE date_field LIKE '%/%/%'; 这个语句的作用是,将日期字段中的'YYYY/MM/DD'或'YYYY/M/D'转换为'YYYY-MM-DD',并补零月份和日。 但是,需要测试该语句是否适用于不同的情况,例如: - '2021/1/12' → '2021-01-12' - '2022/10/5' → '2022-10-05' - '2023/3/9' → '2023-03-09' 另外,需要确保WHERE子句正确筛选出需要转换的日期格式,避免误改其他格式的数据。例如,使用LIKE '%/%/%'来匹配包含两个斜杠的日期。 另外,需要考虑原始数据是否有其他分隔符或格式,例如是否有可能存在三个斜杠的情况,或者年份不是四位的情况,但用户的问题中明确给出的是'2021/1/12'转换为'2021-01-12',所以假设日期格式为YYYY/M/D或YYYY/MM/DD,其中月和日可能为1或2位数字。 此外,执行UPDATE之前,最好先备份数据,或者在测试环境中验证语句的正确性。 总结步骤: 1. 使用INSTR定位斜杠位置,分割年、月、日。 2. 将月和日转换为整数,再使用printf补零为两位。 3. 组合成YYYY-MM-DD格式。 4. 更新符合条件的记录。 需要注意的是,SQLite的日期函数如strftime可能无法直接解析非标准格式,因此需要手动处理字符串。这种方法适用于已知格式的转换,但可能不适用于混合格式的情况。 最后,根据用户提供的引用[3],使用strftime函数可能更简洁,但需要先将字符串转换为合法的日期格式。例如,如果原始日期是'2021/1/12',可以尝试使用substr和替换来构造一个能被SQLite识别的格式,然后使用strftime重新格式化。例如: UPDATE expert SET date_field = strftime('%Y-%m-%d', substr(date_field, 1, 4) || '-' || substr('0' || substr(date_field, 6, instr(substr(date_field, 6), '/') - 1), -2) || '-' || substr('0' || substr(date_field, instr(date_field, '/', 1, 2) + 1), -2) ) WHERE date_field LIKE '%/%/%'; 这里,假设第一个斜杠在位置5(年份后),第二个斜杠在月份后。但可能需要更复杂的处理。例如,将'2021/1/12'转换为'2021-01-12',需要提取月份和日并补零。这种方法可能更高效,但需要确保字符串分割正确。 总的来说,用户的问题需要将'YYYY/M/D'转换为'YYYY-MM-DD',使用字符串分割和格式化补零是最直接的方法。需要确保SQL语句正确处理不同长度的月份和日,并在转换后进行验证。</think>#### 一、问题分析与解决方案选择 该需求属于**非标准日期格式标准化**问题,核心是通过SQL字符串操作将`YYYY/M/D`格式转换为`YYYY-MM-DD`。根据SQLite的字符串处理函数特性,推荐使用`SUBSTR`+`INSTR`组合定位分隔符,配合`printf`补零实现[^3][^5]。 #### 二、完整转换SQL语句 ```sql -- 单次操作完成格式转换 UPDATE expert SET date_field = printf('%04d-%02d-%02d', CAST(SUBSTR(date_field, 1, pos1-1) AS INTEGER), -- 年 CAST(SUBSTR(remain_part1, 1, pos2-1) AS INTEGER), -- 月 CAST(SUBSTR(remain_part1, pos2+1) AS INTEGER) -- 日 ) FROM ( SELECT id, INSTR(date_field, '/') AS pos1, -- 第一个斜杠位置 SUBSTR(date_field, INSTR(date_field, '/')+1) AS remain_part1, -- 剩余部分 INSTR(SUBSTR(date_field, INSTR(date_field, '/')+1), '/') AS pos2 -- 第二个斜杠位置 FROM expert WHERE date_field LIKE '%/%/%' -- 仅处理含两个斜杠的日期 ) AS subquery WHERE expert.id = subquery.id; ``` #### 三、操作分解说明 1. **定位分隔符位置** - `INSTR(date_field, '/')`获取第一个斜杠位置 - 对剩余字符串再次执行`INSTR`获取第二个斜杠位置 2. **分段提取日期组件** ```sql SUBSTR(date_field, 1, pos1-1) -- 提取年份(YYYY) SUBSTR(remain_part1, 1, pos2-1) -- 提取月份(M) SUBSTR(remain_part1, pos2+1) -- 提取日(D) ``` 3. **标准化数字格式** 通过`CAST(... AS INTEGER)`清除前导零(若存在),再使用`printf`强制补零: ```sql printf('%04d-%02d-%02d', year, month, day) -- 输出YYYY-MM-DD格式 ``` #### 四、验证与异常处理 ```sql -- 检查转换结果 SELECT date_field FROM expert WHERE date_field NOT GLOB '????-??-??*'; -- 查找未转换成功的记录 -- 处理特殊格式(如单年份) UPDATE expert SET date_field = NULL WHERE date_field NOT LIKE '%/%/%'; -- 非目标格式设为空 ``` #### 五、性能优化建议 1. **添加临时索引** ```sql CREATE INDEX tmp_idx ON expert(date_field); -- 执行转换操作 DROP INDEX tmp_idx; ``` 2. **分批次更新** ```sql UPDATE expert SET date_field = (...) WHERE id BETWEEN 1 AND 10000; -- 分片更新 ``` #### 六、预防措施 ```sql -- 创建新表约束格式 CREATE TABLE expert_new ( id INTEGER PRIMARY KEY, event_date TEXT CHECK( event_date GLOB '????-??-??*' OR event_date IS NULL ) ); -- 迁移数据 INSERT INTO expert_new SELECT id, date_field FROM expert; ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值