1. 20230619学习记录
目标
- 基于token的简单权限认证
- 线程的生产者消费者模型
文章目录
1.1. 基于token的简单权限认证
为原本简答的登录认证,添加权限认证。
流程:登录时判断用户名密码,用户名和密码匹配成功的同时也获取权限,将权限作为jwt的载荷,每次的用户的操作在拦截器验证token时,验证权限是否匹配。
1.1.1. 设计表
用户一般有多个权限,而实际设计中,用户不直接和权限进行关联。而是先将用户分角色管理,在对角色关联不同的权限。
三表的关系:
(在此只设计一个用户对应一个角色的情况)
用户与角色为一对多
的关系,一个用户对应一个角色,一个角色可以属于多个用户
角色与权限为多对多
的关系,一个角色对应多个权限,一个权限可以属于多个角色
- 角色表
-
权限表
此处权限用方法名模拟,表示访问controller层方法的权限
-
角色权限中间表
-
用户权限
将查询用户权限的语句的结果作为视图保存,相当于一张用户权限表
select u.username as username, u.password as password, p.name as name from user_table u left join role_table r on u.role_id = r.id left join role_privs_table rp on r.id = rp.role_id left join privs_table p on rp.privs_id = p.id
保存为视图
user_privs_view
1.1.2. 建立实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_privs_view")
public class UserPrivs {
@TableField("username")
private String username;
@TableField("privs_name")
private String privs;
}
1.1.3. dao接口
IUserPrivsDao
1.1.4. service
登录业务 查询表:user_privs_view
@Override
public List<UserPrivs> login(String username, String password) {
//判断用户名密码
LambdaQueryWrapper<User> userWrapper = new LambdaQueryWrapper<>();
userWrapper.eq(User::getUsername,username).eq(User::getPassword,password);
User user = userDao.selectOne(userWrapper);
if(null != user){
//用户名密码正确
//查询权限
LambdaQueryWrapper<UserPrivs> privsWrapper = new LambdaQueryWrapper<>();
privsWrapper.select(UserPrivs::getPrivs).eq(UserPrivs::getUsername, username);
List<UserPrivs> userPrivs = userPrivsDao.selectList(privsWrapper);
return userPrivs;
}else {
//用户名密码错误
return null;
}
}
1.1.5. controller
创建jwt,装载权限数据,每个用户多个权限,多个权限字符串拼接起来装载,然后存入redis
@PostMapping("/login")
public RespData login(String username, String password, HttpServletResponse response){
List<UserPrivs> userPrivs = userService.login(username, password);
if(null!= userPrivs && !userPrivs.isEmpty()){
//登录成功
//创建载荷内容
StringBuilder sb = new StringBuilder();
userPrivs.forEach(u -> {
sb.append(u.getPrivs());
sb.append(",");
});
sb.deleteCharAt(sb.length() - 1);
Map<String, Object> map = new HashMap<>();
map.put("username",username);
map.put("privs",sb.toString());
//创建jwt
String token = MyJwtUtil.buildJwt(map, 1000L * 60);
//将jwt传入redis,设置过期时间2分钟(jwt过期时间1分钟)
redisTemplate.opsForValue().set("token:"+token,token,2, TimeUnit.MINUTES);
response.addHeader("token",token);
return RespData.success(ResultCode.USER_LOGIN_SUCCESS, username);
}else {
//登录失败
return RespData.fail(ResultCode.USER_LOGIN_ERROR,null);
}
}
1.1.6. 拦截器
-
自定义一个注解,用于标识访问方法需要的权限
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Privs { String value() default ""; }
-
在拦截器中通过参数
handler(HandlerMethod)
获取请求访问的方法的注解,通过匹对注解值和token载荷携带的用户权限,判断是否可以访问handler
不一定是HandlerMethod
类型,静态资源的访问被拦截时handler
是ResourceHttpRequestHandler
//*******判断token头信息存在后,并且redis中还未过期后,进行权限判断 //判断权限 if(handler instanceof HandlerMethod){ String privs = (String) MyJwtUtil.getPayload(jwt, "privs"); Privs annotation = ((HandlerMethod) handler).getMethod().getAnnotation(Privs.class); if( privs != null && privs.contains(annotation.value()) ){ //有权限访问 return true; }else { //无权限访问 log.debug("无权限访问"); String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "无权限访问", null)); response.getWriter().write(jsonStr); return false; } }else { return true; }
1.2. 线程的生产者消费者模型
生产者消费者模型是一种多线程并发协作的模型
模型主要由两类线程和一个存放数据区构成
- 生产者线程:产生数据,并放到数据区
- 消费者线程:从数据区拿取数据使用
因为这个数据区域的存在,生产者可以主要关注自己的生产,而消费者可以主要关注自己的消费,做到两类线程(or 程序)之间的解耦,异步
两者也不是完全没有联系,平时生产者生产和消费者消费的过程中也要通知对方,进行消费和生产;当数据区内数据占满,生产者可以休息;当数据区内数据空了,消费者可以休息,但是只要有一方工作,就得通知另一方开始工作;基于这样的通知机制,生产速度太快,可以调整生产者数量,消费太快,可以调整消费者数量,这样就能平衡两者之间的处理能力。
可以使用线程的wait()
和notify()(notifyAll())
这两个方法实现生产者消费者之间的通知机制
现模拟生产者线程生产汉堡放进diningTable
队列,消费者线程消费汉堡,生产消费的速度由线程的sleep控制
1.2.1. 生产消费执行的具体方法
private static final int SIZE = 30;
private Queue<Hamburger> diningTable = new LinkedBlockingDeque<>();
public synchronized void producer(){
if(diningTable.size() == SIZE){
//队列满了,生产者wait
System.out.println("队列满了,生产者wait");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//队列未满,生产者生产
Hamburger hmbg = new Hamburger("香辣鸡腿堡",29.9);
diningTable.add(hmbg);
System.out.println("【生产者】产出一个汉堡,剩余汉堡:"+diningTable.size());
//唤醒wait状态的线程(唤醒wait状态的消费者线程)
notifyAll();
}
}
public synchronized void consumer(){
if(diningTable.isEmpty()){
//队列空了,消费者wait
System.out.println("队列空了,消费者wait");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//队列未空,消费者消费
diningTable.poll();
System.out.println("【消费者】消费一个汉堡,剩余汉堡:"+diningTable.size());
//唤醒wait状态的线程(唤醒wait状态的生产者线程)
notifyAll();
}
}
1.2.2. 使用实现Runnable接口的方法创建线程
public class ProducerThread implements Runnable {
private KFC kfc;
public ProducerThread(KFC kfc) {
this.kfc = kfc;
}
@Override
public void run() {
while (true){
kfc.producer();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ConsumerThread implements Runnable {
private KFC kfc;
public ConsumerThread(KFC kfc) {
this.kfc = kfc;
}
@Override
public void run() {
while (true) {
kfc.consumer();
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.2.3. 线程启动
public static void main(String[] args) {
KFC kfc = new KFC();
Thread p1 = new Thread(new ProducerThread(kfc));
Thread p2 = new Thread(new ProducerThread(kfc));
Thread p3 = new Thread(new ProducerThread(kfc));
Thread p4 = new Thread(new ProducerThread(kfc));
Thread c1 = new Thread(new ConsumerThread(kfc));
Thread c2 = new Thread(new ConsumerThread(kfc));
Thread c3 = new Thread(new ConsumerThread(kfc));
Thread c4 = new Thread(new ConsumerThread(kfc));
Thread c5 = new Thread(new ConsumerThread(kfc));
Thread c6 = new Thread(new ConsumerThread(kfc));
p1.start();
p2.start();
p3.start();
p4.start();
c1.start();
c2.start();
c3.start();
c4.start();
c5.start();
c6.start();
}
erThread(kfc));
Thread c5 = new Thread(new ConsumerThread(kfc));
Thread c6 = new Thread(new ConsumerThread(kfc));
p1.start();
p2.start();
p3.start();
p4.start();
c1.start();
c2.start();
c3.start();
c4.start();
c5.start();
c6.start();
}