20230619学习记录

1. 20230619学习记录

目标

  • 基于token的简单权限认证
  • 线程的生产者消费者模型

1.1. 基于token的简单权限认证

为原本简答的登录认证,添加权限认证。

流程:登录时判断用户名密码,用户名和密码匹配成功的同时也获取权限,将权限作为jwt的载荷,每次的用户的操作在拦截器验证token时,验证权限是否匹配。

1.1.1. 设计表

用户一般有多个权限,而实际设计中,用户不直接和权限进行关联。而是先将用户分角色管理,在对角色关联不同的权限。

三表的关系:

(在此只设计一个用户对应一个角色的情况)

用户与角色为一对多的关系,一个用户对应一个角色,一个角色可以属于多个用户

角色与权限为多对多的关系,一个角色对应多个权限,一个权限可以属于多个角色

  1. 角色表

image-20230619104627048

  1. 权限表

    此处权限用方法名模拟,表示访问controller层方法的权限

    image-20230619104524894

  2. 角色权限中间表

    image-20230619104715091

  3. 用户权限

    将查询用户权限的语句的结果作为视图保存,相当于一张用户权限表

    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. 拦截器

  1. 自定义一个注解,用于标识访问方法需要的权限

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Privs {
        String value() default "";
    }
    
  2. 在拦截器中通过参数handler(HandlerMethod)获取请求访问的方法的注解,通过匹对注解值和token载荷携带的用户权限,判断是否可以访问

    handler不一定是HandlerMethod类型,静态资源的访问被拦截时handlerResourceHttpRequestHandler

    //*******判断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();

}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值