• Spring Security 认证应用及执行流程


    一、应用流程

    此处以若依框架的用户认证部分来分析Spring Security的认证应用。

    登录的Controller:

    /**
         * 登录方法
         * 
         * @param loginBody 登陆信息
         * @return 结果
         */
        @PostMapping("/login")
        public AjaxResult login(@RequestBody LoginBody loginBody)
        {
            AjaxResult ajax = AjaxResult.success();
            // 生成令牌
            String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                    loginBody.getUuid());
            ajax.put(Constants.TOKEN, token);
            return ajax;
        }

    service层代码:

        public String login(String username, String password, String code, String uuid)
        {
            ...// 用户验证
            Authentication authentication = null;
            try
            {
                // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
                authentication = authenticationManager
                        .authenticate(new UsernamePasswordAuthenticationToken(username, password));
          ...// 生成token
            return tokenService.createToken(loginUser);
        }

    其中authenticationManager是在SecurityConfig中注入的,如下:

     @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception
        {
            return super.authenticationManagerBean();
        }

    SecurityConfig的完整代码如下:

    package com.ruoyi.framework.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
    import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
    import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
    
    /**
     * spring security配置
     * 
     * @author ruoyi
     */
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter
    {
        /**
         * 自定义用户认证逻辑
         */
        @Autowired
        private UserDetailsService userDetailsService;
        
        /**
         * 认证失败处理类
         */
        @Autowired
        private AuthenticationEntryPointImpl unauthorizedHandler;
    
        /**
         * 退出处理类
         */
        @Autowired
        private LogoutSuccessHandlerImpl logoutSuccessHandler;
    
        /**
         * token认证过滤器
         */
        @Autowired
        private JwtAuthenticationTokenFilter authenticationTokenFilter;
        
        /**
         * 解决 无法直接注入 AuthenticationManager
         *
         * @return
         * @throws Exception
         */
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception
        {
            return super.authenticationManagerBean();
        }
    
        /**
         * anyRequest          |   匹配所有请求路径
         * access              |   SpringEl表达式结果为true时可以访问
         * anonymous           |   匿名可以访问
         * denyAll             |   用户不能访问
         * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
         * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
         * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
         * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
         * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
         * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
         * permitAll           |   用户可以任意访问
         * rememberMe          |   允许通过remember-me登录的用户访问
         * authenticated       |   用户登录后可访问
         */
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception
        {
            httpSecurity
                    // CRSF禁用,因为不使用session
                    .csrf().disable()
                    // 认证失败处理类
                    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                    // 基于token,所以不需要session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    // 过滤请求
                    .authorizeRequests()
                    // 对于登录login 验证码captchaImage 允许匿名访问
                    .antMatchers("/login", "/captchaImage").anonymous()
                    .antMatchers(
                            HttpMethod.GET,
                            "/*.html",
                            "/**/*.html",
                            "/**/*.css",
                            "/**/*.js"
                    ).permitAll()
                    .antMatchers("/profile/**").anonymous()
                    .antMatchers("/common/download**").anonymous()
                    .antMatchers("/common/download/resource**").anonymous()
                    .antMatchers("/swagger-ui.html").anonymous()
                    .antMatchers("/swagger-resources/**").anonymous()
                    .antMatchers("/webjars/**").anonymous()
                    .antMatchers("/*/api-docs").anonymous()
                    .antMatchers("/druid/**").anonymous()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                    .and()
                    .headers().frameOptions().disable();
            httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
            // 添加JWT filter
            httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        }
    
        
        /**
         * 强散列哈希加密实现
         */
        @Bean
        public BCryptPasswordEncoder bCryptPasswordEncoder()
        {
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 身份认证接口
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception
        {
            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
        }
    }
    SecurityConfig

    最终经过一系列处理,到数据库的用户认证要走到如下逻辑:

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService
    {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
        {
            SysUser user = userService.selectUserByUserName(username);
            if (StringUtils.isNull(user))
            {
                log.info("登录用户:{} 不存在.", username);
                throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
            }
            else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
            {
                log.info("登录用户:{} 已被删除.", username);
                throw new BaseException("对不起,您的账号:" + username + " 已被删除");
            }
            else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
            {
                log.info("登录用户:{} 已被停用.", username);
                throw new BaseException("对不起,您的账号:" + username + " 已停用");
            }
    
            return createLoginUser(user);
        }
    }

    而UserDetailsServiceImpl在SecurityConfig中是这样配置进去的:

    /**
         * 身份认证接口
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception
        {
            auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
        }

    那看完上边这个应用流程后,还有两个疑问:

    1、是如何调用到UserDetailsServiceImpl里的loadUserByUsername;

    2、loadUserByUsername方法中只校验了用户名是否存在,并没有校验密码是否正确,密码的校验又在哪里呢。

    二、执行流程

    下面来看下这两个问题:

    经过代码断点跟踪,代码会走到如下地方(当然前边还有一些代码逻辑,本次仅从上述2个问题去重点跟部分代码):

    AbstractUserDetailsAuthenticationProvider类的authenticate方法:

    public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
           ...try {
              //此方法会去获取用户信息,会一步步调用到UserDetailsServiceImpl里的loadUserByUsername方法 user
    = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } try { preAuthenticationChecks.check(user);
            //此方法中会去校验用户的密码 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } ...
    return createSuccessAuthentication(principalToReturn, authentication, user); }
    retrieveUser方法实际调用的地方,DaoAuthenticationProvider类中:
    protected final UserDetails retrieveUser(String username,
                UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
             ...
    //调用我们自定义的UserDetailsServiceImpl进行校验
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 
    ...

    }
    additionalAuthenticationChecks方法实际调用的地方,DaoAuthenticationProvider类中:
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            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.matches(presentedPassword, userDetails.getPassword())) {
                logger.debug("Authentication failed: password does not match stored value");
    
                throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }
        }

    以上就是Spring Security认证的简单流程,采用默认的AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider(都是AuthenticationProvider接口的实现类)来实现。

    另外:我们也可以自定义类实现AuthenticationProvider接口来完全自定义用户的认证过程。



  • 相关阅读:
    啃掉的博文全记录
    DP五十题
    noip 真题班刷题记录及总结思考
    dfklsJj
    【2018.11.7】luogu NOIp热身赛 及刷题思考
    【trie树专题】
    【倍增专题】
    10.23
    简析 NP 问题 和P问题
    [NOIP 2010普及组 No.4] 三国游戏
  • 原文地址:https://www.cnblogs.com/silenceshining/p/15795460.html
Copyright © 2020-2023  润新知