`
FlyAway2
  • 浏览: 111773 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Acegi的学习

    博客分类:
  • j2ee
 
阅读更多

项目上有用到Acegi,而项目上从不会教技术之类的,只好自己看源码。断断续续几个月时间。开始有些眉目。虽然我后面知道Acegi早已经过时了(难怪里面代码难懂,各种代码乱飞,莫非是没人维护的原因?),但是既然开始了,我还是想有始有终。

 

(现在做类似的项目真是悲哀,从没有人讲技术或是业务,就只是给你一个任务,你自己去搞,最终的代码的质量只能靠不断的无比重复烦闷低效的各种的测试来保证,测试不到的就只能是让维护交接的人悲剧了。开发人员能否学到东西?项目经理似乎从来不管,最终只有靠自己了!唉,我觉得项目组安排讲讲技术架构啊、业务流程啊肯定是有利于代码质量的,而且是肯定能提高开发效率的,也能让coder不再是单纯的coder,能够有所提升。可惜从来没有。唉,我要是项目经理一定不能这样啊!)

 

 

Acegi是用java开发的一套安全认证的框架,所谓java程序安全呢,其实主要就是认证和授权。

Acegi主要是由一系列的过滤器或拦截器组成。

首先的一个就是FilterToBeanProxy,它有一个targetClass属性,通常就用Acegi提供的FilterChainProxy,FilterChainProxy有属性:filterInvocationDefinitionSource,这是相当重要的一个属性,在它里面我们配置所有我们需要的的过滤器拦截器,

 

Acegi源码和spring绑定,所以只能在配合spring使用。感觉还是要非常属性spring才行,否则真是没法看下去。

而且名字一个个长的要死,看了一下源码,真晕,看不太懂耶,还是没多少耐心吧,唉!(我是用java decompiler 看的,感觉这个软件还是不太人性化,不好用,没eclipse好啊)

 

一般来说,有:

session过滤器:HttpSessionContextIntegrationFilter(是用来把认证信息记录到Session中的)

注销处理过滤器:LogoutFilter

表单认证过滤器:AuthenticationProcessingFilter

cookie登录过滤器:RememberMeProcessingFilter

匿名认证过滤器:AnonymousProcessingFilter

异常转换过滤器:ExceptionTranslationFilter

 

用户授权拦截器:FilterSecurityInterceptor

 

 

最关键的是两大管理器:

身份认证管理器ProviderManager对应org.acegisecurity.providers.ProviderManager

资源授权管理器accessDecisionManager对应org.acegisecurity.vote.AffirmativeBased

 

 

 

 

 

 

 

下面从FilterChainProxy开始为大家讲讲自己的心得,也望大家多多给我评论:意见、建议或者鼓励,写这篇文章确实是花了不少心血的。

 

:::

FilterChainProxy继承Filter

它主要有类型为FilterInvocationDefinitionSource的属性filterInvocationDefinitionSource;

类型为ApplicationContext的applicationContext,applicationContext当然就是结合spring使用的。

 

 

init

 

init函数里面调用obtainAllDefinedFilters,读取配置,获取其下面的各个filter,然后执行各自的

 

init之前,当然,这还要先初始化了filterInvocationDefinitionSource;filterInvocationDefinitionSource的初始化是由spring调用的,

FilterInvocationDefinitionSource空继承ObjectDefinitionSource接口,主要有三个方法:

 

 

public abstract interface ObjectDefinitionSource
{
  public abstract ConfigAttributeDefinition getAttributes(Object paramObject)
    throws IllegalArgumentException;

  public abstract Iterator getConfigAttributeDefinitions();

  public abstract boolean supports(Class paramClass);
}
 
 

 

ConfigAttributeDefinition主要有一个configAttributes的属性:

private List configAttributes = new Vector();

主要包括addConfigAttribute、getConfigAttributes、contains、equals等方法

addConfigAttribute就是将一个ConfigAttribute添加到configAttributes里面去,getConfigAttributes

很明显是获取configAttributes

 

ConfigAttribute,其实只是一个很简单的接口,就一个方法getAttribute,其实现主要有

 

SecurityConfig后面我们将知道,其实SecurityConfig在acegi里面就是用来封装角色的:

defn.addConfigAttribute(new SecurityConfig(resourceRole.getRole()));

 

 

doFilter

 

之后每当有request的时候,调用FilterChainProxy的doFilter:

首先将请求封装成FilterInvocation fi,

然后根据fi检查是否有配置过滤器,如果无,则直接把request交给chain,结束自己的过滤

否则,调用自身的内部类,VirtualFilterChain implements FilterChain进行依次过滤

 

 

下面说说重量级的FilterSecurityInterceptor,它是典型的filter

 

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter 

 

doFilter

同样的,它首先将请求封装成FilterInvocation fi,然后调用自身的invoke(FilterInvocation fi)方法:

 

 

  public void invoke(FilterInvocation fi)
    throws IOException, ServletException
  {
    if ((fi.getRequest() != null) && (fi.getRequest().getAttribute("__acegi_filterSecurityInterceptor_filterApplied") != null) && (this.observeOncePerRequest))
    {
      fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    }
    else {
      if (fi.getRequest() != null) {
        fi.getRequest().setAttribute("__acegi_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
      }

      InterceptorStatusToken token = super.beforeInvocation(fi);
      try
      {
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
      } finally {
        super.afterInvocation(token, null);
      }
    }
  }
 

 

首先检查request里面是否有 __acegi_filterSecurityInterceptor_filterApplied属性、及自身的observeOncePerRequest(是否对每个request立即检查权限)属性,

是则——表明有权限,继续其他的过滤。

否则——先将__acegi_filterSecurityInterceptor_filterApplied设置成request的属性,然后

InterceptorStatusToken token = super.beforeInvocation(fi);

最后

super.afterInvocation(token, null);

 

然后返回InterceptorStatusToken,它标志了我们的具体过滤结果

 

关键就是super.beforeInvocation,super就是父类AbstractSecurityInterceptor,其beforeInvocation

 

签名是:

 protected InterceptorStatusToken beforeInvocation(Object object) {

可见,其对参数的宽松,不过检查却是可谓有千百道工序:

 

//获取所有的fileter定义

ConfigAttributeDefinition attr = obtainObjectDefinitionSource().getAttributes(object);

然后是SecurityContextHolder的检查

如果没异常的话是,然后是accessDecisionManager的检查。

accessDecisionManager,即授权管理器,是由__spring___设置的

调用其decide方法,看此request是否有权限,:

this.accessDecisionManager.decide(authenticated, object, attr);

如果成功的话,执行RunAsManager相关处理

Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attr);

如果RunAsManager不为null则将runAs的Authentication放入SecurityContextHolder里:

SecurityContextHolder.getContext().setAuthentication(runAs);

 

 

很明显,这个方法主要就是accessDecisionManager的decide了,它直接决定了最终的过滤结果,下面我们仔细看看这个decide的写法:

accessDecisionManager其实FilterInvocationInterceptor的一个属性

accessDecisionManager对应类AffirmativeBased,它可以理解为一个决策处理器,它又由众多voter构成(即投票者们) ,遵循一定的投票规矩完成投票,最后选出投票通过者:

通常规则:如果:“是否让全部弃权的通过”为true,那么就是说,只要没有否决票就算通过(容许弃权票),否则只有全部都是赞成票才通过(不容许弃权票)

 

通常呢,我们只要一个投票着即可:RoleVoter

RoleVoter继承AccessDecisionVoter接口:

 

public abstract interface AccessDecisionVoter
{
  public static final int ACCESS_GRANTED = 1;
  public static final int ACCESS_ABSTAIN = 0;
  public static final int ACCESS_DENIED = -1;

  public abstract boolean supports(ConfigAttribute paramConfigAttribute);

  public abstract boolean supports(Class paramClass);

  public abstract int vote(Authentication paramAuthentication, Object paramObject, ConfigAttributeDefinition paramConfigAttributeDefinition);
}

 

 

主要,也是精华,就是vote方法,他表示单个投票者voter对单个request事件具体的投票过程,理解这一点相当关键。具体是:

 

  public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config) {
    int result = 0;
    Iterator iter = config.getConfigAttributes();

    while (iter.hasNext()) {
      ConfigAttribute attribute = (ConfigAttribute)iter.next();

      if (supports(attribute)) {
        result = -1;

        for (int i = 0; i < authentication.getAuthorities().length; ++i) {
          if (attribute.getAttribute().equals(authentication.getAuthorities()[i].getAuthority())) {
            return 1;
          }
        }
      }
    }

 

—————— 获取当前登录用户的所有角色,然后跟访问目标资源所需的角色对比,只要有一个是相同的,就表示投票赞成。!!!

 

另有一个voter,AuthenticatedVoter一般用不上

 

具体的代码行进流程实在是千回百转,具体就不说了,下面说说其中一些重要的接口和类

 

AuthenticationTrustResolver接口:

 

  public abstract boolean isAnonymous(Authentication paramAuthentication);

 

  public abstract boolean isRememberMe(Authentication paramAuthentication);

实现类是:AuthenticationTrustResolverImpl

有两个重要属性:

 

    this.anonymousClass = AnonymousAuthenticationToken.class;

    this.rememberMeClass = RememberMeAuthenticationToken.class;

 

 

 

Authentication无疑是一个非常重要的接口:

 

public abstract interface Authentication extends Principal, Serializable
{
  public abstract GrantedAuthority[] getAuthorities();

  public abstract Object getCredentials();

  public abstract Object getDetails();

  public abstract Object getPrincipal();

  public abstract boolean isAuthenticated();

  public abstract void setAuthenticated(boolean paramBoolean)
    throws IllegalArgumentException;
}
 

 

感觉GrantedAuthority[]对应了当前登录人的所有角色

getCredentials是获取密码

getPrincipal是获取用户名

isAuthenticated当然就是表明当前用户是否验证通过,是否是有效用户

getDetails获取用户详细资料

 

Principal是java.security包下的类

 

说道Authentication,就不得不说GrantedAuthority,感觉它就是对应了角色

 

public abstract interface GrantedAuthority extends Serializable
{
  public abstract String getAuthority();
}

 

 

实现类GrantedAuthorityImpl,主要属性role,方法:

 

  public String getAuthority() {
    return this.role;
  }

 

 

可见角色role即权限Authority。开始的时候一直不知道,非常搞不懂acegi源码,总是糊里糊涂啊,都是这外国人的文字游戏害的......

 

 

关于Token,从google翻译上得知:

名词

象征 symbol, token, sign, emblem, byword

符记 token

代币 token

地铁硬币 token

动词

象征 symbolize, token, signify

形容词

象征性的 token

表意的 ideographic, token, notional

 

其实可以理解为标记的意思吧,acegi里面有很多的token,其实都是一些简单的属性(用户名啊密码啊是否匿名啊等)封装标记类

 

这里的token都继承AbstractAuthenticationToken,从继承于Authentication (开始真不知道!....),Authentication 前面已经讲过。

 

UsernamePasswordAuthenticationToken 很明显,封装了Username、Password

 

 public UsernamePasswordAuthenticationToken(Object principal, Object credentials, GrantedAuthority[] authorities)

 

 

AnonymousAuthenticationToken 多维护一个keyHash字段,封装了principal和对应特定匿名用户的keyHash

 

  public Object getCredentials()
  {
    return "";//以""作为credentials
  }
 

 

 

投票是由voter完成的,但投票之前的准备工作还有很多:

如何获取登录者身份?

如何获取登录者权限?

acegi提供了多种方式,一般来说数据库方式是通过JdbcDaoImpl

如何确定某个角色的具体权限?

acegi提供了多种方式,一般来说数据库方式是通过FilterInvocationDefinitionSource

通常我们需要重写上面的两个类以完成我们的客制化。

JdbcDaoImpl里面主要方法就是loadUserByUsername:

 

  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException
  {
    List users = this.usersByUsernameMapping.execute(username);

    if (users.size() == 0) {
      throw new UsernameNotFoundException("User not found");
    }

    UserDetails user = (UserDetails)users.get(0);

    List dbAuths = this.authoritiesByUsernameMapping.execute(user.getUsername());

    addCustomAuthorities(user.getUsername(), dbAuths);

    if (dbAuths.size() == 0) {
      throw new UsernameNotFoundException("User has no GrantedAuthority");
    }

    GrantedAuthority[] arrayAuths = (GrantedAuthority[])(GrantedAuthority[])dbAuths.toArray(new GrantedAuthority[dbAuths.size()]);

    String returnUsername = user.getUsername();

    if (!(this.usernameBasedPrimaryKey)) {
      returnUsername = username;
    }

    return new User(returnUsername, user.getPassword(), user.isEnabled(), true, true, true, arrayAuths);
  }

 

 

    List users = this.usersByUsernameMapping.execute(username);//查询身份

 

    UserDetails user = (UserDetails)users.get(0);//不管结果数几个,我们只取第一个,并放入表明用户信息的UserDetails 里面

 

    List dbAuths = this.authoritiesByUsernameMapping.execute(user.getUsername());//查询权限

 

 

    GrantedAuthority[] arrayAuths = (GrantedAuthority[])(GrantedAuthority[])dbAuths.toArray(new GrantedAuthority[dbAuths.size()]);//将对应角色放入GrantedAuthority[]里

 

    User是一个用户信息类,包括了(returnUsername, Password,Enabled,arrayAuths,可见它是一个完整的用户信息类

 

    addCustomAuthorities为空实现,从名字可见,它通常是有待实现的

 

通常,我们会保留有两个重要字段:

usersByUsernameQuery

authoritiesByUsernameQuery

前者,根据用户名从数据库查询密码,enabled——用于身份验证

后者,根据用户名从数据库查询role——对应所有的role,代表了对应的权限,用于授权验证

有人说查询的字段顺序是固定的,其实不然,只要查询出来,后面怎么处理是由开发者实现的。

 

//User实现了接口UserDetails

 

public abstract interface UserDetails extends Serializable
{
  public abstract GrantedAuthority[] getAuthorities();

  public abstract String getPassword();

  public abstract String getUsername();

  public abstract boolean isAccountNonExpired();

  public abstract boolean isAccountNonLocked();

  public abstract boolean isCredentialsNonExpired();

  public abstract boolean isEnabled();
}
 

 

 

FilterInvocationDefinitionSource空继承于接口ObjectDefinitionSource:

 

public abstract interface ObjectDefinitionSource
{
  public abstract ConfigAttributeDefinition getAttributes(Object paramObject)
    throws IllegalArgumentException;

  public abstract Iterator getConfigAttributeDefinitions();

  public abstract boolean supports(Class paramClass);
}
 

 

它有两个主要的实现:

 

RegExpBasedFilterInvocationDefinitionMap

PathBasedFilterInvocationDefinitionMap,

 

分别用在____的场合

 

PathBasedFilterInvocationDefinitionMap继承AbstractFilterInvocationDefinitionSource,后者又实现了FilterInvocationDefinitionSource,又继承ObjectDefinitionSource

————————A继承了B,实现了C,B又实现了C,————这是一个什么设计模式来着??????

 

getAttributes由后者实现,它获取request的url,然后将其作为参数调用前者的lookupAttributes方法,所以,可以说,差不多都是前者实现的

 

PathBasedFilterInvocationDefinitionMap有主要属性requestMap

getAttributes意思就是判断request的url是否在配置的资源里面,是则组装成ConfigAttributeDefinition返回,另外可见这个ConfigAttributeDefinition也是多用途,配置属性定义?

 

这里面涉及EntryHolder

主要封装了下面的属性:

    private ConfigAttributeDefinition configAttributeDefinition;

    private Pattern compiledPattern;

definitionSource.addSecureUrl(resources.get(i).getResource(), defn);

可以看出compiledPattern对应(resource,而ConfigAttributeDefinition对应role数组

所以EntryHolder表明了单个资源(一般就是url,带有通配符的url)及对应的有权限的角色

 

 

getAttributes:

 

public ConfigAttributeDefinition lookupAttributes(String url) {
    PatternMatcher matcher = new Perl5Matcher();

    Iterator iter = this.requestMap.iterator();

    if (isConvertUrlToLowercaseBeforeComparison()) {
      url = url.toLowerCase();

      if (logger.isDebugEnabled()) {
        logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
      }
    }

    while (iter.hasNext()) {
      EntryHolder entryHolder = (EntryHolder)iter.next();

      boolean matched = matcher.matches(url, entryHolder.getCompiledPattern());

      if (logger.isDebugEnabled()) {
        logger.debug("Candidate is: '" + url + "'; pattern is " + 

entryHolder.getCompiledPattern().getPattern() + "; matched=" + matched);
      }

      if (matched) {
        return entryHolder.getConfigAttributeDefinition();
      }
    }

    return null;
  }

 

 

可见getAttributes是返回了单条的ConfigAttributeDefinition

而getConfigAttributeDefinitions返回全部ConfigAttributeDefinition的迭代器

addSecureUrl是重要方法

 

 

ConcurrentSessionController

 

public abstract interface ConcurrentSessionController
{
  public abstract void checkAuthenticationAllowed(Authentication paramAuthentication)
    throws AuthenticationException;

  public abstract void registerSuccessfulAuthentication(Authentication paramAuthentication);
}
 

 

实现类:ConcurrentSessionControllerImpl

作为authenticationManager的sessionController属性的的concurrentSessionController,用以记录当前的session,——为什么不能用HttpSessionContextIntegrationFilter呢?

主要方法:

 

checkAuthenticationAllowed

 

 

concurrentSessionController里面真正用来记录session的其实是:SessionRegistry

 

public abstract interface SessionRegistry
{
  public abstract Object[] getAllPrincipals();

  public abstract SessionInformation[] getAllSessions(Object paramObject, boolean 

paramBoolean);

  public abstract SessionInformation getSessionInformation(String paramString);

  public abstract void refreshLastRequest(String paramString);

  public abstract void registerNewSession(String paramString, Object paramObject)
    throws SessionAlreadyUsedException;

  public abstract void removeSessionInformation(String paramString);
}
 

 

 

SessionRegistryImpl实现

 

 

 

UserDetailsService接口

 

public abstract interface UserDetailsService
{
  public abstract UserDetails loadUserByUsername(String paramString)
    throws UsernameNotFoundException, DataAccessException;
}
 

 

 

PlaintextPasswordEncoder继承BasePasswordEncoder,用来加密password

主要方法

mergePasswordAndSalt

SaltSource是一个简单的接口,但是名字奇怪!盐???

 

public abstract interface SaltSource
{
  public abstract Object getSalt(UserDetails paramUserDetails);
}
 

 

 

AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider

主要方法authenticate

 

 

ProviderManager extends AbstractAuthenticationManager

 implements AuthenticationManager

主要方法authenticate,而authenticate又调用自身的doAuthenticate

 

 

 

 

 

 

 

 

destroy

 

最后,当整个应用关闭了,是调用FilterChainProxy的destroy,它获取所有filter,分别调用各自的destroy,

 

 

分享到:
评论

相关推荐

    acegi学习整理合集

    这个“acegi学习整理合集”包含了多个关于Acegi的讨论和教程,旨在帮助开发者深入理解并有效地运用Acegi进行应用的安全管理。 首先,"学习ACEGI - - New - JavaEye论坛.mht"可能是一个关于Acegi的入门教程,涵盖了...

    acegi学习

    Acegi学习是一个深入探讨Java平台上的安全性框架的主题。Acegi是Spring Framework的早期安全模块,为基于Spring的应用程序提供了强大的身份验证和授权功能。在Java世界中,安全性和权限管理是构建任何企业级应用不可...

    Acegi学习

    Acegi学习 Acegi是Spring Security的前身,它是一个强大且灵活的安全框架,用于Java企业级应用程序。在本文中,我们将深入探讨Acegi的核心概念、功能以及如何在实际项目中应用它。 首先,我们需要理解Acegi的核心...

    spring acegi 学习心得

    前段时间复习了spring怎么做权限的技术,spring acegi 学习心得.记下来勉励自己.

    acegi学习笔记

    ### Acegi学习笔记详解 #### 一、Acegi Security概览 **Acegi Security**,作为Spring Security的前身,是一个深度融入Spring Framework的安全框架,它为开发者提供了一套全面的安全解决方案,尤其在Web应用程序中...

    Acegi学习笔记(JAVA系统安全编程时用到)

    Acegi 是一个强大的 Java 安全框架,专用于系统安全编程,尤其在处理认证和授权方面表现出色。在本文中,我们将深入探讨 Acegi 的基本概念、如何设置以及它如何与 Spring 框架集成。 首先,让我们了解 Acegi 的核心...

    Acegi学习笔记--Acegi详解实战Acegi实例

    通过学习Acegi,我们可以了解到Web应用安全的基本思路和实践方法,这对于理解现代的Spring Security框架非常有帮助。虽然Acegi已经不再更新,但它的理念和架构仍对现代安全框架设计产生深远影响。

    acegi学习指南以及例子

    在本文中,我们将深入探讨Acegi的学习指南,通过实例来理解其核心概念和功能。 首先,我们需要了解Acegi的基础概念。Acegi的核心组件包括SecurityContext、Authentication、Authorization和FilterChainProxy。...

    acegi资料大全-全集

    `Acegi学习小结-Spring-Java -JavaEye做最棒的软件开发交流社区.mht`可能是一个社区成员的学习笔记,汇总了他在使用Acegi过程中遇到的问题和解决办法,这类资料往往能提供实战中的宝贵经验。 `学习Acegi-认证...

    Acegi例子代码+一个很好的学习Acegi的网址

    这个压缩包包含了Acegi的示例代码和一个学习资源,对于初学者来说是非常宝贵的资料。 首先,让我们深入理解Acegi的核心概念: 1. **身份验证(Authentication)**:Acegi允许你实现自定义的身份验证机制,这包括...

    acegi

    Acegi 是一个在Java开发领域,特别是Spring框架中曾经广泛使用的安全组件,全称为Acegi Security。...学习Acegi可以帮助我们更好地理解Spring Security的工作原理,从而提升我们的应用安全开发能力。

    acegi pdf 学习

    由于文章内容是关于acegi pdf学习的参考文档,其中包含了大量关于Acegi安全系统的技术细节,因此以下将详细阐述文档中提及的关键知识点。 首先,Acegi安全系统是一个基于Spring框架的安全解决方案。文档开头简要...

    基于java的ACEGI

    AceGI,全称为Acegi ...理解Acegi对于学习和使用Spring Security仍然大有裨益,因为它可以帮助我们更好地理解Web应用程序的安全设计和实现。在实际开发中,掌握Acegi的相关知识可以提升我们构建安全系统的专业能力。

    acegi——笔记学习

    通过深入学习Acegi,你可以了解Spring Security的基本架构和原理,这对于理解现代的Spring Security配置和使用非常有帮助。尽管Acegi已不再更新,但其思想和技术仍在Spring Security中得到沿用和发展。如果你正在...

    acegi权限控制学习笔记

    Acegi权限控制学习笔记 Acegi安全框架是Spring Security的前身,它提供了一种强大的、灵活的、基于组件的安全解决方案,用于实现企业级应用的安全控制。在这个学习笔记中,我们将探讨两个关键点:身份认证成功后的...

    Acegi使用.pdf

    - **学习曲线**:对于初次接触的开发者而言,Acegi的复杂性和灵活性可能会带来较高的学习成本。 #### 结论 Acegi安全框架凭借其独特的设计和高度的灵活性,成为了Spring框架下处理安全问题的强大工具。通过深入...

    Acegi-spring安全框架

    然而,尽管Acegi非常强大,它也有不足之处,如学习曲线较陡峭,配置复杂,对于初学者来说可能较为困难。此外,随着Spring Security的发展,Acegi的一些功能可能已经被更新的版本替代或优化,因此在使用时需要考虑其...

    spring acegi 使用工程demo

    Spring Acegi是一个安全框架,它为Spring应用提供了全面的安全管理功能。...通过学习和实践这个demo,开发者可以深入理解Spring安全框架的核心概念,并能够将其应用到自己的项目中,增强应用的安全性。

    学习Acegi-认证 文档

    ### Acegi认证服务详解 #### 一、Acegi简介与背景 Acegi是Spring Security的前身,是一款基于Spring框架的安全管理工具,旨在为应用程序提供安全控制...希望本文能够为正在学习或使用Acegi的同学提供有价值的参考。

Global site tag (gtag.js) - Google Analytics