LeeQingShui's Blog

  • 标签

  • 分类

  • 归档

  • 关于

(五)数据结构之二叉搜索树

发表于 2018-12-07 | 更新于 2023-09-04 | 分类于 数据结构与算法
本文字数: 2.2k | 阅读时长 ≈ 3 分钟

简介

  在计算机科学中,二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

阅读全文 »

(四)数据结构之链表

发表于 2018-12-04 | 更新于 2023-08-31 | 分类于 数据结构与算法
本文字数: 3.2k | 阅读时长 ≈ 5 分钟

简介

  链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针)(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到 O(1) 的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而顺序表相应的时间复杂度分别是 O(logn) 和 O(1)。

阅读全文 »

(三)数据结构之队列

发表于 2018-12-03 | 更新于 2023-08-20 | 分类于 数据结构与算法
本文字数: 1.1k | 阅读时长 ≈ 2 分钟
  此文待重构

简介

  队列,又称为伫列(queue),是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。

阅读全文 »

(二)数据结构之栈

发表于 2018-12-02 | 更新于 2023-08-20 | 分类于 数据结构与算法
本文字数: 2k | 阅读时长 ≈ 3 分钟

简介

  堆栈(英语:stack)又称为栈或堆叠,是计算机科学中一种特殊的串列形式的抽象数据类型,其特殊之处在于只能允许在链表或数组的一端(称为堆栈顶端指针,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。另外堆栈也可以用一维数组或链表的形式来完成。堆栈的另外一个相对的操作方式称为队列。

阅读全文 »

(一)数据结构之数组

发表于 2018-12-01 | 更新于 2023-08-19 | 分类于 数据结构与算法
本文字数: 1.9k | 阅读时长 ≈ 3 分钟

简介

  在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。(维基百科)

阅读全文 »

CentOS 7 防火墙

发表于 2018-01-08 | 更新于 2025-01-17 | 分类于 Ops
本文字数: 1.7k | 阅读时长 ≈ 2 分钟

  当我们在 Linux 服务器安装了某个应用服务器后,可能发现其并没有起效,这或许是因为防火墙在搞鬼。。。

阅读全文 »

Linux 复习笔记

发表于 2018-01-06 | 更新于 2025-09-01 | 分类于 Ops
本文字数: 7.7k | 阅读时长 ≈ 11 分钟

  仅用做复习之用。

阅读全文 »

JavaScript 复习笔记

发表于 2017-04-25 | 更新于 2022-06-21 | 分类于 前端
本文字数: 14k | 阅读时长 ≈ 20 分钟

什么是 JavaScript ?

  JavaScript(通常缩写为 JS )是一种高级的、解释型的编程语言[5]。JavaScript 是一门基于原型、函数先行的语言[6],是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。它提供语法来操控文本、数组、日期以及正则表达式等,不支持 I/O ,比如网络、存储和图形等,但这些都可以由它的宿主环境提供支持。它已经由 ECMA(欧洲计算机制造商协会)通过 ECMAScript 实现语言的标准化[5]。它被世界上的绝大多数网站所使用,也被世界主流浏览器( Chrome、IE、Firefox、Safari、Opera )支持。

  虽然 JavaScript 与 Java 这门语言不管是在名字上,或是在语法上都有很多相似性,但这两门编程语言从设计之初就有很大的不同,JavaScript 的语言设计主要受到了 Self(一种基于原型的编程语言)和 Scheme(一门函数式编程语言)的影响[6]。在语法结构上它又与C语言有很多相似(例如 if 条件语句、while 循环、switch 语句、do-while 循环等)[7]。

  在客户端,JavaScript 在传统意义上被实现为一种解释语言,但现在它已经可以被即时编译( JIT )执行。随着最新的 HTML5 和 CSS3 语言标准的推行它还可用于游戏、桌面和移动应用程序的开发和在服务器端网络环境运行,如 Node.js 。

阅读全文 »

CSS 复习笔记

发表于 2017-04-25 | 更新于 2022-10-22 | 分类于 前端
本文字数: 8.8k | 阅读时长 ≈ 13 分钟

  CSS(Cascading Style Sheets),即层叠样式表,是一种用来为结构化文档(如 HTML 文档或 XML 应用)添加样式(字体、间距和颜色等)的计算机语言,由 W3C 定义和维护。

阅读全文 »

(二)Spring Security 认证原理

发表于 2011-10-24 | 更新于 2022-10-22 | 分类于 信息安全
本文字数: 3.3k | 阅读时长 ≈ 5 分钟

序言

  在本系列文章(一)初识 Spring Security 中,我们不仅初步认识了 Spring Security,明晰了 Spring Security 的三个核心概念——认证、鉴权、防护,还准备研究其工作原理。

  现在,就跟随本篇文章来探讨认证与授权是如何实现的吧。

认证与授权

  在 Security 中,认证和授权功能是交由过滤器UsernamePasswordAuthenticationFilter来完成的。
  那么,UsernamePasswordAuthenticationFilter又是如何运作的呢?

  我们直接去阅读源码,最终将得到简化的认证授权流程信息:

  • ① UsernamePasswordAuthenticationFilter执行attemptAuthentication方法将提交的表单信息封装为一个UsernamePasswordAuthenticationToken对象,传递给AuthenticationManager执行authenticate认证方法
  • ② AuthenticationManager接口通过多态选择默认的实现类ProviderManager去执行authenticate方法
  • ③ ProviderManager在执行该方法时会遍历AuthenticationProvider列表,通过多态选择实现类AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider去执行认证方法
  • ④ DaoAuthenticationProvider在retrieveUser方法中会从UserDetailsService的实现类的loadUserByUsername得到UserDetails对象
  • ⑤ 在校验UserDetails对象的合理性之后,创建一个认证成功的存放了权限信息的Authentication对象,用作后续的鉴权

  现在你肯定看的一头雾水,因为你并不知道上面流程中的各个接口和类的作用是什么,不过没关系,下面我们将一步一步地分析它们。

UsernamePasswordAuthenticationToken

  在认证流程的第 ① 步中会将表单信息封装UsernamePasswordAuthenticationToken该对象,这发生在UsernamePasswordAuthenticationFilter的attemptAuthentication方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 1.获取表单中的用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
// 2.封装为一个对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

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

  这个对象是干啥的呢?我们看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
...
}

public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {

// 权限集合
private final Collection<GrantedAuthority> authorities;
private Object details;
private boolean authenticated = false;
...
}

  从源码中得知:

  • principal:等同于username
  • credentials:等同于password
  • authorities:继承的父类权限集合暂时为空

AuthenticationManager

  构造完UsernamePasswordAuthenticationToken对象后应当做些什么?
  那肯定是查看数据库是否存在与UsernamePasswordAuthenticationToken对象匹配的用户名,没有肯定就认证失败了嘛!

  认证具体是交由AuthenticationManager接口的实现类ProviderManager去完成的,我们来看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 认证提供者列表
private List<AuthenticationProvider> providers = Collections.emptyList();

public List<AuthenticationProvider> getProviders() {
return providers;
}

public Authentication authenticate(Authentication authentication)
throws AuthenticationException {

for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
...

try {
// 根据不同的认证提供者进行认证
result = provider.authenticate(authentication);
// 只要有一个认证提供者认证成功,就算认证成功,跳出循环
if (result != null) {
copyDetails(authentication, result);
break;
}
...
} catch {
...
}
}

if (result != null) {
...
return result;
}
}

  ProviderManager维护了一个 List,该 List 中存放了AuthenticationProvider接口不同的实现类,认证时需遍历所有的实现类进行认证,只要有一个AuthenticationProvider认证成功,就算认证成功。

  一般情况下,是由AuthenticationProvider接口的实现类DaoAuthenticationProvider根据用户名从数据源中获取用户信息对象的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {

prepareTimingAttackProtection();
try {
// 从数据源中根据用户名获取 UserDetails 对象
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
} catch (){
...
}
}

  获取到UserDetails对象后,就是密码的比对了,由于注册时密码应当会以特定算法加密密码用密文进行保存,所以解密时也需要先通过对应的加密算法加密为密文,与获取的密文进行匹配,匹配成功,则认证成功。
  以上过程由additionalAuthenticationChecks方法完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
// 表单未输密码抛异常
throw new BadCredentialsException(...);
}

String presentedPassword = authentication.getCredentials().toString();

// 将表单密码加密后与数据源获取的密文进行匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
// 不匹配抛异常
throw new BadCredentialsException(...);
}
}

扩展——ProviderManager 为何维护了一个 AuthenticationProvider 列表?

  在前面的分析过程中,我们发现ProviderManager类中维护了一个AuthenticationProvider列表,为什么需要这个列表呢?
  定义AuthenticationProvider接口的目的在于认证方式可能不同,需要子类实现,默认情况下的认证方式实现类为DaoAuthenticationProvider。
  emmm,认证方式,什么意思?
  我们知道现在表单使用的是用户名/密码的信息,所以我们数据源查询应当是根据用户名查询出用户对象,但如果产品提了需求说再加一种认证方法,使用电话和验证码的方式进行认证,那现在的认证方式不就不符合业务需要了嘛?
  此时,我们可以自定义一个AuthenticationProvider的实现类,重写业务逻辑以匹配业务场景。

  因此,总结来讲就是会存在五花八门的认证方式:

  • 用户名/密码
  • 邮箱/密码
  • 手机号/验证码
  • 人脸识别

  因此,若默认的用户名/密码认证方式不符合业务需要,可以随业务进行扩展。

UserDetailsService

  不得不提一嘴,若没有任何配置,Spring Security 根据用户名查询用户对象的数据源是从默认的内存中读取的,这自然不符合正常场景,我们的用户信息都是保存到数据库的,那么如何进行修改以从数据库中查询用户对象呢?

  从前面知道,根据用户名获取用户信息的过程是由前面的DaoAuthenticationProvider中调用的retrieveUser方法的以下方法实现:

1
2
// 从数据源中根据用户名获取 UserDetails 对象
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

  我们从源码来看看这个UserDetailsService到底是什么:

1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

  很明显,它是一个接口,默认根据用户名读取用户信息的实现类InMemoryUserDetailsManager重写了它的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class InMemoryUserDetailsManager implements UserDetailsManager,
UserDetailsPasswordService {
......

public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());

if (user == null) {
throw new UsernameNotFoundException(username);
}

return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
......
}

  那么,我们现在想走数据库去查询用户信息,肯定就需要实现该接口,重写其方法,从数据库读取用户信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
public class UserServiceImpl implements UserDetailsService {

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private UserMapper userMapper;

/**
* Spring Security 认证授权均通过 UserDetailsService 接口提供的 loadUserByUsername 方法,
* 该方法返回 UserDetails 对象,该对象包含了用户角色权限信息,之后 url 权限校验需要使用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> userWrapper = new QueryWrapper<>();
userWrapper.eq("username", username);
User user = userMapper.selectOne(userWrapper);
if (user == null) {
throw new UsernameNotFoundException(username);
}
Set<String> permissionSet = userMapper.selectPermissionListByUsername(username);

return new SecurityUser(user.getUsername(), user.getPassword(), permissionSet,
true, true, true, true);
}
}

  定义完成以后,还需要将默认的实现类进行替换,直接注入即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@EnableWebSecurity 
public class SecurityConfig extends WebSecurityConfigurerAdapter {

......

@Autowired
private UserServiceImpl userService;

/**
* 认证:使用自定义的 UserDetailsService 实现类读取用户信息
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
......
}

UserDetails

  不知道你有没有注意到一点,loadUserByUsername方法的返回值为UserDetails接口,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserDetails extends Serializable {

// 权限集合
Collection<? extends GrantedAuthority> getAuthorities();

// 获取密码
String getPassword();

// 获取用户名
String getUsername();

// 帐号未过期
boolean isAccountNonExpired();

// 帐号未锁定
boolean isAccountNonLocked();

// 凭证未过期
boolean isCredentialsNonExpired();

// 帐号可用
boolean isEnabled();
}

  咦,这方法不是应该从数据库查询出用户对象嘛?为什么返回这个接口呢?什么意思?

  emmm,这方法准确而言,应当从数据库查询出一个包含角色和权限的用户对象,因为后续鉴权需要使用到,并且由于 Spring Security 后续鉴权都是通过UserDetails接口多态进行判断的,所以我们最好创建一个对象实现该接口,然后根据数据库信息填充该对象,不然是不符合 Security 的要求滴!

PasswordEncoder

  前面说过,数据库的密码是加密后的密文形式,所以比对时需要将表单的密码进行加密,之后才能判断它们是否相等,表单密码的加密就需要用到PasswordEncoder了:

1
2
3
4
5
6
7
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"));
}

参考

  • Spring Security 官网
1…12131415
LeeQingShui

LeeQingShui

144 日志
16 分类
68 标签
RSS
© 2018 – 2025 LeeQingShui | 站点总字数: 846k
赣 ICP 备 2022002212 号
本站已运行
本站总访问量 次 | 本站访客 人次
0%