深入理解Spring-Security之认证授权分析

参考文件:Spring Security 参考手册|Spring Security中文版
学习一个技术想要深入研究肯定需要先看官网介绍,否则你可能不太好入手分析代码,可以根据官网的思路一点点的拆解。这是Spring-Security的中文介绍,有能力的可以看英文原版,对我这样只会hello world的只能看中文文档了。
git源码下载地址:https://github.com/spring-projects/spring-security
Spring Security是一个权限框架权限框架,常见的还有Shiro。权限框架的核心功能就是 认证授权
下面来简单分析一下Spring-Security是怎么实现认证授权的。首先在Security的过滤器中找一下认证过滤器。认证带有Authentication单词,这就是规范代码的好处。我们可以找到UsernamePasswordAuthenticationFilterBasicAuthenticationFilterCasAuthenticationFilterOAuth2LoginAuthenticationFilter等。CAS和OAuth2暂时不分析了,这些扩展过滤器可以自己看一下源码,BasicAuthenticationFilter与UsernamePasswordAuthenticationFilter是Spring-Security的基本过滤器,我们通常搭建个简单demo就会用到的,其doFilter()方法的逻辑其实都差不多的,就以UsernamePasswordAuthenticationFilter用户名密码认证过滤器为例来分析一下
UsernamePasswordAuthenticationFilter

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	...
	/**
	* 获取用户名密码信息,而且只能用POST方式发送。很简单的操作获取Request中的用户名和密码防撞到Token中。然后调用认证管理器获取认证类来认证Token
	*/
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

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

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            /*调用了认证管理器获取*/
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
	...
}

认证器管理器包括认证我们都在配置文件中配置过,就是下面这段代码

   /**
     * 认证管理器,这里也可以设置多个认证策略,Security支持多认证体系
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return new ProviderManager(Arrays.asList(authenticationProvider));
    }

看一下有关用户的AuthenticationProvider
AbstractUserDetailsAuthenticationProvider

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
	...
	//核心认证方法
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        //获取用户名,getPrincipal()就是获取用户信息,Shiro和Security权限框架都叫这个名
        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 = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }
                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }
		...//省略用户校验和缓存代码
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }
    //返回用户信息封装的Token
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }
    ...
}

看一下刚才有个retrieveUser获取用户的代码

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();
        //使用用户服务通过用户名获取用户信息,是不是看着眼熟,只要是用过这个框架的都知道需要重写个用户服务获取用户信息的,就在这调用的
	    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        ...//省略其他代码try/catch
    }

解释下刚才那个获取用户名的代码,在权限框架中我们一般这样获取用户信息:

	Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
	//principal就是用户信息,刚才获取用户名就是这里来的
	if (principal instanceof UserDetails) {
		String username = ((UserDetails)principal).getUsername();
	} else {
		String username = principal.toString();
	}

再往里深入一下看一下用户上下文维持类SecurityContextHolder,这是使用的ThreadLocal维护的用户信息,ThreadLocal就不用说了吧,多线程基础知识,可以创建每个线程独享的资源,也就是Map线程ID做Key,把变量放进去了。这样就不会导致线程安全问题了。

public class SecurityContextHolder {
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
   	/*使用的策略模式,如果没有设置默认为ThreadLocal模式*/
    private static String strategyName = System.getProperty("spring.security.strategy");
    ...
	/*初始化,这里采用的策略模式*/
    private static void initialize() {
    	/*如果没有设置模式,默认为ThreadLocal模式*/
        if (!StringUtils.hasText(strategyName)) {
            strategyName = "MODE_THREADLOCAL";
        }
        if (strategyName.equals("MODE_THREADLOCAL")) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
        	 /*可继承的ThreadLocal策略(百度翻译出来的)*/
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        } else if (strategyName.equals("MODE_GLOBAL")) {
        	/*不适用ThreadLocal模式,全局使用一个常见于SWing组件中*/
            strategy = new GlobalSecurityContextHolderStrategy();
        } else {
        	/*自定义策略*/
            try {
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
            } catch (Exception var2) {
                ReflectionUtils.handleReflectionException(var2);
            }
        }
    }
	...
}

看一下默认的用户信息存放的ThreadLocal类ThreadLocalSecurityContextHolderStrategy

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
	/*通过ThreadLocal保存认证信息(每个线程一份互不影响)*/
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();
    ...
    public SecurityContext getContext() {
        SecurityContext ctx = (SecurityContext)contextHolder.get();
        if (ctx == null) {
            ctx = this.createEmptyContext();
            contextHolder.set(ctx);
        }
        return ctx;
    }
    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(context);
    }
    ...
}

打个断点看一下:
在这里插入图片描述

下面再看一下用户信息是怎么维护进去的,这就需要看另一个过滤器SecurityContextPersistenceFilter 了,它的主要工作就是维护SecurityContext的,下面看一下它的源码:

/**
* 用来维护SecurityContextHolder上下文
*/
public class SecurityContextPersistenceFilter extends GenericFilterBean {
    ...
	//进入请求将请求信息维护到SecurityContextHolder 
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (request.getAttribute("__spring_security_scpf_applied") != null) {
            chain.doFilter(request, response);
        } else {
            boolean debug = this.logger.isDebugEnabled();
            request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
            if (this.forceEagerSessionCreation) {
                HttpSession session = request.getSession();
                if (debug && session.isNew()) {
                    this.logger.debug("Eagerly created session: " + session.getId());
                }
            }
			
            HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
            SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
            boolean var13 = false;
		   //将请求信息维护到SecurityContextHolder 
            try {
                var13 = true;
                SecurityContextHolder.setContext(contextBeforeChainExecution);
                chain.doFilter(holder.getRequest(), holder.getResponse());
                var13 = false;
            } finally {
                if (var13) {
                    SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
                    //清除SecurityContextHolder维护的信息。
                    SecurityContextHolder.clearContext();
                    this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
                    request.removeAttribute("__spring_security_scpf_applied");
                    if (debug) {
                        this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
                    }

                }
            }

            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            //清除SecurityContextHolder维护的信息。这个阿里代码规范中可能会遇到,ThreadLocal用完后要清除,否则内存泄漏。
            SecurityContextHolder.clearContext();
            this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute("__spring_security_scpf_applied");
            if (debug) {
                this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }

        }
    }
 	...
}

授权部分很简单可以自己尝试着分析下我这就不介绍了。
ThreadLocal以前也没怎么用过,但是上次写的代码里面每个Controller用到了一个状态标记,为了减少代码抽出到BaseController了,但是这时候就出问题了,Spring 的Controller好像不是线程安全的,状态不对报错了,用了ThreadLocal,用阿里代码规范扫描又扫出了高级漏洞,所以记住了每次用完都得clear一下防止内存泄漏。关于阿里代码规范插件安装的方式就不说了,网上有的是,个人建议:对于一些中小公司没有代码管理规范的可以用一下,一方面减少代码隐藏漏洞,就像我的漏洞可能用户量大了一直维持的用户信息没用释放掉,最终内存不足宕机了;第二方面代码交接的时候别人来读你的代码。

https://www.cnblogs.com/jiangbei/p/7668654.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值