**

一. 登录

一.结合redis的验证码

页面打开就加载一个验证码信息,其中包括一个uuid 和一个验证码,将其uuid加上前缀然后存入到redis中,将uuid的大概信息当作key,将验证码结果当中value。最后设置其过期时间,一般也就是一两分钟,使用不需要担心验证码点击过多。难度很低,但是使用了redis,可以学习其思维

登录redis 容器 redis实现登录_java


登录redis 容器 redis实现登录_验证码_02

二.登录流程

这边比较特殊,我原本项目是结合的shiro+oauth2,这边使用的是spring-security ,我感觉两个框架的流程差不多,首先spring-security 将传入的信息封装到UsernamePasswordAuthenticationToken这个类里面,其核心验证方法为其内部类DaoAuthenticationProvider中的retrieveUser方法的 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 这一段,若依这边实现了其UserDetailsService 重写了其loadUserByUsername 根据其用户名信息查询数据库查出一个UserDetail的实体类,最后通过DaoAuthenticationProvider的additionalAuthenticationChecks方法来比对两个类里面的密码是否一致。一旦登录成功就获取一个uuid将其加密返回为token(jwt非对称加密)(),若依这里还将一些用户登录信息保存到了redis中,key值为token加上一段拼接string,默认有效期为30分钟。若依这里还放入redis的信息中包含了很多系统有关的内容,后续开发登录信息管理时我觉得应该是那里用到了

登录redis 容器 redis实现登录_redis_03


登录redis 容器 redis实现登录_验证码_04


补充一个登录验证方法,就是单独实现了一下验证逻辑,将其注入spring容器中,登录会走这个方法

/**
 * 登录校验方法
 * 
 * @author ruoyi
 */
@Component
public class SysLoginService
{
    @Autowired
    private TokenService tokenService;

    @Resource
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    /**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisCache.getCacheObject(verifyKey);
        redisCache.deleteObject(verifyKey);
        if (captcha == null)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
        // 用户验证
        Authentication authentication = null;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new CustomException(e.getMessage());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenService.createToken(loginUser);
    }
}

三. 前端登录流程

本人前端做的不多了解不深,使用简单描述一下,首先就是发生请求,在这个方法内还做了有没有记住密码的操作,在

this.$store.dispatch(“Login”, this.loginForm)这一段中发送了请求,ctrl加点击“Login”进入下一步

登录redis 容器 redis实现登录_redis_05


这一步是发送请求做的操作

登录redis 容器 redis实现登录_验证码_06


一旦登录成功就会被一个钩子函数所拦截,是在src下面的permission.js的,这段代码就是登录成功后一些操作,其中也获取了用户信息

router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(res => {
          // 拉取user_info
          const roles = res.roles
          store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {
            // 根据roles权限生成可访问的路由表
            router.addRoutes(accessRoutes) // 动态添加可访问路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          })
        }).catch(err => {
            store.dispatch('LogOut').then(() => {
              Message.error(err)
              next({ path: '/' })
            })
          })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

最终会调用这个方法,用户权限和用户的一些基本信息就是在这里获取的

// 获取用户信息
    GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        getInfo(state.token).then(res => {
          const user = res.user
          const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
          if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', res.roles)
            commit('SET_PERMISSIONS', res.permissions)
          } else {
            commit('SET_ROLES', ['ROLE_DEFAULT'])
          }
          commit('SET_NAME', user.userName)
          commit('SET_AVATAR', avatar)
          resolve(res)
        }).catch(error => {
          reject(error)
        })
      })
    },

四. 获取用户信息的后端代码分析

记得之前提过我们将一些用户信息存到的redis中,这里就是这么实现的,通过获取请求中的token解析得到其中的uuid信息,拼接上请求头然后到redis中获取。

/**
     * 获取用户信息
     * 
     * @return 用户信息
     */
    @GetMapping("getInfo")
    public AjaxResult getInfo()
    {
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        SysUser user = loginUser.getUser();
        // 角色集合
        Set<String> roles = permissionService.getRolePermission(user);
        // 权限集合
        Set<String> permissions = permissionService.getMenuPermission(user);
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        return ajax;
    }


  /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            Claims claims = parseToken(token);
            // 解析对应的权限以及用户信息
            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
            String userKey = getTokenKey(uuid);
            LoginUser user = redisCache.getCacheObject(userKey);
            return user;
        }
        return null;
    }


    /**
     * 获取请求token
     *
     * @param request
     * @return token
     */
    private String getToken(HttpServletRequest request)
    {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
        {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

同时如果只是想获取用户简单信息还可以使用SecurityUtils类中的一些方法,核心就是Authentication ,这个里面存着一些用户信息,

/**
     * 获取用户
     **/
    public static LoginUser getLoginUser()
    {
        try
        {
            return (LoginUser) getAuthentication().getPrincipal();
        }
        catch (Exception e)
        {
            throw new CustomException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
        }
    }

    /**
     * 获取Authentication
     */
    public static Authentication getAuthentication()
    {
        return SecurityContextHolder.getContext().getAuthentication();
    }

这个里面的数据是若依自己存储的,存储方式其实也是通过token,使用的是过滤器

五 Token拦截 过期处理和刷新
这边的token是携带过来的,将取出的值作为redis的key值取出登录时存入的用户信息,然后对其数据进行简单的检验,如果通过了就会将其信息交给Spring Security 处理,具体如何解读下面那段代码可以参考
如果未通过就会被Spring Security当作异常处理,这边就比较复杂了,最终是 会去处理,这边若依是通过自定义AuthenticationEntryPointImpl 来处理的,具体逻辑比较多,可以去debug跑一下

/**
 * token过滤器 验证token有效性
 * 
 * @author ruoyi
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
       
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}




/**
 * 认证失败处理类 返回未授权
 * 
 * @author ruoyi
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException
    {
        int code = HttpStatus.HTTP_UNAUTHORIZED;
        String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

六 退出登录

若依在SecurityConfig类中设置各个访问路由的时候设置了退出访问路径,将会访问自定义的退出类LogoutSuccessHandlerImpl 当中,综合上述我们可以得知,若依的登录就是在登录时将用户信息存入redis当中,key值写入token,当后续请求访问进来的时候会提取token当中的key去取用户信息,来实现权限的认证,所以当退出的时候就必须一是去除请求当中的token,二是删除有redis当中信息,这就是自定义退出类的实现逻辑

package com.wzld.framework.security.handle;

import cn.hutool.core.lang.Validator;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.wzld.common.constant.Constants;
import com.wzld.common.core.domain.AjaxResult;
import com.wzld.common.core.domain.model.LoginUser;
import com.wzld.common.utils.ServletUtils;
import com.wzld.framework.manager.AsyncManager;
import com.wzld.framework.manager.factory.AsyncFactory;
import com.wzld.framework.web.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义退出处理类 返回成功
 * 
 * @author ruoyi
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
    @Autowired
    private TokenService tokenService;

    /**
     * 退出处理
     * 
     * @return
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (Validator.isNotNull(loginUser))
        {
            String userName = loginUser.getUsername();
            // 删除用户缓存记录
            tokenService.delLoginUser(loginUser.getToken());
            // 记录用户退出日志
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
        }
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.HTTP_OK, "退出成功")));
    }
}