一、用户角色权限管理模块设计
“用户-角色-权限”管理是 “访问控制” 的一种实现方式
RBAC
Shiro框架,安全框架
1、用户密码加密
加盐hash
加盐加密与验证的逻辑:
- 用户注册时,输入用户名密码(明文),向后台发送请求
- 后台将密码加上随机生成的盐并 hash,再将 hash 后的值作为密码存入数据库,盐也作为单独的字段存起来
- 用户登录时,输入用户名密码(明文),向后台发送请求
- 后台根据用户名查询出盐,和密码组合并 hash,将得到的值与数据库中存储的密码比对,若一致则通过验证
改了一下result,整合了返回响应码,建了一个枚举类 ResultCode,建了一个ResultFactory 通过静态方法 来调用返回 new Result(resultCode);
(Result里 这两个值 我不知道干嘛用的,删了逻辑正常 // private String message; //响应提示信息 private Object object; //响应结果对象) 以后需要可以再加上
注册中的加密:
@PostMapping("api/register")
@ResponseBody
public Result register(@RequestBody User user){
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
// 设置 hash 算法迭代次数
int times = 2;
// 得到 hash 后的密码
String encodedPassword = new SimpleHash("md5", password, salt, times).toString();
userService.add(user); //user.set方法设置属性;service写入数据库
}
生成盐,结合输入的密码 hash后加密存起来
2、Shiro 认证登录
Shiro,需要理解三个核心概念:Subject、SecurityManager 和 Realms。
Subject: 穿了马甲的用户类,负责存储与修改当前用户的信息和状态;;
SecurityManager: Subject 背后的女人,安全相关的操作实际上是由她管理的。只用在项目中配置一次,就可以忘掉她了;
Realms: Shiro 和安全相关数据(比如用户信息)的桥梁,也就是说,Realm 负责从数据源中获取数据并加工后传给 SecurityManager。
可以通过配置使用特定的 Realm 替代 DAO,和 JPA 类似,Realm 获取数据的方法被封装了起来,但是数据库中的表名、字段等需要与源码预定义的查询保持一致,
所以在我们的项目中获取数据的功能仍旧可以交给 JPA 完成,Realm 只负责加工并传递这些数据。
Shiro 配置
1)首先 添加 Shiro的 maven 依赖;
2)创建 Realm 并重写 获取 认证 与 授权 信息的方法
3)创建配置类,包括创建并配置 SecurityManager 等
第二步中
新建 WJRealm类,继承 AuthorizingRealm,重写 doGetAuthorizationInfo、doGetAuthenticationInfo 方法
返回值两个类 AuthorizationInfo 授权、AuthenticationInfo 认证
获取认证信息,即根据 token 中的用户名从数据库中获取密码、盐等并返回
public class WJRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 简单重写获取授权信息方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
return s;
}
// 获取认证信息,即根据 token 中的用户名从数据库中获取密码、盐等并返回
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = token.getPrincipal().toString();
User user = userService.getByUserName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt), getName());
return authenticationInfo;
}
}
token.getPrincipal().toString(); 获取token里的用户名
因为SimpleAuthenticationInfo() 要求 salt为byte[]类型,所以从String转了回去;
第三步
新建 ShiroConfiguration配置类
Controller中 使用Shiro 验证 密码:
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, requestUser.getPassword());
subject.login(usernamePasswordToken);
就这一句 就可以执行验证,
这里面 大概经过七八层调用,Shiro 通过 Realm 里我们重写的 doGetAuthenticationInfo
方法获取到了验证信息,再根据我们在配置类里定义的 CredentialsMatcher(HashedCredentialsMatcher),执行 doCredentialsMatch方法(shiro自己写的)判断是否匹配,
贴出来方便理解:
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenHashedCredentials, accountCredentials);
}
accountCredentials
获取到了我们存在数据库中的 hash 后的密码, 而 tokenHashedCredentials
则调用 hash 算法根据 salt 和客户端传入的 password 算出了 hash 值,他俩是byte[] 类型
再分析 equals 方法,发现最终调用的是 java.security.MessageDigest
包中的 isEqual()
方法,讲道理这个方法才是逻辑的核心(比较两个 hash 值 是否相等)
3、登出功能
使用shiro 实现用户登出。
Subject subject = SecurityUtils.getSubject();
subject.logout();
默认 Subject 接口是由 DelegatingSubject 类实现,调用其 logout
方法,
this.session = null; this.principals = null; this.authenticated = false;
引入vuex
1) cnpm install vuex -S
2) 在src目录下,创建store模块,与router同级,创建 index.js, state.js, 还有其他三个 actions/mutations/getters.js
3) 除index.js 之外,引入 export default{ };在index.js中, 新建vuex的store实例
4)main.js中 导入 store 实例
4、完善的访问拦截
1.认证方案(session 与 token)
session和token 可以 在某种程度上 解决 :
以往的方式 : 前 端在每次请求时都加上用户名和密码,交由后端验证。
- 一,需要频繁查询数据库,导致服务器压力较大
- 二,安全性,如果信息被截取,攻击者就可以 一直 利用用户名密码登录(注意不是因为明文不安全,是由于无法控制时效性)
- session
管理用户状态, 比如控制会话存在时间,在会话中保存属性
通常的作用方式:
- 后端 (服务器 )接收到第一个请求时,生成 session 对象,并通过 响应头 告诉 前端(客户端/ 浏览器) 在 cookie 中放入 sessionId
- 前端之后发送请求时,会带上包含 sessionId 的 cookie
- 服务器通过 sessionId 获取 session ,进而得到当前用户的状态(是否登录)等信息
即,前端 只需要在 登录 的时候发送一次 用户名密码,此后只需要 在发送请求 时带上 sessionId, 后端服务器 就可以验证用户是否登录了
- token
cookie是一个 存放 seeionId的东西;session存放在后端服务器内存中;
token 的优势是无需服务器存储!!!令牌
token 解决方案是 JWT
作用流程:
- 用户使用用户名密码登录,服务器验证通过后,根据用户名(或用户 id 等),按照预先设置的算法生成 token,其中也可以封装其它信息,并将 token 返回给客户端(可以设置到客户端的 cookie 中,也可以作为 response body)
- 客户端接收到 token,并在之后发送请求时带上它(利用 cookie、作为请求头或作为参数均可)
- 服务器对 token 进行解密、验证
签名算法、用 base64 编个码,方便传输
2.客户端存储方案 (cookie、localStorage、sessionStorage)
明文 用户名密码,还是 sessionId 和 token,都可以用这三种方式存储
cookie 可以作为传递的参数,并可通过后端进行控制,
local/session Storage 则主要用于在客户端中保存数据,其传输需要借助 cookie 或其它方式完成。
通常来说,在可以使用 cookie 的场景下,作为验证用途进行传输的用户名密码、sessionId、token 直接放在 cookie 里即可。
而后端传来的其它信息则可以根据需要放在 local/session Storage 中,作为全局变量之类进行处理。
3.后端登录拦截
shiro 的安全管理,实际上是基于会话实现的,使用session方案。
subject.login() 该过程会产生 session,并自动把 sessionId 设置到 cookie。
由pastman工具可见 , sessionId 在 tomcat 中的叫 JSESSIONID
。
完善的访问拦截 就是:
前端带上 sesisonId 发送请求交由后端 认证,坑爹之处主要在于 前后端分离的情况下 需要额外的配置解决 跨域问题。
默认的情况下,跨域的 cookie 是被禁止的,后端不能设置,前端也不能发送,所以两边都要设置。
1、后端设置 LoginInterceptor()类 preHandle
方法, 放行 options 请求
小知识:OPTIONS请求即 预检请求,可用于检测服务器允许的http方法。当发起跨域请求时,由于安全原因,触发一定条件时浏览器会在正式请求之前自动先发起OPTIONS请求,即CORS预检请求,服务器若接受该跨域请求,浏览器才继续发起正式请求。
2、配置类 MyWebConfigurer
做一些修改,主要是 addCorsMappings
方法
允许跨域的 cookie cookie就是Credentials证书吗???
4、前端拦截
前端 在 main.js
中 , 通过 axios 主动开启 withCredentials
,为了让前端能够带上 cookie
这样,前端每次发送请求时就会带上 sessionId,shiro 就可以通过 sessionId 获取登录状态并执行是否登录的判断。
为实现 前端页面拦截:
-beforeEach的使用
使用场景: 一般用在跳转前需要做校验的地方,如:进入首页前做登录校验,如果已登录则跳转首页,否则跳转登录页。
使用的地方:
如果是做跳转首页前做登录校验,需要写在main.js文件中,表示在所有路由被访问之前做校验;
结果:访问每个页面前都向后端发送一个请求,目的是经由拦截器验证服务器端的登录状态
论bug是怎么产生的???
bug一:
login转index转不了
直接登录index以及其他页面也进不了
靠前端实现的拦截
“全局前置守卫”(router.beforeEach)前端拦截登录
后端判断是否登录以及登录的是谁?
localStorage, 主要用于在前端中 保存数据,其传输需要借助 cookie 或其它方式完成; 仅在客户端(即浏览器)中保存,不参与和服务器的通信
跨源资源共享(CORS),允许您灵活地指定什么样的跨域请求被授权
我们理一下逻辑:
shiro 的安全管理,用户加密与验证 我们用到了subject
subject.login 会产生 session,并自动把 sessionId 设置到 cookie。
浏览器之后再发送请求 访问的时候,可以带上cookie 的这个sessionid;服务器 通过sessionid获得session,即用户的登录状态
目标:
1)、靠前端实现的拦截 :实现
2)、靠后端实现拦截 : LoginInterceptor 认证一下是否subject.isAuthenticated() 、 MyWebConfigurer 在遇到哪些请求 生成拦截器
3)、解决跨域 :开启 cors,允许跨域的cookie
我们做了啥
前端:
第一步,靠前端实现的拦截
2、main.js中,通过 axios 主动开启 withCredentials
3、修改 router.beforeEach
方法, 访问每个页面前发送'/authentication' 请求,
访问每个页面前都向后端发送一个请求,目的是经由拦截器验证服务器端的登录状态
后端:
1、LoginInterceptor
, preHandle
方法 放行options请求
2、MyWebConfigurer
, addCorsMappings
方法 .allowCredentials(true)允许跨域的cookie, 配置了适用于特定的路径模式的CORS
3、后端 '/authentication' 这个接口 暂时写成空的
结果:
通过 后端12,前端2 可实现 目标2 3:
前端每次发送请求时就会带上 sessionId,shiro 就可以通过 sessionId 获取登录状态并执行是否登录的判断。
通过 前后端的3 可实现:
实现 页面的拦截,不能访问本来需要登录才能访问的页面
目标 2 3 完成,做 目标 1:
实现前端拦截
1、引入vuex,写store index.js
在项目打开的时候会判断本地存储中是否有 user 这个对象存在,如果存在就取出来并获得 username
的值,否则则把 username
设置为空。
2、 router\index.js, 在需要拦截的路由中加 meta: { requireAuth: true }
3、使用钩子函数
钩子函数是 在某些时机会被调用的函数。这里我们使用 router.beforeEach()
,意思是在访问每一个路由前调用。
1)在src\main.js
,导入store; // if (store.state.user.username) 不用username还报错
2)在Vue对象里 添加 store;3)写router.beforeEach ;4)改login.vue
必须要用_this.$store.commit (‘SET_TOKEN’, tokenV) 调用mutations里的方法,才能在store存储成功
bug一 解决
bug二 这怎么remmberme全是false
他这俩全是fasle我怎么登录进去的啊????!!!
bug三 我图书馆里的书呢???
浏览器发送url请求,首先通过MyWebConfigurer 判断是否拦截,然后触发LoginInterceptor。
// 白卷时间到了,我得看 springbootplus+mybatisplus了