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

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

前言关于小说系统源码身份认证的方式,最近在工作中用到了些,想着就干脆针对目前常用的身份认证组件的实际使用,就做一个总结,会涉及Json Web Token(JWT),Shiro的常见认证方式。传统的认证流程在学习这些组件的时候,还是需要熟悉一下认证的流程,其实一般的身份认证流程也不复杂,只是由于本身HTTP是无状态协议,因此需要交互多次才能验证用户身份。用户认证的一般流程如下1、用户向服务器发送用户名和密码2、若用户信息通过验证,在当前会话(session)中保存相关客户数据,比如用户登录时间和用户角色等等。3、服务器向用户发送一个session_id,存入用户端的cookie4、用户随后访问服务器的每一次请求,都需要带上cookie中的session_id5、服务器收到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建立一个项目,然后搭建三个模块,整体接口如下准备一下SQLCREATEDATABASEdb_user_auth;USEdb_user_auth;–用户操作日志记录表CREATETABLE`log`(`id`int(11)NOTNULLAUTO_INCREMENT,`user_id`int(11)DEFAULTNULLCOMMENT'用户id',`user_name`varchar(255)CHARACTERSETutf8mb4DEFAULTNULLCOMMENT'用户名',`create_time`datetimeDEFAULTNULLCOMMENT'操作时间',`memo`varchar(255)CHARACTERSETutf8mb4DEFAULTNULLCOMMENT'操作备注',PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=2DEFAULTCHARSET=utf8COMMENT='操作日志';–用户表CREATETABLE`user`(`id`int(11)NOTNULLAUTO_INCREMENT,`user_name`varchar(100)CHARACTERSETutf8mb4NOTNULLCOMMENT'用户名',`password`varchar(200)CHARACTERSETutf8mb4NOTNULLCOMMENT'密码',`phone`varchar(50)NOTNULLCOMMENT'手机号',`email`varchar(100)CHARACTERSETutf8mb4NOTNULLCOMMENT'邮箱',`is_active`tinyint(11)DEFAULT'1'COMMENT'是否有效(1=是;0=否)',`create_time`datetimeDEFAULTNULLCOMMENT'创建时间',`update_time`timestampNULLDEFAULTNULLCOMMENT'更新时间',PRIMARYKEY(`id`),UNIQUEKEY`idx_user_name`(`user_name`)USINGBTREE)ENGINE=InnoDBAUTO_INCREMENT=9DEFAULTCHARSET=utf8COMMENT='用户信息表';–用于存储token的数据表CREATETABLE`auth_token`(`id`int(11)NOTNULLAUTO_INCREMENT,`user_id`int(11)NOTNULLCOMMENT'用户id',`access_token`varchar(255)CHARACTERSETutf8mb4NOTNULL,`access_expire`bigint(11)NOTNULLCOMMENT'access_token失效时间(ms)',`token_timestamp`bigint(20)NOTNULLCOMMENT'生成access_token时的时间',`is_active`tinyint(4)DEFAULT'1'COMMENT'是否有效',`create_time`datetimeDEFAULTNULLCOMMENT'创建时间',`update_time`datetimeDEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=54DEFAULTCHARSET=utf8COMMENT='访问认证授权的Token';后续会发布相关源码,博客只介绍核心代码部分。数据库+token的认证方式其实数据库+token的认证也没有什么高深的,只是服务端为指定的客户生成的token替代了sessionid,然后每次客户端请求带上token即可。服务端完成对该token的验证。token生成的方式有很多种,我们这个实例中就根据先关的元素和雪花算法生成的唯一id,并通过相关密钥对称加密生成tokentoken生成与验证1、需要生成token的相关属性@Data@AllArgsConstructor@NoArgsConstructor/***生成token的元素,生成的最初的token,需要用到这些元素*/publicclassAccessTokenDtoimplementsSerializable{privateIntegeruserId;//userIdprivateStringuserName;//userNameprivateLongtimestamp;//时间戳privateStringrandomStr;//随机串privateLongexpire;}2、生成token的方法,如下所示,生成的token会保存数据库,用于后续验证客户身份@Transactional(rollbackFor=Exception.class)//加入事务控制publicAuthTokenModelauthAndCreateToken(StringuserName,Stringpassword)throwsException{Useruser=userService.authUser(userName,password);if(user!=null){//正式创建token之前需要实现之前的tokenauthTokenMapper.invalidateTokenByUser(user.getId());//创建token//1.生成时间戳和随机串LongtimeStamp=System.currentTimeMillis();StringrandomStr=Constant.snowFlake.nextId().toString();//利用雪花算法,产生随机串//2.利用dto构建生成token需要的元素AccessTokenDtotokenDto=newAccessTokenDto(user.getId(),user.getUserName(),timeStamp,randomStr,Constant.ACCESS_TOKEN_EXPIRE);StringtokenJsonStr=objectMapper.writeValueAsString(tokenDto);//3.生成真正的tokenStringtoken=EncryptUtil.aesEncrypt(tokenJsonStr,Constant.TOKEN_AUTH_KEY);//AES加密算法生成的token//4.将token返回给前端AuthTokenModelauthToken=newAuthTokenModel(token,Constant.ACCESS_TOKEN_EXPIRE);//5.保存token到数据库AuthTokenauthTokenEntity=newAuthToken();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–");returnauthToken;}returnnull;}3、验证token/***验证并解析token*@paramaccessToken*@return*/publicBaseResponsevalidateToken(finalStringaccessToken){log.info("开始验证token:{}",accessToken);BaseResponseresponse=newBaseResponse(StatusCode.Success);try{if(StringUtils.isBlank(accessToken)){returnnewBaseResponse(StatusCode.AccessTokenNotBlank);}//验证token是否在数据库中是否存在AuthTokenauthToken=authTokenMapper.selectByAccessToken(accessToken);if(authToken==null){log.info("token不存在:{}",accessToken);returnnewBaseResponse(StatusCode.AccessTokenNotExist);}//解析token,为了防止伪造,需要解析一下,看是否合法AccessTokenDtoaccessTokenDto;try{log.info("开始解析token:{}",accessToken);accessTokenDto=decodeAccessToken(accessToken);}catch(Exceptione){//解析异常,token不合法returnnewBaseResponse(StatusCode.AccessTokenInvalidate);}//验证token是否过期if(accessTokenDto!=null){log.info("开始验证token是否过期:{}",accessToken);if(System.currentTimeMillis()-accessTokenDto.getTimestamp()>accessTokenDto.getExpire()){invalidateAccessToken(accessToken);returnnewBaseResponse(StatusCode.TokenValidateExpireToken);}}}catch(Exceptione){returnnewBaseResponse(StatusCode.Fail.getCode(),e.getMessage());}log.info("token验证通过:{}",accessToken);returnresponse;}加入拦截器拦截器中完成对token的验证@Slf4jpublicclassDBAuthInterceptorimplementsHandlerInterceptor{@AutowiredprivateCommonServicecommonService;@AutowiredprivateDBAuthServicedbAuthService;/***正式进入请求方法之前的拦截操作*@paramrequest*@paramresponse*@paramhandler*@return*@throwsException*/@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{if(handlerinstanceofHandlerMethod)//从HTTP的请求头header中获取tokenStringaccessToken=request.getHeader("accessToken");if(StringUtils.isBlank(accessToken)){log.error("–db+token认证缺失token参数");BaseResponsebaseResponse=newBaseResponse(StatusCode.AccessTokenNotExist,"accessToken不存在");commonService.print(response,baseResponse);}else{log.info("-db+token认证开始");//这里开始解析并验证tokenBaseResponseresult=dbAuthService.validateToken(accessToken);if(Objects.equals(result.getStatus(),StatusCode.Success.getCode())){returntrue;}else{commonService.print(response,result);returnfalse;}}}returnfalse;}/***执行完目标方法之后的操作*@paramrequest*@paramresponse*@paramhandler*@parammodelAndView*@throwsException*/@OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{if(response.getStatus()==500){modelAndView.setViewName("/error/500");}elseif(response.getStatus()==404){modelAndView.setViewName("/error/404");}}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{}}将拦截器交给容器管理,同时指定需要认证的路径,具体如下@ConfigurationpublicclassCustomerWebConfigimplementsWebMvcConfigurer{/***每一个类型的拦截器需要单独添加,不可串行添加**@paramregistry*/@OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){finalString[]interceptorPaths=newString[]{"/dbAuth/token/auth","/dbAuth/token/password/update","/dbAuth/token/logout"};registry.addInterceptor(dbAuthInterceptor()).addPathPatterns(interceptorPaths);}@BeanpublicDBAuthInterceptordbAuthInterceptor(){returnnewDBAuthInterceptor();}}由于这里我们用到了数据库存放token,因此需要有一个批量任务,定时删除过期无效的token批量删除token1、顶一个一个线程池,交给spring容器托管@ConfigurationpublicclassScheduleConfig{@Bean("taskExecutor")publicExecutortaskExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();//核心线程数executor.setCorePoolSize(4);//最大核心线程数executor.setMaxPoolSize(10);//设置队列中等待被调度处理的任务数量executor.setQueueCapacity(8);returnexecutor;}}2、开始定时批量,将定时任务交给指定线程池@Component//@EnableAsync@Slf4jpublicclassCommonScheduler{@AutowiredprivateAuthTokenMapperauthTokenMapper;@Scheduled(cron="0/60****?")//60S执行一次的表达式@Async("taskExecutor")publicvoiddeleteAnyInvalidateToken(){try{log.info("–删除无效token的批次开始执行–");authTokenMapper.deleteUnactiveToken();}catch(Exceptione){log.error("–删除无效的token批次执行异常");}log.info("–删除无效token的批次执行结束–");}}至此,算是完成简单的token开发任务Redis+token的认证方式Redis+token的认证方式,身份认证流程上与数据库+token认证方式相比并没任何区别,只是存储token的地方变成了Redis,Redis本身较好的读写性能,以及可以定时设置的特性,使得维护token更加简单。由于其他流程一样,这里就只贴出Redis+token的生成和验证token的部分代码/***认证并产生token*@paramuserName*@parampassword*@return*/publicAuthTokenModelauthAndCreateToken(StringuserName,Stringpassword)throwsException{//用户名密码的认证依旧来自数据库的数据Useruser=userService.authUser(userName,password);if(user!=null){Longtimestamp=System.currentTimeMillis();AccessTokenDtoaccessTokenDto=newAccessTokenDto(user.getId(),userName,timestamp,Constant.snowFlake.nextId().toString(),Constant.ACCESS_TOKEN_EXPIRE);StringjsonStr=objectMapper.writeValueAsString(accessTokenDto);log.info("–redis认证生成的token的json字符串–:{}",jsonStr);//根据客户关键信息加密生成tokenStringaccessToken=EncryptUtil.aesEncrypt(jsonStr,Constant.TOKEN_AUTH_KEY);//开始缓存生成的tokenstringRedisTemplate.opsForValue().set(Constant.TOKEN_REDIS_KEY_PREFIX+userName,accessToken,Constant.ACCESS_TOKEN_EXPIRE,TimeUnit.MILLISECONDS);log.info("–token生成成功,已经存入缓存–");AuthTokenModelauthTokenModel=newAuthTokenModel(accessToken,Constant.ACCESS_TOKEN_EXPIRE);returnauthTokenModel;}returnnull;}/***redis的验证token*@paramaccessToken*@return*/publicBaseResponsevalidateToken(finalStringaccessToken){BaseResponseresult=newBaseResponse(StatusCode.Success);try{if(StringUtils.isBlank(accessToken)){returnnewBaseResponse(StatusCode.AccessTokenNotBlank);}//解密获取tokenAccessTokenDtoaccessTokenDto=decodeAccessToken(accessToken);if(StringUtils.isBlank(accessTokenDto.getUserName())){returnnewBaseResponse(StatusCode.AccessTokenInvalidate);}//查看redis中token是否存在finalStringkey=Constant.TOKEN_REDIS_KEY_PREFIX+accessTokenDto.getUserName();BooleanisExist=stringRedisTemplate.hasKey(key);//这个redis的方式,如果存在这个可以,则直接返回trueif(!isExist){//token已经不存在,失效returnnewBaseResponse(StatusCode.AccessTokenNotExist);}//开始对比token,这个操作在数据库认证中没有StringredisToken=stringRedisTemplate.opsForValue().get(key);if(!accessToken.equals(redisToken)){returnnewBaseResponse(StatusCode.AccessTokenInvalidate);}}catch(Exceptione){returnnewBaseResponse(StatusCode.Fail.getCode(),e.getMessage());}returnresult;}测试步骤主要有四个基本的接口(每个认证模块几乎都是)。具体如下路径(接口名称)说明/login登录获取token,找个接口会返回token/token/auth这个接口需要校验token,认证成功了才能顺利访问该接口/token/unauth不需要认证就能访问的接口,区别于上一个接口token/logout退出接口,该接口会失效token测试方式,直接用postman发起请求即可,注意请求顺序。1、通过登录获取token2、之后将第一步获取的token放在header中,发起需要认证访问的接口请求总结这篇博客只是简单说明了传统的身份认证方式的基础框架,但其实,如果小说系统源码需要扩展,更好的是不需要服务端去维护这些信息,而是交个某种第三方组件,根据某些关键信息去自动生成,于是JWT和shrio等第三方认证组件就出现了,这些后续都会一一总结。声明:本文由云豹科技转发自谜一样的Coder博客,如有侵权请联系作者删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Proudly powered by WordPress | Theme: HoneyWaves by SpiceThemes