框架学习SMPE后端框架 - Spring Security

Posted 毛_三月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了框架学习SMPE后端框架 - Spring Security相关的知识,希望对你有一定的参考价值。

Spring Security

1. SecurityContextHolder 工具类

提供一些静态方法,目的是用来保存应用程序中当前使用人的安全上下文(调用接口的权限,每个用户的权限不同)。


// 获取安全上下文对象,就是那个保存在 ThreadLocal 里面的安全上下文对象
// 总是不为null(如果不存在,则创建一个authentication属性为null的empty安全上下文对象)
SecurityContext securityContext = SecurityContextHolder.getContext();

// 获取当前认证了的 principal(当事人),或者 request token (令牌)
// 如果没有认证,会是 null,该例子是认证之后的情况
Authentication authentication = securityContext.getAuthentication()

// 获取当事人信息对象,返回结果是 Object 类型,但实际上可以是应用程序自定义的带有更多应用相关信息的某个类型。
// 很多情况下,该对象是 Spring Security 核心接口 UserDetails 的一个实现类,你可以把 UserDetails 想像成我们数据库中保存的一个用户信息到 SecurityContextHolder 中 Spring Security 需要的用户信息格式的一个适配器。
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) 
	String username = ((UserDetails)principal).getUsername();
 else 
	String username = principal.toString();

2. Spring Security中将使用username和password封装成Authentication的实现类UsernamePasswordAuthenticationToken,声明为了authenticationToken

UsernamePasswordAuthenticationToken authenticationToken =
    new UsernamePasswordAuthenticationToken(userId, password);

Authentication authentication = null;
try 
    authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
 catch (BadCredentialsException e) 
    //账号或密码错误
    throw new BadRequestException(ResultEnum.LOGIN_FAIL);
 catch (InternalAuthenticationServiceException e) 
    throw new BadRequestException(ResultEnum.COUNT_NOT_ENABLE);

SecurityContextHolder.getContext().setAuthentication(authentication);

2.1 整个过程的讲解

Spring Security 是通过**AbstractAuthenticationProcessingFilter类向Web应用的基于HTTP、浏览器的请求提供身份验证服务。**

2.1.1 UsernamePasswordAuthenticationFilter类的说明

UsernamePasswordAuthenticationFilter类是AbstractAuthenticationProcessingFilter抽象类针对 使用 用户名和密码 进行身份验证 而定制化的一个过滤器类

首先我们来了解一下AbstractAuthenticationProcessingFilter抽象类在框架中的角色和职责

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ih4uGAwK-1638009468755)(G:\\三月\\Java文件\\JAVA路线\\Typora笔记\\项目-框架\\SMPE\\SMPE框架分享图片\\photo\\AbstractAuthenticationProcessingFilter.png)]

AbstractAuthenticationProcessingFilter抽象类在整个身份验证的流程中主要处理的工作就是所有与Web资源相关的事情,并且将其封装成Authentication对象,最后调用AuthenticationManager的验证方法。

所以UsernamePasswordAuthenticationFilter类的工作大致也是这样。只不过当前情境下更加明确了Authentication对象的封装数据的来源–用户名和密码。

AbstractAuthenticationProcessingFilter抽象类的方法和属性如图所示。

UsernamePasswordAuthenticationFilter类继承拓展了AbstractAuthenticationProcessingFilter抽象类,有了一下几个改动:

  1. 属性中增加了usernameParameter、passwordParameter和postOnly属性。
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;
  1. 此类的构造器中强制指定请求的方式为POST。
public UsernamePasswordAuthenticationFilter() 
    super(new AntPathRequestMatcher("/login", "POST"));

  1. 重写了attemptAuthentication身份验证入口方法(创建了UsernamePasswordAuthenticationToken类对象来封装request中传过来的username和password)
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);

    // 将我们当前请求request中的session,ip等放到token中
    setDetails(request, authRequest);

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

2.1.2 封装用户名和密码的基石:UsernamePasswordAuthenticationToken类

UsernamePasswordAuthenticationToken类需要从HttpRequest中获取username和password字段,并将其封装进Authentication中传递给AuthenticationManager进行身份验证。

Authentication接口中属性如图

UsernamePasswordAuthenticationFilter类的功能是 用户名和密码的过滤器 。给它一组用户名和密码,如果匹配,那么就算验证成功,否则验证失败。

此类的attemptAuthentication身份验证入口方法 定义了UsernamePasswordAuthenticationToken类对象来存储从request中获取到的username 和 password。看上方的代码块。

UsernamePasswordAuthenticationToken类继承了AbstractAuthenticationToken抽象类,与抽象类的主要区别为 将username赋值给属性principal,将password赋值给credentials。

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

UsernamePasswordAuthenticationFilter类attemptAuthentication方法中,通过UsernamePasswordAuthenticationToken实例化了Authentication接口,继而按照流程,将其传递给AuthenticationMananger调用身份验证核心完成相关工作。

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    username, password);

//将我们当前请求request中的session,ip等放到token中
    setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
2.1.2.1 第一部分

上方程序的第一部分,创建了一个还没有经过认证的token,principal和credentials属性分别为username 和 password

//初始化为 未认证过的token,通过认证后,会重新调用另一个UsernamePasswordAuthenticationToken构造方法,将authenticated属性设置为true
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) 
    //父类权限,当前为登录,所有权限为空
    super(null);
    this.principal = principal;
    this.credentials = credentials;
    //设置是否认证属性为false
    setAuthenticated(false);

super(null)详解

UsernamePasswordAuthenticationToken类的父类的构造器为自定义构造器,子类构造器中需要显示调用父类构造器 。父类构造器需要authorities参数,表示权限集合。当前场景为登录,所以权限集合设置为空。

public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer 

    private final Collection<GrantedAuthority> authorities;
    private Object details;
    private boolean authenticated = false;

	//构造器 需要给authorities属性赋值
    public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) 
        //如果传入的参数为null,authorities设置为常量
        /**
        public abstract class AuthorityUtils 
            public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();
		
        */
        if (authorities == null) 
            this.authorities = AuthorityUtils.NO_AUTHORITIES;
            return;
        

        for (GrantedAuthority a : authorities) 
            if (a == null) 
                throw new IllegalArgumentException(
                    "Authorities collection cannot contain any null elements");
            
        
        ArrayList<GrantedAuthority> temp = new ArrayList<>(
            authorities.size());
        temp.addAll(authorities);
        //将temp集合变成一个不能修改的集合,给authorities属性赋值
        this.authorities = Collections.unmodifiableList(temp);
    

Collection authorities属性详解

GrantedAuthority是一个接口,里面定义getAuthority方法

public interface GrantedAuthority extends Serializable 
	String getAuthority();

通常使用GrantedAuthority接口的一个实现类SimpleGrantedAuthority

定义一个String类型属性role 用来存放用户的权限

重写接口的getAuthority方法,返回对象的role属性

public final class SimpleGrantedAuthority implements GrantedAuthority 

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	
	private final String role;

	public SimpleGrantedAuthority(String role) 
		Assert.hasText(role, "A granted authority textual representation is required");
		this.role = role;
	
	
	@Override
	public String getAuthority() 
		return role;
	

	@Override
	public boolean equals(Object obj) 
		if (this == obj) 
			return true;
		

		if (obj instanceof SimpleGrantedAuthority) 
			return role.equals(((SimpleGrantedAuthority) obj).role);
		

		return false;
	

	@Override
	public int hashCode() 
		return this.role.hashCode();
	

	@Override
	public String toString() 
		return this.role;
	

2.1.2.2 第二部分

上方程序的第二部分 setDetails(request,authRequest); 调用UsernamePasswordAuthenticationFilter类中的setDetails方法

protected void setDetails(HttpServletRequest request,UsernamePasswordAuthenticationToken authRequest) 
   //将request作为authRequest的details属性的值(把我们当前请求request中的session、ip等放到token中)
  authRequest.setDetails(authenticationDetailsSource.buildDetails(request));

UsernamePasswordAuthenticationToken类继承于AbstractAuthenticationToken类,类中定义了三个属性

public abstract class AbstractAuthenticationToken implements Authentication,CredentialsContainer 
    // ~ Instance fields
    // ================================================================================================
	//权限集合
    private final Collection<GrantedAuthority> authorities;
    //详情
    private Object details;
    //是否通过验证
    private boolean authenticated = false;

2.1.2.3 第三部分

return this.getAuthenticationManager().authenticate(authRequest); 返回认证之后的Authentication接口类型的实现类。详见2.1.3

2.1.3 验证核心的工作者:AuthenticationProvider

AuthenticationManager接口在设计上并不是用于完成特定的身份验证工作,而是调用其配发的AuthenticationProvider接口去实现的。

到这里就有一个疑问:

针对接口声明参数 声明的Authentication,针对不同验证协议的AuthenticationProvider的实现类们是如何完成对应的工作的,并且AuthenticationManager是如何知道应该使用哪一个AuthenticationProvider才能完成对应协议的验证工作?

AuthenticationProvider只包含两个方法声明,核心验证方法入口:

Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

另一个是让AuthenticationManager可以 通过调用此方法 辨别当前AuthenticationProvider 是否能完成响应验证工作 的supports方法。

boolean supports(Class<?> authentication);

简单来说:AuthenticationProvider有两个方法,authenticate方法用来验证当前的Authentication接口的实现类

supports方法用来返回 AuthenticationProvider接口能不能验证当前的Authentication。

在Spring Security中唯一AuthenticationManager的实现类ProviderManager,在处理authenticate身份验证入口方法时,首先解决的问题便是:哪一个AuthenticationProvider实现类可以验证当前传入的Authentication?

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException 
    for (AuthenticationProvider provider : getProviders()) 
    //...


UsernamePasswordAuthenticationFilter类attemptAuthentication方法中

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

this.getAuthenticationManager() 调用了从抽象类AbstractAuthenticationProcessingFilter中继承的方法,返回AuthenticationManager接口的对象。实际上返回的是实现了AuthenticationManager接口的类的对象。AuthenticationManager接口的实现类为ProviderManager。

也就是执行的是providerManager.authenticate(authRequest);

UsernamePasswordAuthenticationFilter类和ProviderManager类中的程序执行 如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9VkTm4gu-1638009468761)(G:\\三月\\Java文件\\JAVA路线\\Typora笔记\\项目-框架\\SMPE\\SMPE框架分享图片\\photo\\providerManager程序执行.png)]

在ProviderManager的视角里,所有的Authentication接口的实现类都不具名,ProviderManager不仅不能通过自身完成 验证工作,也不能独立完成辨别当前AuthenticationProvider 是否能完成响应验证工作。

而是统统交给AuthenticationProvider去完成。而不同的AuthenticationProvider接口的实现类开发的初衷就是为了支持指定的某种验证协议,所以在特定的AuthenticaitonProvider的视角中,它只关心当前Authentication是不是它预先设计处理的类型即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xc4cNUzL-1638009468762)(G:\\三月\\Java文件\\JAVA路线\\Typora笔记\\项目-框架\\SMPE\\SMPE框架分享图片\\photo\\AuthenticationProvider接口的方法.png)]

DaoAuthenticationProvider类是AbstractUserDetailsAuthenticationProvider抽象类的子类。AbstractUserDetailsAuthenticationProvider抽象类实现了AuthenticationProvider接口。

AbstractUserDetailsAuthenticationProvider抽象类中实现了AuthenticationProvider接口的supports方法

DaoAuthenticationProvider针对UsernamePasswordAuthenticationToken的大部分逻辑都是通过AbstractUserDetailsAuthenticationProvider完成的。比如针对ProviderManager询问是否支持当前Authentication的supports方法:

//AuthenticationProvider接口的supports方法
boolean supports(Class<?> authentication);
//AbstractUserDetailsAuthenticationProvider实现后的supports方法
public boolean supports(Class<?> authentication) 
    return (UsernamePasswordAuthenticationToken.class
            .isAssignableFrom(authentication));

isAssignableFrom方法是判断两个类之间是否存在继承关系。

DaoAuthenticationProvider类会判断当前的Authentication的实现类是否是UsernamePasswordAuthenticationToken它本身,或者是扩展了UsernamePasswordAuthenticationToken的子孙类。返回true的场景只有一种,便是当前的Authentication是UsernamePasswordAuthenticationToken实现。

核心验证逻辑:AbstractUserDetailsAuthenticationProvider抽象中实现的authenticate方法

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);

当ProviderManager.authenticate方法执行了以下代码后,就会将验证工作交给DaoAuthenticationProvider进行处理

if (!provider.supports(toTest)) 
    continue;


if (debug) 
    logger.debug("Authentication attempt using "
                 + provider.getClass().getName());

//此处开始调用DaoAuthenticationProvider类中的authenticate方法。
try 
    result = provider.authenticate(authentication);

    if (result != null) 
        copyDetails(authentication, result);
        break;
    

与ProviderManager最不同的一点是,在DaoAuthenticationProvider的视角中,当前的Authentication最起码一定是UsernamePasswordAuthenticationToken的形式了,不用和ProviderManager一样因为匮乏信息而不知道干什么。然后DaoAuthenticationProvider分别会按照预先设计的一样分别从authentication的principal和credentials中获取用户名和密码。

String username = (authentication.getPrincipal()以上是关于框架学习SMPE后端框架 - Spring Security的主要内容,如果未能解决你的问题,请参考以下文章

自己手写一个 SpringMVC 框架

荧客技荐自己手写一个 SpringMVC 框架

用于 Spring Boot(和其他 Web 框架)的 Java 或 Kotlin [关闭]

基于SpringBoot的ES整合

Spring MVC框架学习 ---- 传递参数

Spring MVC 框架学习----连接程序