Springboot Vue实现单点登陆功能示例详解

作者:harhar 时间:2023-11-05 00:29:11 

登陆是系统最基础的功能之一。这么长时间了,一直在写业务,这个基础功能反而没怎么好好研究,都忘差不多了。今天没事儿就来撸一下。

以目前在接触和学习的一个开源系统为例,来分析一下登陆该怎么做。代码的话我就直接CV了。

简单上个图

(有水印。因为穷所以没开会员)

Springboot Vue实现单点登陆功能示例详解

先分析下登陆要做啥

首先,搞清楚要做什么。

登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。

怎么落实?

怎么实现它?用什么实现?

我们的项目是Springboot + Vue前后端分离类型的。

选择用token + redis 实现,权限的话用SpringSecurity来做。

前后端分离避不开的一个问题就是单点登陆,单点登陆咱们有很多实现方式:CAS中央认证、JWT、token等,咱们这种方式其实本身就是基于token的一个单点登陆的实现方案。

单点登陆我们改天整理一篇OAuth2.0的实现方式,今天不搞这个。

上代码

概念这个东西越说越玄。咱们直接上代码吧。

接口:

@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
   AjaxResult ajax = AjaxResult.success();
   // 生成令牌
   //用户名、密码、验证码、uuid
   String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                     loginBody.getUuid());
   ajax.put(Constants.TOKEN, token);
   return ajax;
}

用户信息验证交给SpringSecurity

/**
* 登录验证
*/
public String login(String username, String password, String code, String uuid)
{
   // 验证码开关,顺便说一下,系统配置相关的开关之类都缓存在redis里,系统启动的时候加载进来的。这一块儿的代码就不贴出来了
   boolean captchaEnabled = configService.selectCaptchaEnabled();
   if (captchaEnabled)
   {
       //uuid是验证码的redis key,登陆页加载的时候验证码生成接口返回的
       validateCaptcha(username, code, uuid);
   }
   // 用户验证 -- SpringSecurity
   Authentication authentication = null;
   try
   {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       AuthenticationContextHolder.setContext(authenticationToken);
       // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername。
       //
       authentication = authenticationManager.authenticate(authenticationToken);
   }
   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 ServiceException(e.getMessage());
       }
   }
   finally
   {
       AuthenticationContextHolder.clearContext();
   }
   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
   LoginUser loginUser = (LoginUser) authentication.getPrincipal();
   recordLoginInfo(loginUser.getUserId());
   // 生成token
   return tokenService.createToken(loginUser);
}

把校验验证码的部分贴出来,看看大概的逻辑(这个代码封装得太碎了。。。没全整出来)

/**
* 校验验证码
*/
public void validateCaptcha(String username, String code, String uuid)
{
   //uuid是验证码的redis key
   String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //String CAPTCHA_CODE_KEY = "captcha_codes:";
   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();
   }
}

token生成部分

这里,token

/**
* 创建令牌
*/
public String createToken(LoginUser loginUser)
{
   String token = IdUtils.fastUUID();
   loginUser.setToken(token);
   setUserAgent(loginUser);
   refreshToken(loginUser);
   Map<String, Object> claims = new HashMap<>();
   claims.put(Constants.LOGIN_USER_KEY, token);
   return createToken(claims);
}

刷新token

/**
* 刷新令牌
*/
public void refreshToken(LoginUser loginUser)
{
   loginUser.setLoginTime(System.currentTimeMillis());
   loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
   // 根据uuid将loginUser缓存
   String userKey = getTokenKey(loginUser.getToken());
   redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

验证token

/**
* 验证令牌
*/
public void verifyToken(LoginUser loginUser)
{
   long expireTime = loginUser.getExpireTime();
   long currentTime = System.currentTimeMillis();
   if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
   {
       refreshToken(loginUser);
   }
}

注意这里返回给前端的token其实用JWT加密了一下,SpringSecurity的过滤器里有进行解析。

另外,鉴权时会刷新token有效期,看下面第二个代码块的注释。

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
   //...无关的代码删了
   httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
}
@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()))
       {
           //刷新token有效期
           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);
   }
}

这个登陆方案里用了token + redis,还有JWT,其实用哪一种方案都可以独立实现,并且两种方案都可以用来做单点登陆。

这里JWT只是起到个加密的作用,无它。

来源:https://juejin.cn/post/7184266088652210231

标签:Springboot,Vue,单点登陆
0
投稿

猜你喜欢

  • java微信公众号开发案例

    2023-01-30 11:05:36
  • Android如何让APP无法在指定的系统版本上运行(实现方法)

    2022-10-16 03:24:46
  • RocketMQ生产者如何规避故障Broker方式详解

    2022-06-23 04:36:10
  • java读取excel文件的两种方法

    2022-08-24 16:55:45
  • 图解Java经典算法冒泡排序的原理与实现

    2023-03-14 21:41:23
  • Android 短信转换成彩信的消息数量(实例代码)

    2021-11-13 13:01:58
  • C# ODP.NET 调用Oracle函数返回值时报错的一个解决方案

    2021-10-03 01:54:19
  • Java Map接口及其实现类原理解析

    2022-06-04 22:54:29
  • Java多态和实现接口的类的对象赋值给接口引用的方法(推荐)

    2023-11-26 11:59:41
  • 浅谈C#中简单的异常引发与处理操作

    2022-03-10 04:54:49
  • 详解SpringMVC中的日期处理和文件上传操作

    2021-11-13 05:39:18
  • 全面总结java IO体系

    2023-05-16 13:19:12
  • Java数据结构之线段树的原理与实现

    2021-12-17 13:30:52
  • Android电量优化提高手机续航

    2022-06-14 11:39:40
  • 深入解析Java中反射中的invoke()方法

    2023-03-11 10:17:29
  • Java ThreadPoolExecutor 线程池的使用介绍

    2021-06-28 12:40:35
  • 简单易懂的java8新特性之lambda表达式知识总结

    2023-04-14 23:44:42
  • Android自定义密码输入框和数字键盘

    2022-02-04 11:14:00
  • SpringBoot--- SpringSecurity进行注销权限控制的配置方法

    2022-11-11 03:49:54
  • Android开发之SeekBar基本使用及各种美观样式示例

    2023-06-30 07:15:22
  • asp之家 软件编程 m.aspxhome.com