ITPub博客

首页 > 应用开发 > IT综合 > 小说系统源码开发,实现系统的用户登录验证

小说系统源码开发,实现系统的用户登录验证

IT综合 作者:云豹科技阿星 时间:2021-11-30 16:52:06 0 删除 编辑

前言
关于小说系统源码身份认证的方式,最近在工作中用到了些,想着就干脆针对目前常用的身份认证组件的实际使用,就做一个总结,会涉及Json Web Token(JWT),Shiro的常见认证方式。

传统的认证流程
在学习这些组件的时候,还是需要熟悉一下认证的流程,其实一般的身份认证流程也不复杂,只是由于本身HTTP是无状态协议,因此需要交互多次才能验证用户身份。

用户认证的一般流程如下

1、用户向服务器发送用户名和密码
2、若用户信息通过验证,在当前会话(session)中保存相关客户数据,比如用户登录时间和用户角色等等。
3、服务器向用户发送一个session_id,存入用户端的cookie
4、用户随后访问服务器的每一次请求,都需要带上cookie中的session_id
5、服务器收到session_id之后,就可以根据session_id 找到对应的session用户数据。
通过这个流程,至少应该知道了sessionid,session与cookie的区别,以及各自在web开发中的角色。同时需要说明的是,很多时候小说系统源码客户端会禁用cookie,因此更多的时候会用token机制替代sessionid,之后小说系统源码每次的请求都会在HTTP的header头中带上token。同时token还有更高级的认证用法,主要用于第三方登录的用户授权,当然这个是后话了。

但是会发现,这个流程会有很多问题,其核心就是扩展性很差,如果只有一个服务器这个是没有任何毛病的,但是如果有多个服务器,如果实现了跨域访问,这个就有点麻烦了,解决方案其实也有无非就是以下几种思路

一种解决思路就是session数据共享,让每台服务器都能从固定的地方读取session,比如将session存入统一的缓存中间件。

另一种解决思路就是session持久化,直接将用户session数据存入到数据库或者持久层,这样工作量会很大,而且性能也会有一点影响,同时针对失效的session还需要有单独的定时任务批量删除。

我个人归纳,如果需要服务端来维护session信息的,都属于比较传统的身份认证方式,如果session信息不需要服务端来维护,则属于非传统的身份认证方式,这种认证方式可扩展性较好。

本篇博客总结一下传统的认证方式,并附上相关实例。

实例
项目的搭建
用idea建立一个项目,然后搭建三个模块,整体接口如下

在这里插入图片描述

准备一下SQL

CREATE DATABASE db_user_auth;
USE db_user_auth;
-- 用户操作日志记录表
CREATE TABLE `log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
  `user_name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户名',
  `create_time` datetime DEFAULT NULL COMMENT '操作时间',
  `memo` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '操作备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='操作日志';
-- 用户表
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '用户名',
  `password` varchar(200) CHARACTER SET utf8mb4 NOT NULL COMMENT '密码',
  `phone` varchar(50) NOT NULL COMMENT '手机号',
  `email` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '邮箱',
  `is_active` tinyint(11) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_name` (`user_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COMMENT='用户信息表';
-- 用于存储token的数据表
CREATE TABLE `auth_token` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '用户id',
  `access_token` varchar(255) CHARACTER SET utf8mb4 NOT NULL,
  `access_expire` bigint(11) NOT NULL COMMENT 'access_token失效时间(ms)',
  `token_timestamp` bigint(20) NOT NULL COMMENT '生成access_token时的时间',
  `is_active` tinyint(4) DEFAULT '1' COMMENT '是否有效',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=54 DEFAULT CHARSET=utf8 COMMENT='访问认证授权的Token';

后续会发布相关源码,博客只介绍核心代码部分。

数据库+token的认证方式
其实数据库+token的认证也没有什么高深的,只是服务端为指定的客户生成的token替代了sessionid,然后每次客户端请求带上token即可。服务端完成对该token的验证。

token生成的方式有很多种,我们这个实例中就根据先关的元素和雪花算法生成的唯一id,并通过相关密钥对称加密生成token

token生成与验证
1、需要生成token的相关属性

@Data
@AllArgsConstructor
@NoArgsConstructor
/**
 * 生成token的元素,生成的最初的token,需要用到这些元素
 */
public class AccessTokenDto implements Serializable {
    private Integer userId; //userId
    private String userName;    //userName
    private Long timestamp;     //时间戳
    private String randomStr;   //随机串
    private Long expire;
}

2、生成token的方法,如下所示,生成的token会保存数据库,用于后续验证客户身份

@Transactional(rollbackFor = Exception.class)//加入事务控制
public AuthTokenModel authAndCreateToken(String userName, String password) throws Exception {
    User user = userService.authUser(userName,password);
    if(user!=null){
        //正式创建token之前需要实现之前的token
        authTokenMapper.invalidateTokenByUser(user.getId());
        //创建token
        //1.生成时间戳和随机串
        Long timeStamp = System.currentTimeMillis();
        String randomStr = Constant.snowFlake.nextId().toString();//利用雪花算法,产生随机串
        //2.利用dto构建生成token需要的元素
        AccessTokenDto tokenDto = new AccessTokenDto(user.getId(),user.getUserName()
                ,timeStamp,randomStr
                ,Constant.ACCESS_TOKEN_EXPIRE);
        String tokenJsonStr = objectMapper.writeValueAsString(tokenDto);
        //3.生成真正的token
        String token = EncryptUtil.aesEncrypt(tokenJsonStr, Constant.TOKEN_AUTH_KEY);   //AES加密算法生成的token
        //4.将token返回给前端
        AuthTokenModel authToken = new AuthTokenModel(token,Constant.ACCESS_TOKEN_EXPIRE);
        //5.保存token到数据库
        AuthToken authTokenEntity = new AuthToken();
        authTokenEntity.setUserId(user.getId());
        authTokenEntity.setAccessToken(token);
        authTokenEntity.setAccessExpire(Constant.ACCESS_TOKEN_EXPIRE);
        authTokenEntity.setTokenTimestamp(timeStamp);
        authTokenEntity.setCreateTime(DateTime.now().toDate());
        authTokenMapper.insertSelective(authTokenEntity);
        log.info("[token]+数据库用户认证成功,成功生成accessToken--");
        return authToken;
    }
    return null;
}

3、验证token

/**
 * 验证并解析token
 * @param accessToken
 * @return
 */
public BaseResponse validateToken(final String accessToken){
    log.info("开始验证token:{}",accessToken);
    BaseResponse response = new BaseResponse(StatusCode.Success);
    try{
        if(StringUtils.isBlank(accessToken)){
            return new BaseResponse(StatusCode.AccessTokenNotBlank);
        }
        //验证token是否在数据库中是否存在
        AuthToken authToken = authTokenMapper.selectByAccessToken(accessToken);
        if(authToken==null){
            log.info("token 不存在:{}",accessToken);
            return new BaseResponse(StatusCode.AccessTokenNotExist);
        }
        //解析token,为了防止伪造,需要解析一下,看是否合法
        AccessTokenDto accessTokenDto;
        try{
            log.info("开始解析token:{}",accessToken);
            accessTokenDto = decodeAccessToken(accessToken);
        }catch (Exception e){//解析异常,token不合法
            return new BaseResponse(StatusCode.AccessTokenInvalidate);
        }
        //验证token是否过期
        if(accessTokenDto != null){
            log.info("开始验证token是否过期:{}",accessToken);
            if(System.currentTimeMillis()-accessTokenDto.getTimestamp() > accessTokenDto.getExpire()){
                invalidateAccessToken(accessToken);
                return new BaseResponse(StatusCode.TokenValidateExpireToken);
            }
        }
    }catch (Exception e){
        return new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    log.info("token验证通过:{}",accessToken);
    return response;
}

加入拦截器

拦截器中完成对token的验证

@Slf4j
public class DBAuthInterceptor implements HandlerInterceptor {
    @Autowired
    private CommonService commonService;
    @Autowired
    private DBAuthService dbAuthService;
    /**
     * 正式进入请求方法之前的拦截操作
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod)
            //从HTTP的请求头header中获取token
            String accessToken = request.getHeader("accessToken");
            if(StringUtils.isBlank(accessToken)){
                log.error("--db+token认证缺失token参数");
                BaseResponse baseResponse = new BaseResponse(StatusCode.AccessTokenNotExist,"accessToken不存在");
                commonService.print(response,baseResponse);
            }else{
                log.info("-db+token认证开始");
                //这里开始解析并验证token
                BaseResponse result = dbAuthService.validateToken(accessToken);
                if (Objects.equals(result.getStatus(),StatusCode.Success.getCode())){
                    return true;
                }else{
                    commonService.print(response,result);
                    return false;
                }
            }
        }
        return false;
    }
    /**
     *  执行完目标方法之后的操作
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if(response.getStatus() == 500){
            modelAndView.setViewName("/error/500");
        }else if(response.getStatus() == 404){
            modelAndView.setViewName("/error/404");
        }
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

将拦截器交给容器管理,同时指定需要认证的路径,具体如下

@Configuration
public class CustomerWebConfig implements WebMvcConfigurer {
    /**
     * 每一个类型的拦截器需要单独添加,不可串行添加
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        final String[] interceptorPaths = new String[]{"/dbAuth/token/auth"
                , "/dbAuth/token/password/update", "/dbAuth/token/logout"};
        registry.addInterceptor(dbAuthInterceptor())
                .addPathPatterns(interceptorPaths);
    }
    @Bean
    public DBAuthInterceptor dbAuthInterceptor() {
        return new DBAuthInterceptor();
    }
}

由于这里我们用到了数据库存放token,因此需要有一个批量任务,定时删除过期无效的token

批量删除token

1、顶一个一个线程池,交给spring容器托管

@Configuration
public class ScheduleConfig {
    @Bean("taskExecutor")
    public Executor taskExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程数
        executor.setCorePoolSize(4);
        //最大核心线程数
        executor.setMaxPoolSize(10);
        //设置队列中等待被调度处理的任务数量
        executor.setQueueCapacity(8);
        return executor;
    }
}

2、开始定时批量,将定时任务交给指定线程池

@Component
//@EnableAsync
@Slf4j
public class CommonScheduler {
    @Autowired
    private AuthTokenMapper authTokenMapper;
    @Scheduled(cron = "0/60 * * * * ? ")//60S执行一次的表达式
    @Async("taskExecutor")
    public void deleteAnyInvalidateToken(){
        try{
            log.info("--删除无效token的批次开始执行--");
            authTokenMapper.deleteUnactiveToken();
        }catch (Exception e){
            log.error("--删除无效的token批次执行异常");
        }
        log.info("--删除无效token的批次执行结束--");
    }
}

至此,算是完成简单的token开发任务

Redis+token的认证方式
Redis+token的认证方式,身份认证流程上与数据库+token认证方式相比并没任何区别,只是存储token的地方变成了Redis,Redis本身较好的读写性能,以及可以定时设置的特性,使得维护token更加简单。由于其他流程一样,这里就只贴出Redis+token的生成和验证token的部分代码

/**
 * 认证并产生token
 * @param userName
 * @param password
 * @return
 */
public AuthTokenModel authAndCreateToken(String userName, String password) throws Exception {
    //用户名密码的认证依旧来自数据库的数据
    User user = userService.authUser(userName, password);
    if(user!=null){
        Long timestamp = System.currentTimeMillis();
        AccessTokenDto accessTokenDto = new AccessTokenDto(user.getId(),userName,
                timestamp, Constant.snowFlake.nextId().toString(),Constant.ACCESS_TOKEN_EXPIRE);
        String jsonStr = objectMapper.writeValueAsString(accessTokenDto);
        log.info("--redis认证生成的token的json字符串--:{}",jsonStr);
        //根据客户关键信息加密生成token
        String accessToken = EncryptUtil.aesEncrypt(jsonStr,Constant.TOKEN_AUTH_KEY);
        //开始缓存生成的token
        stringRedisTemplate.opsForValue().set(Constant.TOKEN_REDIS_KEY_PREFIX+userName,
                accessToken,Constant.ACCESS_TOKEN_EXPIRE,TimeUnit.MILLISECONDS);
        
        log.info("--token生成成功,已经存入缓存--");
        AuthTokenModel authTokenModel = new AuthTokenModel(accessToken, Constant.ACCESS_TOKEN_EXPIRE);
        return authTokenModel;
    }
    return null;
}
/**
 * redis的验证token
 * @param accessToken
 * @return
 */
public BaseResponse validateToken(final String accessToken){
    BaseResponse result = new BaseResponse(StatusCode.Success);
    try{
        if(StringUtils.isBlank(accessToken)){
            return new BaseResponse(StatusCode.AccessTokenNotBlank);
        }
        //解密获取token
        AccessTokenDto accessTokenDto = decodeAccessToken(accessToken);
        if(StringUtils.isBlank(accessTokenDto.getUserName())){
            return new BaseResponse(StatusCode.AccessTokenInvalidate);
        }
        //查看redis中token是否存在
        final String key = Constant.TOKEN_REDIS_KEY_PREFIX+accessTokenDto.getUserName();
        Boolean isExist = stringRedisTemplate.hasKey(key);//这个redis的方式,如果存在这个可以,则直接返回true
        if(!isExist){//token已经不存在,失效
            return new BaseResponse(StatusCode.AccessTokenNotExist);
        }
        //开始对比token,这个操作在数据库认证中没有
        String redisToken = stringRedisTemplate.opsForValue().get(key);
        if(!accessToken.equals(redisToken)){
            return new BaseResponse(StatusCode.AccessTokenInvalidate);
        }
    }catch (Exception e){
        return new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return result;
}

测试步骤
主要有四个基本的接口(每个认证模块几乎都是)。具体如下

路径(接口名称)

说明

/login

登录获取token,找个接口会返回token

/token/auth

这个接口需要校验token,认证成功了才能顺利访问该接口

/token/unauth

不需要认证就能访问的接口,区别于上一个接口

token/logout

退出接口,该接口会失效token

测试方式,直接用postman发起请求即可,注意请求顺序。

1、通过登录获取token

在这里插入图片描述

2、之后将第一步获取的token放在header中,发起需要认证访问的接口请求

在这里插入图片描述

总结

这篇博客只是简单说明了传统的身份认证方式的基础框架,但其实,如果小说系统源码需要扩展,更好的是不需要服务端去维护这些信息,而是交个某种第三方组件,根据某些关键信息去自动生成,于是JWT和shrio等第三方认证组件就出现了,这些后续都会一一总结。
声明:本文由云豹科技转发自谜一样的Coder博客,如有侵权请联系作者删除


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69982461/viewspace-2844953/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2020-08-24

  • 博文量
    239
  • 访问量
    115527