SpringSecurity入门

一、概述

        SpringSecurity是基于Spring的一个安全管理框架,他提供一般Web项目所需要的认证授权功能。
        认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

        授权:经过认证后判断当前用户是否有权限进行某个操作

引入SpringSecurity:

        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

二、认证

2.1 登录校验流程

        实际的前后端分离项目中,我们的整个登录校验流程如下所示,其核心是使用token令牌对客户进行认证。

2.2 认证原理 

        SpringSecurity的原理其实就是一个过滤器链内部包含了提供各种功能的过滤器。如下所示:

         上图只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。上图中所示的核心过滤器功能如下:

        UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。

        ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

        FilterSecurityInterceptor: 负责权限校验的过滤器。

 2.3 认证流程详解

 概念速查:

        Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息

        AuthenticationManager接口:定义了认证Authentication的方法

        UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

        UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中

        DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider抽象类,而AbstractUserDetailsAuthenticationProvider抽象类又实现了AuthenticationProvider这个接口。

        AuthenticationProvider接口和AuthenticationManager接口都有 authenticate() 这个方法.

认证流程:

        1、传入用户名和密码

        2、UsernamePasswordAuthenticationFilter(前后端分离项目,该类可以替换成自己写的类MyUsernamePasswordAuthenticationFilter,来调用AuthenticationManager接口的authenticate()方法)会把用户名和密码封装成Authentication对象

        3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了AuthenticationProvider接口抽象类的authenticate()方法进行认证。

        4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

        5、DaoAuthenticationProvider类继承了抽象类AbstractUserDetailsAuthenticationProvider并在重写方法retrieveUser()里调用了loadUserByUsername()方法

        6、loadUserByUsername()方法会返回UserDetails对象,认证成功后逐一返回上一层

2.5 前后端登录思路

登录:

  1. 自定义登录接口 ;
  2. 调用ProviderManager的方法,然后进一步调用DaoAuthenticationProvider方法进行认证;
  3. 自定义UserDetailsService,并在其LoadUserByUsername方法中查询数据库,
  4. 将查询数据库的结果返回到DaoAuthenticationProvider方法,并通过PasswordEncoder进行密码校验,如果认证通过生成jwt把用户信息存入redis中,
  5. 最终将生成的token令牌传输给前端。

校验:

  • 定义Jwt认证过滤器:
  1. 获取token
  2. 解析token获取其中的userid
  3. 从redis中获取用户信息

三、认证示例 

3.1 SpringSecurity配置类:

/**
 * spring security配置
 *
 */
@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();
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

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

    /**
     * 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
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/register", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

3.2 认证流程源码

        上文中,详细叙述了SpringSecurity登录认证的流程,本节按照上述流程,以一个小案例,进一步对上述流程进行说明。

1. 编写登录接口:

    @PostMapping("/login")
    public ResponseResult login(@RequestBody LoginBody loginBody) {
        ResponseResult ajax = ResponseResult.success();
        // 登录验证并生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        if(StringUtils.isNull(token)){
            return ResponseResult.error("登录失败,请检查用户名和密码,并确认账号是否过期!");
        }
        ajax.put(Constants.TOKEN, token); // 响应添加token令牌
        return ajax; // 返回响应值(包含Token令牌)。
    }

        在上述登录接口中,主要是调用了loginService.login()函数,函数传入登录用户及密码,执行登录流程,如下所示:

2. 编写MyUsernamePasswordAuthenticationFilter类,调用AuthenticationManager接口的authenticate()方法:

    public String login(String username, String password, String code, String uuid) {
        // 用户验证
        Authentication authentication = null;
        try {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e) {
            return null;
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenService.createToken(loginUser); // 根据用户信息,创建令牌
    }

        上述函数中,我们调用了AuthenticationManager接口的authenticate()方法进行认证。authenticate()方法如下所示:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       ...

                try {
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (AuthenticationException var14) {
                    lastException = var14;
                }
            }
        ...
    }

在该方法中,又调用了AuthenticationProvider接口实现类AbstractUserDetailsAuthenticationProvider的authenticate()方法。

3. 调用AuthenticationProvider接口实现类的authenticate()方法:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ...
            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;
            }

            
        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }
        }

        ...

                在AuthenticationProvider接口实现类AbstractUserDetailsAuthenticationProvider的authenticate()方法中:又调用了AbstractUserDetailsAuthenticationProvider类的子类DaoAuthenticationProvider类中的retrieveUser()方法:

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

         在retrieveUser()方法 中,调用了UserDetailsService的实现类UserDetailsServiceImpl的方法loadUserByUsername()UserDetailsServiceImpl类是由开发者自己实现的,并在方法loadUserByUsername()中编写操作数据库获取用户信息的代码,如下所示:

4. 编写UserDetailsService接口实现类,并重写loadUserByUsername()方法:

@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    @Autowired
    private ISysUserService userService;

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

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser( user, null);
    }

}

        loadUserByUsername() 方法返回值为UserDetails,该类封装了从数据库中查询到的用户信息。然后返回到DaoAuthenticationProvider类中的retrieveUser()方法,接着进一步返回到AbstractUserDetailsAuthenticationProviderauthenticate()方法中,在该方法中,调用该类的方法additionalAuthenticationChecks():

    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

        在调用函数 additionalAuthenticationChecks()时,将从数据库中查询到的用户信息UserDetails ,以及前端传入的用户信息UsernamePasswordAuthenticationToken 作为参数传入,函数内部通过调用passwordEncoder.matches()方法,来判断两者密码是否相等。并进一步确认用户是否通过认证,若密码不相同,则抛出BadCredentialsException异常,否则继续执行,返回到ProviderManager类(AuthenticationManager接口)authenticate()方法,并进一步返回到login()方法

5. 验证成功,计算token

        密码比对一致之后,层层返回,会再次返回到初始的login()方法继续执行该方法,计算出token值,存储到redis数据库中,并响应给前端,作为后续访问的依据。

 

 
 
 
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值