SpringSecurity原理解析(一)

一、SpringSecurity 核心组件

在SpringSecurity中的jar包有4个,作用分别为:

|------------------------|---------------------------------------------------------------------------|
| spring-security-core | SpringSecurity的核心jar包,认证和授权的核心代码都在这里面 |
| spring-security-config | 如果使用Spring Security XML名称空间进行配置或Spring Security的Java configuration支持,则需要它 |
| spring-security-web | 用于Spring Security web身份验证服务和基于url的访问控制 |
| spring-security-test | 测试单元 |

1、Authentication

Authentication 是 org.springframework.security.core 包下的一个接口,

Authentication 表示一个当前认证的对象,继承了接口 Principal,接口Principal 用来

表示一个主题的抽象概念,可以用来表示任何实体对象,如:个人、公司、登录id等,

简单的来说 Principal 可以表示任何具体的对象。

Authentication 定义的方法如下:

java 复制代码
public interface Authentication extends Principal, Serializable {

	// 获取认证用户拥有的对应的权限
	Collection<? extends GrantedAuthority> getAuthorities();

	// 获取用户的凭证(认证)
	Object getCredentials();

    // 存储有关身份验证请求的其他详细信息。这些可能是 IP地址、证书编号等
    //即获取认证用户的详细信息
	Object getDetails();

     // 获取用户信息 通常是 UserDetails 对象
	Object getPrincipal();

    // 判断当前用户的登录状态
	boolean isAuthenticated();

    // 设置认证状态
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

2、SecurityContextHolder

2.1、SecurityContext

既然上面说 Authentication 表示一个认证对象,那么Authentication对象是如何放到

SpringSecurity 中的?

SecurityContext 表示 SpringSecurity 上下文对象(也可看成是一个容器);

SpringSecurity 是包 org.springframework.security.core.context 中一个接口,定义如下:

java 复制代码
public interface SecurityContext extends Serializable {
   
    //获取Authentication 
	Authentication getAuthentication();
    
    //保存Authentication 
	void setAuthentication(Authentication authentication);

}

通过 SecurityContext 的定义可以发现, SecurityContext就干了2件事情,即:

1)保存 Authentication

2)获取 Authentication

2.2、SecurityContextHolder

下面来看看在spring-security-core中的SecurityContextHolder,这个是一个非常基础的

对象,存储了当前应用的上下文SecurityContext,而在SecurityContext可以获取

Authentication对象。也就是当前认证的相关信息会存储在Authentication对象中。

另外 SecurityContextHolder 还定义了 SecurityContext 的存储方式。

SecurityContextHolder 定义如下:

java 复制代码
public class SecurityContextHolder {
	
    //下边2个常量表示 SecurityContext  存储模式
	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	public static final String MODE_GLOBAL = "MODE_GLOBAL";

    //配置文件中自定义strategy时,配置项的名称
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

    //用于存储SecurityContext 
	private static SecurityContextHolderStrategy strategy;
    //SecurityContextHolder 初始化次数
	private static int initializeCount = 0;

	static {
		initialize();
	}

	
	/**
	 * 清除保存的SecurityContext 
	 */
	public static void clearContext() {
		strategy.clearContext();
	}

	/**
	 * 获取 SecurityContext 
	 * 注意这个方法是static 静态方法,可由类直接调用
	 * @return the security context (never <code>null</code>)
	 */
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	/**
	 * 返回初始化次数
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

    //初始化
	private static void initialize() {
        //判断配置文件中是否指定 strategy的实现类,若没指定则使用 MODE_THREADLOCAL
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}

		if (strategyName.equals(MODE_THREADLOCAL)) {
            //表示把SecurityContext 存储在当前线程的 ThreadLocal中,线程之间隔离
			strategy = new ThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
            //表示SecurityContext 在父子线程之间可以共享
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_GLOBAL)) {
            //表示把SecurityContext 存储在全局变量中,全局共享
			strategy = new GlobalSecurityContextHolderStrategy();
		}
		else {
			// Try to load a custom strategy
			try {
				Class<?> clazz = Class.forName(strategyName);
				Constructor<?> customStrategy = clazz.getConstructor();
				strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
			}
			catch (Exception ex) {
				ReflectionUtils.handleReflectionException(ex);
			}
		}

		initializeCount++;
	}

	/**
	 * 保存 SecurityContext 
	 */
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}

	
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}

	
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}

	@Override
	public String toString() {
		return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount="
				+ initializeCount + "]";
	}
}

默认情况下,SecurityContextHolder是通过 `ThreadLocal`来存储对应的信息的。也就是

在一个线程中我们可以通过这种方式来获取当前登录的用户的相关信息。而在

SecurityContext中就只提供了对Authentication对象操作的方法

2.3、SecurityContextHolderStrategy

在上边可以发现 SecurityContext 真正保存在 SecurityContextHolderStrategy 中的。

SecurityContextHolderStrategy有三个是实现类,分别是:

|-----------------------------------------------|----------------------------------------------------------------------------------------|
| GlobalSecurityContextHolderStrategy | 把SecurityContext存储为static变量,全局共享 |
| InheritableThreadLocalSecurityContextStrategy | 把SecurityContext存储在InheritableThreadLocal中 InheritableThreadLocal解决父线程生成的变量传递到子线程中进行使用 |
| ThreadLocalSecurityContextStrategy | 把SecurityContext存储在ThreadLocal中,只有当前线程可以使用 |

2.4、Authentication、SecurityContext、SecurityContextHolder三者之间的关系

SecurityContext 存储保存 Authentication,SecurityContextHolder存储并保存

SecurityContext,通过SecurityContextHolder可以获取到 Authentication

即如下图所示:

明白了3者之间的关系,下面可以通过 SecurityContextHolder 来获取当前登录的用户信息

示例代码如下:

java 复制代码
public String getLoginUser(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        if(principal instanceof UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            System.out.println(userDetails.getUsername());
            return "当前登录的账号是:" + userDetails.getUsername();
        }
        return "当前登录的账号-->" + principal.toString();
    }

调用 getContext()返回的对象是 SecurityContext接口的一个实例,这个对象默认是保

存在线程中的。接下来将看到,Spring Security中的认证大都返回一个 UserDetails的实

例作为Principa。

3、UserDetailsService

在上面的关系中我们看到在Authentication中存储当前登录用户的是Principal对象,而通常

情况下Principal对象可以转换为UserDetails对象。UserDetails是Spring Security中的一个

核心接口。它表示一个Principal,但是是可扩展的、特定于应用的。可以认为 UserDetails

是数据库中用户表记录和Spring Security在 SecurityContextHolder中所必须信息的适配器。

UserDetails 接口定义如下

java 复制代码
public interface UserDetails extends Serializable {

	// 对应的权限
	Collection<? extends GrantedAuthority> getAuthorities();

	// 密码
	String getPassword();

	// 账号
	String getUsername();

	// 账号是否过期
	boolean isAccountNonExpired();

	// 是否锁定
	boolean isAccountNonLocked();

	// 凭证是否过期
	boolean isCredentialsNonExpired();

	// 账号是否可用
	boolean isEnabled();

}

UserDetails 接口的默认实现是类User,如下图所示:

那么问题来了,这个UserDetails对象什么时候提供给SecurityContextHolder呢?

这就需要用到接口UserDetailsService,在用户认证时我们需要实现UserDetailsService

,在UserDetailsService的方法 loadUserByUsername 中,将从数据库查询到的用户信息封

装成User对象。

UserDetailsService接口定义如下:

java 复制代码
public interface UserDetailsService {

	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

Spring Security提供了许多 UserDetailsSerivice接口的实现,包括使用内存中map的实现

(InMemoryDaoImpl低版本 InMemoryUserDetailsManager)和使用JDBC的实现

(JdbcDaoImpl)。但在实际开发中我们更喜欢自己来编写,比如UserServiceImpl我们的案例

UserDetailsService接口的实现有如下:

UserServiceImpl我们的案例代码如下:

java 复制代码
/**
 * 用户认证接口
 * todo 注意:
 *    继承 UserDetailsService
 */
public interface UserService extends UserDetailsService {
}

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private SysUserMapper sysUserMapper;

    /**
     * 根据账号查询用户信息,并进行用户认证
     * @param userName
     * @return
     * @throws UsernameNotFoundException
     */
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {

        //根据账号查询用户信息
        SysUser sysUser = sysUserMapper.queryByUserName(userName);
        //账号存在
        if(sysUser != null){
            /**
             * 封装用户的权限
             */
            List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
            //添加用户的权限,
            list.add(new SimpleGrantedAuthority("ROLE_USER"));
            // 表示账号存在
            UserDetails userDetails = new User(
                    sysUser.getUserName() // 账号
                    ,sysUser.getPasseord() // 密码,{noop}表示不加密
                    ,true  //表示当前账号可用
                    ,false //表示当前账号是否过期,false=过期
                    ,true //表示凭证是否过期,true=未过期
                    ,true //用户是否被锁定,true=未被锁定
                    ,list
            );
            return userDetails;
        }

        //用户不存在
        return null;
    }

    public static void main(String[] args) {

        //BCryptPasswordEncoder是Spring Security 提供的密码加密方式
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String password = "admin";
        System.out.println(encoder.encode(password));
    }
}

4、GrantedAuthority

我们在Authentication中看到不光关联了Principal还提供了一个getAuthorities()方法来获取

对应的GrantedAuthority对象数组。和权限相关,后面在权限模块详细讲解

GrantedAuthority定义如下:

java 复制代码
public interface GrantedAuthority extends Serializable {

    //获取权限信息
	String getAuthority();

}

5、总结

上面介绍到的核心对象总结

|-----------------------|--------------------------------------------|
| Authentication | 特定于Spring Security的principal |
| SecurityContext | 存放了Authentication和特定于请求的安全信息 |
| SecurityContextHolder | 用于获取SecurityContext |
| UserDetails | 提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息 |
| UserDetailsService | 接受String类型的用户名,创建并返回UserDetail |
| GrantedAuthority | 对某个principal的应用范围内的授权许可 |

相关推荐
Chan16几秒前
【 SpringCloud | 微服务 MQ基础 】
java·spring·spring cloud·微服务·云原生·rabbitmq
LucianaiB3 分钟前
如何做好一份优秀的技术文档:专业指南与最佳实践
android·java·数据库
面朝大海,春不暖,花不开27 分钟前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
得过且过的勇者y27 分钟前
Java安全点safepoint
java
夜晚回家1 小时前
「Java基本语法」代码格式与注释规范
java·开发语言
斯普信云原生组1 小时前
Docker构建自定义的镜像
java·spring cloud·docker
wangjinjin1801 小时前
使用 IntelliJ IDEA 安装通义灵码(TONGYI Lingma)插件,进行后端 Java Spring Boot 项目的用户用例生成及常见问题处理
java·spring boot·intellij-idea
wtg44521 小时前
使用 Rest-Assured 和 TestNG 进行购物车功能的 API 自动化测试
java
白宇横流学长2 小时前
基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
java·spring boot·后端
fat house cat_2 小时前
【redis】线程IO模型
java·redis