整理2.0当前登录认证实现过程
过滤链
JwtAuthenticationTokenFilter -> LicenseExpireFilter -> 接口中的过滤器
JwtAuthenticationTokenFilter
首先从请求的header中获取token
如果token存在并且是以指定的前缀开头那么开始判断token是否正确
首先把token的前缀去掉,然后获得token中的Claims 然后从claims的subjiect获得username,如果当前用户名不为空并且安全上下文不为空,那么继续验证token。
调用loadUserByUsername方法判断用户名是否存在,是否被禁用,并且set用户对应的角色的菜单权限。
然后调用jwtTokenUtil.validateToken方法验证页面是否过期,验证用户名是否存在,验证token是否失效,如果不是系统用户还会调用所有自定义拦截器的tokenInterceptorHandler方法
如果验证都通过了那么就设置安全上下文和
request.setAttribute(“requestUserEntity”, securityUserEntity);
request.setAttribute(“requestUserAccount”, username);
如果上述过程抛出了异常 那么一律转发到/filter/login_auth_fail 然后返回给前端
登录接口
/auth/login
请求进入登录接口首先会调用
//根据dto中的用户名去mysql获取用户实体并且根据角色id得到菜单访问权限 (把 "菜单id_菜单权限" set成 Authorities)
UserDetails userDetails = securityUserServiceImpl.loadUserByUsername(dto.getUsername());
该方法会对用户名进行判断 如果用户名不存在或者用户被禁用那么方法会抛出异常,如果用户名没有问题那么系统会根据当前用户的roleId查询菜单权限表 ,然后把对应的权限set到SecurityUserEntity 实体中,然后返回。
然后继续调用
//根据用户名和密码生成upToken
UsernamePasswordAuthenticationToken upToken = newUsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword());
根据用户名和密码生成upToken,然后调用方法获得自定义拦截器的所有实现类
Map<String, ICustomInterceptor> interceptorMap = applicationContext.getBeansOfType(ICustomInterceptor.class);
然后从mysql中获取一条安全策略,根据配置文件中的defaultSysUser来判断当前用户是否为admin 上面都是准备工作。
如果用户不是admin 那么就调用每一个ICustomInterceptor的实现类的beforeInterceptorHandler方法 挨个拦截!
FirstLoginInterceptorImpl 首次登陆拦截(after和token)
从安全策略中获取 是否开启首次登录拦截 如果开启并且用户没有过修改密码的记录那么就抛出异常10002。
IpRuleInterceptorImpl IP规则拦截(before和token)
首先根据userId返回ip规则实体列表(sys_ip_rule),然后根据HttpRequest获取当前请求的IP,然后从Reids查询是否有当前IP被lock的记录 如果被锁住就抛出10015,如果没有被锁住就看ip是否在ipAccess中(webcoreatd.sys_ip_rule.access_ip),如果不在允许范围内就抛出10008;
PasswrodExpiredInterceptorImpl 密码是否超期拦截(before和token)
首先获得安全策略中的密码期限然后根据最后一次修改密码的时间或者创建用户的时间 和密码期限来判断密码是否过期,如果密码过期抛出10009;
TimeRuleInterceptorImpl 时间规则拦截(before和token)
进入方法会先判断一次当前用户是否被锁住,然后根据用户id获取对应的时间拦截规则(sys_time_rule)(周几,时间),然后判断当前时间是否允许当前用户登录,如果不符合规则那么抛出10007;
TwofactorInterceptorImpl 双因子拦截(after)
首先从安全策略中获取是否开启双因子拦截或者开启了邮箱验证拦截或者短信验证拦截,如果双因子拦截失败抛出10006,如果只开了邮箱验证拦截并且验证失败抛出10021,如果只开启了短信验证拦截并且拦截失败那么抛出10022;
如果上述拦截都通过了那么就调用Springsecurit的安全验证方法 传入upToken返回一个authentication , 然后根据authentication设置安全上下文 , 如果账户失效(根据报错信息)那么抛出10010,如果认证失败抛出10010。
认证成功后会调用auth方法 验证密码是否匹配,如果密码不正确 返回10005
接下来根据passwordTimeLimit和lastModifyPasswordTime或createTime设置密码剩余时间
如果如果上述过程出现了异常10005 那么调用loginFail方法根据安全策略计算剩余输入密码机会 并抛出10005密码错误,剩余n次机会。
如果上面的过程都没有问题那就说明登录成功了,调用loginSuccess方法记录相关属性,(token创建时间(mysql)和页面过期时间(redis))然后根据当前时间和用户名生成一个JWT的token,并且返回token,登陆成功。
登出接口
/user/loginOut
从request中获取实体信息
request.getAttribute("requestUserEntity");
然后通过操作使token无效
//该操作会使token无效
userEntity.setIssueTokenTime(LocalDateTime.now().plusDays(1));
securityUserMapper.updateById(userEntity);
///因为这个方法
//验证校验是否失效
jwtValidateToken.isExpired(claims, securityUserEntity.getIssueTokenTime());
这个方法也使用了这个操作 可能不对?
涉及到的mysql表以及redis key
MYSQL表:
sys_ip_rule
id | 主键 |
---|---|
user_id | 用户id |
access_ip | 可访问ip |
sys_login_fail
id | 主键 |
---|---|
fail_type | 登录失败错误类别: 1,用户名、密码错误 2、双因子验证码不对 3、 |
fail_reason | 登录失败原因描述 |
fail_login_username | 登陆失败用户名 |
fail_login_password | 登陆失败用户的密码 |
fail_login_ip | 登陆失败用户的ip |
fail_login_time | 登陆失败的时间 |
sys_menu
id | |
---|---|
url | url |
menu_describe | 资源描述信息 |
menu_level | 资源的层级 |
path | 资源的完整路径 |
parent_id | 上级资源ID |
is_view | 当前资源是否可以显示0表示可以显示,1表示不可以显示 |
order_num | 资源的顺序编号 |
op_id | |
op_value | |
icon | |
component | |
type | |
front_path |
sys_password_record
id | |
---|---|
user_id | |
password | |
change_time |
sys_role
id | |
---|---|
role_name | |
remark | |
create_time |
sys_role_menu
id | |
---|---|
role_id | |
menu_id | |
menu_authority | 菜单权限,0: 仅查看 1:可编辑 |
sys_security_policy
id | |
---|---|
is_enable_first_login_change_password | 是否开启,初次登录修改密码策略(1:开启 0:关闭) |
is_enable_strong_password | 是否开启,强密码策略(1:开启 0:关闭) |
password_time_limit | 密码使用时间期限策略,单位: 天 (0表示无期限) |
historical_password_check_count | 新密码不能与近期的多少次历史密码重复。0为不进行校验 |
page_timeout | 页面超时时间,单位:分 (0:不超期) |
is_enable_two_factor | 是否开启双因子认证(0:不开启 1:开启) |
is_enalbe_lock_fail_ip | |
cheak_time | |
login_fail_times | |
lock_time | |
password_length | |
have_number | |
have_lowercase | |
have_capital | |
have_special | |
is_enable_mail_factor | |
is_enable_sms_factor |
sys_time_rule
id | |
---|---|
user_id | |
access_week | 可访问星期几 |
access_time_begin | 可访问时间段起始,格式 HHmmss |
access_time_end | 可访问时间段结束,格式 HHmmss |
sys_user
id | 主键 |
---|---|
role_id | 主键 |
username | 用户名,登录系统帐号 |
password | 密码 |
realname | 用户真实名称 |
电子邮箱 | |
tel | 手机号码 |
jobnumber | 工号 |
status | 用户状态: 1启用 0禁用,默认1 |
expired_time | 超期时间戳 |
issue_token_time | 最近一次登录的token发放时间 |
last_modify_password_time | 最近一次密码修改时间 |
create_user_id | 创建者id |
create_time | 用户创建时间 |
branch_id | 分支id |
redis key
- 登录成功后在redis中设置当前用户的页面过期时间 username+“pageTimeOut”,issue
redisTemplate.opsForValue().set(userDetails.getUsername() + "pageTimeOut", issue);
- 发送短信并将验证码存在redis
redisTemplate.opsForValue().set("login" + tel, code);
- 密码输错次数过多 锁住该用户
redisTemplate.opsForValue().set("lock" + dto.getUsername(), dto.getUsername(), lockTime.intValue(), TimeUnit.MINUTES);
- 使用redis存储邮箱验证码
redisTemplate.opsForValue().set("login" + email,code,5, TimeUnit.MINUTES);
- 邮箱短信双因子验证
redisTemplate.opsForValue().set("login" + tel, code);
优化方案:
取消不带token可以直接访问
获取请求路径 如果是登录那么直接放过 uri : “/auth/login”
如果是登出 setSecurityUserEntity之后放过 uri : “/user/loginOut”
如果是swagger 放过 url: “http://localhost:15000/*”