持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
引言
上篇:前置知识blog.csdn.net/u011018979/… 下篇:核心实现(打开app,如果 token不过期,就使用最近一次登录的token进行接口请求)blog.csdn.net/u011018979/…
iOS小技能: app侧退出登录处理流程blog.csdn.net/z929118967/…
I 前置知识
1.1 分布式系统下的session
session: 一种保存key-value的机制 key:
- sessionID
- token (配合签名一起使用)
会话相关的信息存储在Redis,REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。
Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。
Spring Data Redis是较大的Spring数据家族的一部分,它提供了从Spring应用程序轻松配置和访问redis的功能
。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Redis可视化工具 Redis Desktop Manager
:
官网下载:redisdesktop.com/download
mac:apps.apple.com/us/app/redi…
关注公号:
iOS逆向
,找我获取资源:redis-desktop-manager-0.8.3-2550.dmg
。
1.2 服务端侧的登录处理
登录:
- openid去和数据库里的数据匹配(采用微信授权登录)
- 设置token至redis
- 设置token至cookie
有session的接口,使用AOP获取HttpRequest,进行身份验证。
注销:
- 从cookie里查询
- 清除redis
- 清除cookie
1.3 app侧需求
-
冷启动app时,若token不过期,就使用最近一次登录的token进行数据请求
-
优化token的存储方式:
之前只是存储在内存,只要杀死app,重新打开就要求重新登录。现在改为将token信息存储到本地数据库,每次打开app使用最近一次登录获得的token。
II app侧登录流程
2.1 开发步骤
- I、保存token到UserInfoModel 对象中
- II、再次打开app的时候获取token
- III、退出登录或者(token)失效进行信息信息清除
- IV、在登录界面的viewDidLoad 进行判断是否直接进入首页
- V : token存储区分正式环境和测试环境(UserInfoModel 对象新增一个当前token的域名属性currentHost,用于查询判断) 5.1) 更换表名 5.2)UserInfoModel新增字段currentHost, 5.3)查询token新增条件currentHost 5.4) 存储token新增字段currentHost
2.2 token信息存储注意事项
登录账号得到的token信息。最好不要作为一个独立的单利对象存储;而是将它作为单例对象的属性userInfo,这样便于切换账号存储token和其他账号信息
- 如果之前是使用独立的单利对象UserInfoModel ,为了兼容代码可以这么做
/**
登录账号得到的token信息。最好不要作为一个独立的单利对象存储;而是将它作为单例对象的属性userInfo,便于切换账号存储token和其他账号信息。
*/
+ (instancetype)shareUserInfoModel{
return [QCTSession.shareQCTSession userInfo];
}
2.3 初始化当前用户信息的时机
- 登录成功之后在存储userInfo的地方,初始化当前用户相关的信息(比如
GetCurrentSysUser
)。
避免第一次登录的时候,使用上次的userInfo进行初始化。
NSLog(@"shareUserInfoModel: %@",UserInfoModel.shareUserInfoModel);//0x105a70930
NSLog(@"weakSelf: %@",weakSelf);//0x105eab3f0
HSSingletonM(QCTSession);
+ (void)SaveUserInfo:(UserInfoModel *)userInfo{
QCTSession.shareQCTSession.userInfo = userInfo;
// 初始化信息
[userInfo setupinitInfo];
}
- 打开App的时候检测是否存在token,如果存在token,初始化数据并跳转至首页。
if(UserInfoModel.shareUserInfoModel.isLoginByToken){
QCTSession.shareQCTSession.isLoginby_LoginVC =NO;
[ UserInfoModel.shareUserInfoModel setupinitInfo];
[[self class] jumpHome];
return;
}
使用线程安全模式来创建共享实例,并使用条件编译#if进行ARC、MRC的适配
2.4 整体思路
保存和清除token
- 使用BGFMDB 进保存最近一次登录的token
pod 'BGFMDB', '~> 2.0.13' #2.0.9
- 切换账号的时候更换token
- 请求接口发现token 失效的时候,回到登录界面
III AOP
Aspect Oriented Programming(面向切面编程) 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.
AOP专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,在JavaEE应用中,常常通过AOP来处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等。
应用场景例子:对有session的接口,使用AOP获取HttpRequest,进行身份验证。
Spring框架的组成结构图如下所示:
3.1 AOP实现
- 静态AOP实现: AOP框架在编译阶段对程序进行修改,即实现对目标类的增强,生成静态的AOP代理类,以AspectJ为代表。
静态AOP实现具有较好的性能,但需要使用特殊的编译器。
- 动态AOP实现: AOP框架在运行阶段动态生成AOP代理,以实现对目标对象的增强,以Spring AOP为代表。
动态AOP实现是纯Java实现,因此无须特殊的编译器,但是通常性能略差。
3.2 AOP的基本概念
- 切面(Aspect): 切面用于组织多个Advice,Advice放在切面中定义。
- 连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用,或者异常的抛出。在Spring AOP中,连接点总是方法的调用。
- 增强处理(Advice): AOP框架在特定的切入点执行的增强处理。处理有"around"、"before"和"after"等类型
- 切入点(Pointcut): 可以插入增强处理的连接点。简而言之,当某个连接点满足指定要求时,该连接点将被添加增强处理,该连接点也就变成了切入点。
3.3 相关注解
@Aspect:作用是把当前类标识为一个切面,供容器读取。
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
@After: final增强,不管是抛出异常或者正常退出都会执行
3.4 AOP实现身份验证
- 创建一个切面类Aspect
@Aspect
@Component
@Slf4j
public class AuthorizeAspect {
}
- 设置切入点Pointcut
public class AuthorizeAspect {
//拦截有session的操作,排除登陆登出的操作
@Pointcut("execution(public * com.jess.sell.controller.xxxSeller*.*(..))" +
" && !execution(public * com.jess.sell.controller.xxxxxSellerUserController.*(..))")
public void verify() {
}
}
- 设置拦截后的操作Advice
@Before("verify()")//@Before:标识一个前置增强方法
public void doVerify() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//查询 cookie
Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
if (cookie == null) { //没有登录
log.warn("【登录校验】 cookie中没有token");
throw new AuthorizeException();
}
//查询 redis
String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));
if(StringUtils.isEmpty(tokenValue)){
log.warn("【登录校验】 redis中没有token");
throw new AuthorizeException();
}
}
- 异常拦截:SpringBoot自带异常拦截@ControllerAdvice
创建一个SellerExceptionHandler类
@ControllerAdvice
public class ExceptionHandler {
private ProjectUrlConfig projectUrlConfig;
/**
* 拦截登录异常,并跳转到登录界面
* value = AuthorizeException.class)表示拦截的异常为AuthorizeException异常
*/
@ExceptionHandler(value = AuthorizeException.class)
public ModelAndView handlerAuthorizeException(){
return new ModelAndView("redirect:"
.concat(projectUrlConfig.getWechatOpenAuthorize())
.concat("/sell/wechat/qrAuthorize")
.concat("?returnUrl=")
.concat(projectUrlConfig.getSell())
.concat("/sell/seller/login"));
}
}