若依登录认证(后端)

0. SpringSecurity知识点

若项目中加入了SpringSecurity的依赖,会默认对项目的方法都提供保护。

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

启动程序的时候,会自动弹出登录认证页面

在这里插入图片描述
这个过程的大致流程如图所示,弹窗认证功能是由UsernamePasswordAuthenticationFilter过滤器提供的

在这里插入图片描述

SpringSecurity有很多过滤器,但是核心的过滤器主要有三个:

  • UsernamePasswordAuthenticationFilter:用户名密码认证过滤器。

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

  • ExceptionTranslationFilter:异常转换过滤器。

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

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

SpringSecurity的基础认证流程

在这里插入图片描述

这是默认的认证流程,这个过程中UsernamePasswordAuthenticationFilter会调用上述的认证界面对用户进行用户名和密码的认证,并且InMemoryUserDetailsManager是在内存中进行相关操作的,这两点和实际开发不太符合。

我们可以自定义login接口,在这个方法中手动调用AuthenticationManager的authenticate方法。由于默认的loadUserByUsername是对内存操作的,我们可以改写该方法,查询数据库中用户名及密码等相关信息

自定义的认证流程

在这里插入图片描述

1. 认证

登录校验

自定义登录接口 login,在这个方法中手动调用AuthenticationManager的authenticate方法对用户进行认证。
/**
	自定义登录接口 login
		在这个方法中手动调用AuthenticationManager的authenticate方法对用户进行认证
*/
loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),loginBody.getUuid());

若验证码校验通过并且用户名和密码前置校验通过后,会调用AuthenticationManager的authenticate方法进行用户认证

@Resource
private AuthenticationManager authenticationManager;

// TODO 验证码校验 登录前置校验
 
...
    
try{
        /*
            封装Authentication对象
            把用户的用户名和密码传入,方便后续authenticationManager(该对象的authenticate传入的对象是Authentication authentication)就行认证
        */    
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        //存储Authentication对象  ThreadLocal 存数据
        AuthenticationContextHolder.setContext(authenticationToken);
        // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername(自定义的loadUserByUsername方法)
        authentication = authenticationManager.authenticate(authenticationToken);
}
catch()
    ...
    //TODO 异常捕捉
finally
{
    /**
      * ThreadLocal确保了每个线程使用一个认证对象,不用每次使用都传递 Authentication 认证对象了,但是在本次线程完必须清除数据,
      * (因为spring使用的线程池,请求完不会销毁线程,回到线程池由下一个请求继续使用,防止携带'前世'数据,本次请求完需销毁)
     */
    //ThreadLocal 销毁
    AuthenticationContextHolder.clearContext();
}

数据库校验

​ 未重写的loadUserByUsername是对内存进行处理,重写后的loadUserByUsername可以根据自身的业务逻辑进行编写,先获取数据库中该用户名的相关信息,再对密码的有效性校验,最后返回UserDetails对象

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询 该用户的用户信息、部门信息、角色信息
        SysUser user = userService.selectUserByUserName(username);
        //抛出的ServiceException由GlobalExceptionHandler中的handleServiceException捕捉
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException(MessageUtils.message("user.not.exists"));
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException(MessageUtils.message("user.password.delete"));
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException(MessageUtils.message("user.blocked"));
        }
        //校验用户密码的正确性
        passwordService.validate(user);

        //返回UserDetails对象
        return createLoginUser(user);
    }

在这里插入图片描述

    public void validate(SysUser user)
    {
        //获取用户登入时 填写的用户名和密码
        Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
        String username = usernamePasswordAuthenticationToken.getName();
        String password = usernamePasswordAuthenticationToken.getCredentials().toString();

        //记录用户登录次数
        Integer retryCount = redisCache.getCacheObject(getCacheKey(username));

        if (retryCount == null)
        {
            retryCount = 0;
        }

        if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                    MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
            throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
        }
        //密码和用户名 不匹配 (user是数据库获取的,密码是加密过的)
        if (!matches(user, password))
        {
            retryCount = retryCount + 1;
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
                    MessageUtils.message("user.password.retry.limit.count", retryCount)));
            //密码不正确 登录次数+1
            redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
            throw new UserPasswordNotMatchException();
        }
        else
        {   //用户名和密码匹配 清空redis中的缓存
            clearLoginRecordCache(username);
        }
    }

jwt认证过滤器

​ login方法完成了上述用户名和密码的验证后,生成token再把这个token返回给客户端,并该用户token作为key,其权限相关信息作为vaule存放再redis中。前端请求数据时携带token能够快速进行身份验证。

public String login(String username, String password, String code, String uuid)
{
    //TODO 登录认证
    ....
    //获取当前登录用户的信息
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    // 生成token
    return tokenService.createToken(loginUser);
}        
    public String createToken(LoginUser loginUser)
    {
        //生成uuid作为token
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        //设置用户代理信息
        setUserAgent(loginUser);
        //更新redis中的信息 以token作为key loginUser作为value
        refreshToken(loginUser);

        //{"login_user_key":uuid} jwt编码生成返回给前端的token
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

​ 当用户登录成功后,redis保存了token(uuid):loginUser这样的键值对信息,当前端携带token发送请求时,需要在SecurityConfig设置过滤器(即在UsernamePasswordAuthenticationFilter过滤器对用户信息认证前验证token的有效性

SecurityConfig

/**
 * token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
	//TODO ...其他设置
    // 添加JWT filter  把JwtAuthenticationTokenFilter对象添加在UsernamePasswordAuthenticationFilter过滤器前
    httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}

JwtAuthenticationTokenFilter

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        //解析token 获取
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            //验证令牌有效期 相差不足20分钟,自动刷新缓存
            tokenService.verifyToken(loginUser);
            //存入SecurityContextHolder  loginUser.getAuthorities()获取权限信息
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            // 放入Authentication对象 
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        //放行
        //(如果没有token)未登录状态的时候 放行会被后面的security拦截下来进行登录相关操作(后面的过滤器会对其进行处理)
        chain.doFilter(request, response);
    }

退出登录

​ 退出登录同样在SecurityConfig中配置

SecurityConfig

/**
  * 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
	//TODO ...其他设置
    // 添加Logout filter
    httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
}
Logo

快速构建 Web 应用程序

更多推荐