Spring Security 表单认证实现及逻辑解析

1. 表单认证

1.1.Pom依赖

<dependency>
    <groupId>org.springframework.social</groupId>
    <artifactId>spring-social-security</artifactId>
    <version>1.1.6.RELEASE</version>
</dependency>

1.2.实现SpingSecurity的安全适配器类

实现接口:WebSecurityConfigurerAdapter

简单实现configure方法:
protected void configure(HttpSecurity http) throws Exception {
        http
            .formLogin()//表单登录方式
                .loginPage(url)//执行登录跳转的controller的URL
                .loginProcessingUrl(url)//验证登录URL
                .successHandler(class)//表单登录成功以后的响应
                .failureHandler(class)//表单登录失败以后的响应
                .and()  
            .authorizeRequests()//权限控制
                .antMatchers(url,url).permitAll()//无需权限可访问的url      
                .anyRequest().authenticated()//其余访问皆需要权限
                .and()
            .csrf().disable()//关闭csrf攻击防护
            ;   
    }

1.3.处理用户信息校验逻辑

实现接口:
  UserDetailsService:获取用户信息逻辑处理
  UserDetails:处理用户逻辑
  PasswordEncoder:处理用户密码加密逻辑

public class CustomUserDeatilsService implements UserDetailsService{
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    /**
     * 处理用户信息校验逻辑
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名得到用户信息,返回给userDetails接口的实例化user对象
        // AuthorityUtils.commaSeparatedStringToAuthorityList字符串转换权限collection对象
        //passwordEncoder.encode:该方法在注册时使用,实际上直接在数据库中拿出密码匹配
        String password = passwordEncoder.encode("123456");
        return new User(username,password, true,true,true,true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

1.4.认证成功与失败处理逻辑

实现接口:
  AuthenticationFailureHandler
  AuthenticationSuccessHandler

public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
            //实现跳转登录页显示响应信息
            super.onAuthenticationFailure(request,response,exception);      
}
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
    
    @Autowired
    private ObjectMapper objectMapper;
    
    //响应处理方式为json格式,返回json
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
            //将authentication对象转换成json格式的字符串,写回响应里
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        
    }

}


2.SpringSecurity原理解析


2.1.基本原理

2.1.1.过滤器基本原理图

过滤器机制

过滤器工作原理

2.1.2.SpringSecurity基本原理图

SpringSecurity实质是一组过滤器链的集合。

SpringSecurity基本原理

2.1.3.过滤器作用

  • SecurityContextPersistenceFilter:用于设置SecurityContext到SecurityContextHolder中,以保证获取的认证结果在多个请求之间共享
  • UsernamePasswordAuthenticationFilter:对于表单登陆进行验证的处理逻辑
  • ExceptionTranslationFilter:解决在处理一个请求时产生的指定异常
  • FilterSecurityInterceptor:校验登陆信息的权限校验

2.1.4.大体认证流程

  1. 当用户发送request请求,首先经过SecurityContextPersistenceFiltersecurity过滤器,Context从SecurityContextRepository创建一个securityContext给以后的过滤器使用
  2. 各类认证过滤器根据各自认证逻辑对用户进行认证处理,此时用户请求被保存,跳转到指定过滤器的请求上,例如UsernamePasswordAuthenticationFilter对表单登录逻辑进行认证处理(request="/login",method="POST")
  3. 在FilterSecurityInterceptor进行用户识别和权限的校验,认证失败,交由ExceptionTranslationFilter根据指定的异常进行处理。
  4. 认证成功,则重定向到用户请求,访问指定的资源

2.2.以表单认证逻辑演示springsecurity的认证处理逻辑

2.2.1.主要涉及组建

接口 实现类(接口) 方法 作用描述
AbstractAuthenticationProcessingFilter UsernamePasswordAuthenticationFilter attemptAuthentication(HttpServletRequest request,HttpServletResponse response) 接收用户名密码并创建响应token,组装Authentication
AuthenticationManager ProviderManager authenticate(Authentication authentication) 对认证进行管理,并分发给指定的provider类校验认证信息
AuthenticationProvider(AbstractUserDetailsAuthenticationProvider) DaoAuthenticationProvider 1.authenticate(Authentication authentication) 2.additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) 3.createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) 校验用户的凭证信息,失败会抛出一个特定的异常,成功则重新填充一个指定的token(UsernamePasswordAuthenticationToken);三个方法作用:1.校验逻辑处理2.校验密码是否匹配3.检验成功组装一个新的token
UserDetailsChecker DefaultPreAuthenticationChecks和DefaultPostAuthenticationChecks(在AbstractUserDetailsAuthenticationProvider抽象类中的私有类) check(UserDetails user) 1.预检查(账户是否锁定,账户是否过期,该账户是否存在)2.后检查(密码是否过期)
AbstractAuthenticationToken(Authentication) UsernamePasswordAuthenticationToken - Authentication对象,保存用户的详细信息,用于spring security执行逻辑使用
UserDetailsService CustomUserDetailsService(自己实现) loadUserByUsername(String username) 获取用户信息
UserDetails user() - 封装用户信息的实体类,存储从后台获取的用户信息

2.2.2.逻辑流程图

SpringSecurity 表单认证流程.png

2.2.3.代码解析

  • 表单形式登录,发送request请求

  • 当spring security执行时发现该请求用户并未通过认证,会将request请求引导到AbstractAuthenticationProcessingFilter中的request(url="/login",method=POST)
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        ....  ....
        Authentication authResult;
        try {
            // 用户名密码验证处理逻辑,最后获取一个Authentication
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        catch (AuthenticationException failed) {
            // 认证失败处理逻辑
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        ....  ....
        //认证成功处理逻辑
        successfulAuthentication(request, response, chain, authResult);
    }

此过滤器执行doFilter()处理逻辑,主要执行三个主要方法
①. attemptAuthentication(request, response);
②. unsuccessfulAuthentication(request, response, failed)
③. successfulAuthentication(request, response, chain, authResult)

①. attemptAuthentication(request, response)
由其实现类UsernamePasswordAuthenticationFilter的 attemptAuthentication()方法实现

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();
        //将用户名密码封装成Token    
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        setDetails(request, authRequest);
        //将token传递给AuthenticationManager执行认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }

在该类中attemptAuthentication方法主要将请求的用户名密码封装成UsernamePasswordAuthenticationToken对象,并交给AuthenticationManager处理认证逻辑;
AuthenticationManager管理着不同认证的处理逻辑,相当于一个认证的管理者,不同认证需要不同的处理逻辑,而由其实现类ProviderManager来分配具体的认证验证逻辑。

②. unsuccessfulAuthentication(request, response, failed)

protected void unsuccessfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {    
        //清空保存在securitycontext中的用户数据
        SecurityContextHolder.clearContext();
        ....  ...
        rememberMeServices.loginFail(request, response);
        //失败的逻辑处理
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

此方法主要完成了两个动作,1.认证失败后将securitycontext中的用户信息删除。2.将错误信息交给接口AuthenticationFailureHandler来完成对浏览器的响应,我们可以继承该接口及其实现类来完成对响应的操作

③. successfulAuthentication(request, response, chain, authResult)

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        //将认证成功的用户信息Authention保存到线程级的SecurityContextHolder的SecurityContext中,以便后面过滤器使用
        SecurityContextHolder.getContext().setAuthentication(authResult);
        rememberMeServices.loginSuccess(request, response, authResult);
        ....  .....
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

此方法主要完成了两个动作,1.将认证成功的Authention保存下来,并写入securitycontext中去。2.将Authention交给接口AuthenticationSuccessHandler来完成对浏览器的响应,可以继承该接口及其实现类来完成对响应的操作


  • 封装成UsernamePasswordAuthenticationToken的用户信息交给AuthenticationManager来执行具体用户信息校验,由其实现类ProviderManager来具体实现
public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        .... ....
        Authentication result = null;
        .... ....
         //遍历所有的AuthenticationProvider的实现,找到其实现类中可以解析Authenticaion用户信息的provider
        for (AuthenticationProvider provider : getProviders()) {
        .... ....
            try {
                //具体执行方法
                result = provider.authenticate(authentication);
            }   
        ....  ....  
        if (result == null && parent != null) {
            try {
                result = parent.authenticate(authentication);
            }
        ....  .....
        eventPublisher.publishAuthenticationSuccess(result);
        return result;
        }
    }

在该方法中主要遍历接口AuthenticationProvider的所有实现类,找到能够解析Authentication的逻辑,并进行用户信息校验。在该案例中,主要由抽象类AbstractUserDetailsAuthenticationProvider以及其实现类DaoAuthenticationProvider完成验证


  • 当ProviderManager遍历出了合适的处理逻辑,即将Authentication传递给它进行具体的逻辑校验,本案例由AbstractUserDetailsAuthenticationProvider完成
public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        .... .....
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            try {
                //该方法查询后台,获取用户信息(例.数据库中存放的)
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }   
            .... ....  
        }
        try {
            //预检查:检查账户是否锁定,过期和存在
            preAuthenticationChecks.check(user);
            //附加检查:密码是否匹配
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        ....  ....
        //后检查:密码是否过期
        postAuthenticationChecks.check(user);
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

provider主要在authenticate执行验证,设计四个具体流程方法。
1.获取用户信息UserDetails
  ①. retrieveUser(username,authentication)
2.对用户信息UserDetails进行校验
  ②. preAuthenticationChecks.check(user)
  ③. additionalAuthenticationChecks(user,authentication)
  ④. postAuthenticationChecks.check(user)

①. retrieveUser(username,authentication)

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;
        try {
            //根据username向后台请求用户信息
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        }
        .... ....
        return loadedUser;
    }

该方法在DaoAuthenticationProvider中具体执行,通过UserDetailsService类执行获取用户信息逻辑,并封装成UserDetails接口的实现类。该方法由编程人员实现。

②. preAuthenticationChecks.check(user)

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            //账户是否被锁定
            if (!user.isAccountNonLocked()) {
                logger.debug("User account is locked");
                throw new LockedException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"));
            }
            //账户是否存在
            if (!user.isEnabled()) {
                logger.debug("User account is disabled");
                throw new DisabledException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.disabled",
                        "User is disabled"));
            }
            //账户上是否过期
            if (!user.isAccountNonExpired()) {
                logger.debug("User account is expired");
                throw new AccountExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.expired",
                        "User account has expired"));
            }
        }
    }

该方法在AbstractUserDetailsAuthenticationProvider抽象类中的私有类中实现。主要判断账户是否被锁定,过期和存在。

③. additionalAuthenticationChecks(user,authentication)

    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        Object salt = null;
        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
                presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

该方法在DaoAuthenticationProvider中实现,主要验证匹配密码

④. postAuthenticationChecks.check(user)

private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                logger.debug("User account credentials have expired");
                throw new CredentialsExpiredException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                        "User credentials have expired"));
            }
        }
    }

该方法在AbstractUserDetailsAuthenticationProvider抽象类中的私有类中实现。主要判断密码是否过期。

由以上流程中可以看出我们可实现部分
①实现UserDetailsService以及UserDetails接口,完成获取并验证用户信息的逻辑
②实现AuthenticationFailureHandler接口,完成登陆失败的逻辑处理
③实现AuthenticationSuccessHandler接口,完后登录成功的逻辑处理



2.3.Authentication解析以及实现在多个请求中共享


2.1.Authentication

Authentication存储了用户的详细信息,包括唯一标识(如用户名)、凭证信息(如密码)以及本用户被授予的一个或多个权限.开发人员通常会使用Authentication对象来获取用户的详细信息,或者使用自定义的认证实现以便在Authentication对象中增加应用依赖的额外信息

Authentication接口可以实现的方法:

方法签名 描述
Object getPrincipal() 返回安全实体的唯一标识(如,一个用户名)
Object getCredentials() 返回安全实体的凭证信息
List<GrantedAuthority>getAuthorities() 得到安全实体的权限集合,根据认证信息的存储决定的
Object getDetails() 返回一个跟认证提供者相关的安全实体细节信息

2.2.SecurityContext

参考:

  1. Spring Security 认证系统浅析:http://z276356445t.iteye.com/blog/2372689
  2. Spring Security 的过滤器机制:Spring Security 的过滤器机制
  3. Spring Security技术栈开发企业级认证与授权
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容