【SpringBoot+Redis】实现多端登录+token自动续期和定期刷新+自定义注解和拦截器实现鉴权(角色和权限校验)

news2025/7/10 8:08:33

目录

  • 前言
  • 思路
    • 1、登录、token相关
    • 2、鉴权相关
  • 实现
    • 一、登录
      • 1、先定义一个Component组件
      • 2、登录、退出
    • 二、鉴权、token相关
      • 1、自定义注解
      • 2、拦截器鉴权、token续期和定期刷新
      • 3、新增/更新角色时,更新redis中角色对应的权限
      • 4、更新菜单权限标识时,更新redis中对应的权限标识
  • 最后
    • 源码

前言

前面写了几篇鉴权框架SaToken的使用的文章,用这个框架我们很容易就实现了多端登录、单点登录、鉴权等这些功能(详情可以看看这两篇链接)

【SaToken使用】springboot+redis+satoken权限认证

【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权

然后又写了如果项目中没有权限这些东西,不使用SaToken,如何实现多端登录、防重登录、token续期等需求的文章:

SpringBoot+Redis 防止用户重复登录

现在我闲来无事,我想在多端登录、防重登录、token续期这些基础上再实现注解鉴权,而且不使用SaToken,我们自定义注解+拦截器去实现。在实现的过程中,我尝试参考SaToken的源码,边阅读边自己去实现,奈何自己能力有限,啃源码的过程还是挺吃力的。。。那些具体实现要一层一层找过去,大大增加了阅读难度。。。不说了,再说下去要被自己菜哭了😭我们还是说正事吧。

思路

1、登录、token相关

首先,我按照SaToken的方式,将登录相关、退出、token相关的这些操作,全部抽出来,放到一个自定义的@Component组件中,在这里实现具体过程,其他地方我们不用关心实现步骤,只需要直接调用这里面的相关方法就行。

登录时在redis中,我们需要存三个东西:一个是token,一个是登录用户信息,还有一个是token的创建时间(用于定期刷新token)。

2、鉴权相关

一般权限设计都是用角色进行关联的,我们需要考虑每次鉴权时:用户对应的角色——>角色对应的权限,这两个东西从哪里拿?

如果从数据库拿,请求数据库是否过于频繁?

如果从缓存中拿,那么每次修改角色或菜单的权限标识时,就要更新对应的缓存,操作是否过于麻烦?

上面这两个大概就是两种获取方式的缺点,然后综合考虑之下,我选择了第二种方式,从缓存中拿角色和权限,虽然更新麻烦一点,但不用每校验一次就查一遍数据库,可以减轻数据库的压力

既然有了思路和解决方式,那就开干!

实现

一、登录

1、先定义一个Component组件

用于实现登录和token相关的操作

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.common.base.BaseConstant;
import com.common.vo.ExceptionVo;
import com.entity.sys.SysRoleMenu;
import com.entity.sys.SysUser;
import com.service.sys.SysRoleMenuService;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 登录、token相关组件工具类
 */
@Component
public class LoginUtil {

    private RedisUtil redisUtil = SpringUtil.getBean(RedisUtil.class);
    private SysRoleMenuService sysRoleMenuService = SpringUtil.getBean(SysRoleMenuService.class);
    private long tokeTime = 3600;

    /**
     * 登录
     * @param user 登录的用户信息
     */
    public String login(SysUser user){
        return login(user,null,tokeTime);
    }

    /**
     * 登录(指定登录类型)
     * @param user 登录的用户信息
     * @param loginType 登录类型
     */
    public String login(SysUser user,String loginType){
        return login(user,loginType,tokeTime);
    }

    /**
     * 登录(指定登录类型和有效时长)
     * @param user 登录的用户信息
     * @param loginType 登录类型
     * @param time token有效期
     */
    public String login(SysUser user,String loginType,long time){
        String str = DigestUtil.md5Hex(String.valueOf(user.getId())); // userId加密
        //获取所有包含当前用户id加密后的字符串开头的key
        redisUtil.del(redisUtil.allKey(BaseConstant.cachePrefix + str + "*")); // 将旧的key删除
        if (loginType != null) str = loginType + "_" + str;
        String token = str + EncryptionUtil.generateRandom(32,false); // token组成为 用户id加密字符串+随机32为字符串
        Map<String,Object> map = new HashMap<>();
        map.put("token",token);
        map.put("createTime",System.currentTimeMillis());
        map.put("user",user);
        redisUtil.hmset(BaseConstant.cachePrefix+token,map,time);
        return token;
    }

    /**
     * 退出(根据token退出)
     */
    public void logout(String token){
        redisUtil.del(token);
    }

    /**
     * 获取当前登录用户的token
     */
    public String getToken(HttpServletRequest request){
        String token = request.getHeader(BaseConstant.tokenHeader);
        if (token == null || token.length() == 0 || !redisUtil.hasKey(BaseConstant.cachePrefix+token)){
            throw new ExceptionVo(1003,"用户未登录");
        }
        return token;
    }

    /**
     * 获取当前用户的剩余有效时长
     */
    public long getTimeOut(String token){
        return redisUtil.getExpire(BaseConstant.cachePrefix+token);
    }

    /**
     * 获取当前登录信息
     */
    public Map<String,Object> getLogin(String token){
        return redisUtil.hmget(BaseConstant.cachePrefix+token);
    }

    /**
     * 获取当前用户
     */
    public SysUser getLoginUser(String token){
        return (SysUser) redisUtil.hget(BaseConstant.cachePrefix+token,"user");
    }

    /**
     * 获取当前用户token的创建时间
     */
    public long getCreateTime(String token){
        return (Long) redisUtil.hget(BaseConstant.cachePrefix+token,"createTime");
    }

    /**
     * 获取当前用户的所有权限集合
     */
    public List<String> getPermList(String token,List<String> roleList){
        if (roleList == null){
            roleList = getRoleList(token);
        }
        List<String> permList = new ArrayList<>();
        if (redisUtil.hasKey(BaseConstant.rolePermList)) { // 如果redis中有所有角色对应的所有权限,直接从redis中拿
            Map<String, Object> map = redisUtil.hmget(BaseConstant.rolePermList);
            for (String role : roleList) {
                permList.addAll((List<String>) map.get(role));
            }
        }else {
            // redis没有,则去数据库查询,并将查询到的数据设置到redis中
            List<SysRoleMenu> allRolePerm = sysRoleMenuService.listAll();
            Map<String, List<SysRoleMenu>> group = allRolePerm.stream().collect(Collectors.groupingBy(SysRoleMenu::getRoleId));
            Map<String, List<String>> map = new HashMap<>();
            for (String key : group.keySet()) {
                List<String> perms = group.get(key).stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList());
                map.put(key,perms);
                if (roleList.contains(key)){
                    permList.addAll(perms);
                }
            }
            redisUtil.hmset(BaseConstant.rolePermList,map);
        }
        return permList;
    }

    /**
     * 获取当前用户的所有角色集合
     */
    public List<String> getRoleList(String token){
        String[] roleIds = getLoginUser(token).getRoleIds();
        return Arrays.asList(roleIds);
    }

    /**
     * 刷新过期时间
     */
    public void updateTimeOut(String token){
        updateTimeOut(token,tokeTime);
    }

    /**
     * 刷新过期时间(指定过期时间)
     */
    public void updateTimeOut(String token,long time){
        redisUtil.hmset(BaseConstant.cachePrefix+token,getLogin(token),time);
    }

    /**
     * 更新用户信息
     */
    public void updateUser(String token,SysUser user){
        redisUtil.hset(BaseConstant.cachePrefix+token,"user",user);
    }

}

2、登录、退出

登录

@Resource
private LoginUtil loginUtil;

/** 登录 */
@PostMapping("/login")
public ResultUtil login(String userName, String password, String code,HttpServletRequest request){
    try {
        code = code.toUpperCase();
        Object verCode = redisUtil.get(BaseConstant.verCode+code);
        if (Objects.isNull(verCode)) {
            return ResultUtil.error("验证码已失效,请重新输入");
        }
        redisUtil.del(BaseConstant.verCode+code);
        password = RSAUtil.decrypt(password);   //密码私钥解密
        SysSafe safe = sysSafeService.list().get(0);
        SysUser user = passwordErrorNum(userName, password,safe);// 校验用户名密码是否正确
        int i = safe.getIdleTimeSetting(); //如果系统闲置时间为0,设置token和session永不过期
        String token = "";
        if (i==0){
            token = loginUtil.login(user,null,-1);
        }else {
            token = loginUtil.login(user);
        }
        return ResultUtil.success(token);
    } catch (ExceptionVo e) {
        return ResultUtil.error(e.getCode(),e.getMessage());
    }catch (Exception e) {
        e.printStackTrace();
        return ResultUtil.error(BaseConstant.UNKNOWN_EXCEPTION);
    }
}

退出

/** 退出登录 */
@DeleteMapping("/logout")
public ResultUtil logout(HttpServletRequest request){
    String token = request.getHeader(BaseConstant.tokenHeader);
    //根据token退出登录
    loginUtil.logout(token);
    return ResultUtil.success("退出登录成功");
}

获取当前登录用户信息

/**
 * 获取当前登录用户信息
 */
@GetMapping("/getLoginUser")
public ResultUtil getLoginUserInfo() {
    Map<String,Object> map = new HashMap<>();
    SysUser user = getLoginUser();
    map.put("user",user);
    //查询角色和权限
    map.put("permissions",loginUtil.getPermList(token(),Arrays.asList(user.getRoleIds())));
    return ResultUtil.success(map);
}

公共的controller类

import com.common.util.LoginUtil;
import com.entity.sys.SysUser;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;


public abstract class BaseController<S extends BaseService, E extends BaseEntity>{

    @Autowired(required=false)
    protected S service;
    @Resource
    protected LoginUtil loginUtil;
    @Resource
    protected HttpServletRequest httpServletRequest;

    public String token(){
        return service.token();
    }

    /**
     * 获取当前登录的用户
     */
    public SysUser getLoginUser(){
        return service.getLoginUser();
    }

    public String loginId(){
        return service.loginId();
    }

}

公共的service类

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.common.util.LoginUtil;
import com.common.util.RedisUtil;
import com.entity.sys.SysUser;
import com.entity.sys.query.SysQuery;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class BaseService<M extends BaseMapper<E>, E extends BaseEntity> extends ServiceImpl<M, E>{

    @Resource
    protected RedisUtil redisUtil;
    @Resource
    protected LoginUtil loginUtil;
    @Resource
    protected HttpServletRequest httpServletRequest;

    /**
     * 获取当前token
     */
    public String token(){
        return loginUtil.getToken(httpServletRequest);
    }

    /**
     * 获取当前登录的用户
     */
    public SysUser getLoginUser(){
        return loginUtil.getLoginUser(token());
    }

    /**
     * 获取当前登录用户的id
     */
    public String loginId(){
        return getLoginUser().getId();
    }

    /**
     * 更新redis中的权限标识
     */
    public void updatePerm(String oldPerm,String newPerm){
        Map<String, Object> map = redisUtil.hmget(BaseConstant.rolePermList);
        for (String key : map.keySet()) {
            List<String> permList = (List<String>) map.get(key);
            int index = permList.indexOf(oldPerm); // 获取旧权限标识的索引
            if (index != -1){
                permList.set(index,newPerm); // 更新新标识
            }
        }
        redisUtil.hmset(BaseConstant.rolePermList,map);
    }

    /**
     * 删除redis中的角色与权限关联
     */
    public void deleteRolePerm(String roleId){
        redisUtil.hdel(BaseConstant.rolePermList,roleId);
    }

	// ......省略其他代码

}

上面我们就实现了多端登录功能,token自动续期和定期刷新是在拦截器中实现的,和鉴权的拦截器是同一个,我们放到后面说。

二、鉴权、token相关

1、自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 权限校验
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckPermission {

    /** 拥有指定权限 */
    String[] perm() default {};

    /** 拥有指定角色 */
    String[] role() default {};

    /**
     * 校验类型:
     *  0: 只校验权限,多个权限需要同时满足;
     *  1: 只校验权限,多个权限满足一个即可;
     *  2: 只校验角色,多个角色需要同时满足;
     *  3: 只校验角色,多个角色满足一个即可;
     *  4: 校验权限和角色,需要权限和角色同时满足;
     *  5: 校验权限和角色,权限和角色任意满足一个;
     * @return
     */
    int type() default 0;
}

2、拦截器鉴权、token续期和定期刷新

import cn.hutool.extra.spring.SpringUtil;
import com.common.anno.CheckPermission;
import com.common.base.BaseConstant;
import com.common.util.LoginUtil;
import com.common.vo.ExceptionVo;
import com.entity.sys.SysUser;
import com.entity.sys.SysUserRole;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 自定义拦截器,实现注解鉴权和token验证
 */
public class CustomInterceptor implements HandlerInterceptor {

    private LoginUtil loginUtil = SpringUtil.getBean(LoginUtil.class);

    /** 设置响应头部信息 */
    private void setHeader(HttpServletRequest request,HttpServletResponse response){
        response.setHeader( "Set-Cookie" , "cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");
        response.setHeader( "Content-Security-Policy" , "default-src 'self'; script-src 'self'; frame-ancestors 'self'");
        response.setHeader("Access-Control-Allow-Origin", (request).getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Referrer-Policy","no-referrer");
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
    }

    /** token有效期刷新和定期刷新 */
    private void refreshToken(HttpServletResponse response,String tokenValue){
        //判断token的创建时间是否大于2小时,如果是的话则需要刷新token
        long time = System.currentTimeMillis() - loginUtil.getCreateTime(tokenValue);
        long hour = time/1000/(60 * 60);
        if (hour>2){
            // TODO 获取当前用户信息,重新登录,生成新的token,将新token设置到响应头中
            SysUser user = loginUtil.getLoginUser(tokenValue);
            // TODO 这里重新登录(会把旧登录删除),生成新的token
            String newToken = loginUtil.login(user);
            response.setHeader(BaseConstant.tokenHeader, newToken);
        }
        long tokenTimeout = loginUtil.getTimeOut(tokenValue);// 获取过期时间
        if (tokenTimeout != -1){ // token没过期,过期时间不是-1的时候,每次请求都刷新过期时间
            loginUtil.updateTimeOut(tokenValue);
        }
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
        setHeader(request,response);
        // 获取当前token(获取的是请求头的token),同时会校验token是否过期,过期了会直接抛出异常,所以这里不用做额外处理。
        String tokenValue = loginUtil.getToken(request);
        if (!checkPermisson(handler,tokenValue)){ // 鉴权
            throw new ExceptionVo(-1,"没有操作权限");
        }
        refreshToken(response,tokenValue);
        return true;
    }

    /** 校验注解权限或角色 */
    private boolean checkPermisson(Object handler,String tokenValue){
        boolean flag = true;
        if (handler instanceof HandlerMethod){
            // 获得请求的方法
            Method method = ((HandlerMethod) handler).getMethod();
            // 获得该方法上面的注解,如果没有注解,直接返回true,通过
            CheckPermission annotation = method.getAnnotation(CheckPermission.class);
            if (annotation != null) {
                String[] perm = annotation.perm(); // 注解上指定的权限标识
                String[] role = annotation.role(); // 注解上指定的角色
                int type = annotation.type();      // 校验的类别方式
                SysUser user = loginUtil.getLoginUser(tokenValue); //得到当前登录人的权限和角色
                List<String> roleList = user.getRoleList().stream().map(SysUserRole.UserRoleVo::getRoleKey).collect(Collectors.toList());
                List<String> permList = loginUtil.getPermList(tokenValue, Arrays.asList(user.getRoleIds()));
                if(type == 0){ // 0: 只校验权限,多个权限需要同时满足;
                    flag = hasElement(permList, Arrays.asList(perm),true);
                }else if(type == 1){ // 1: 只校验权限,多个权限满足一个即可;
                    flag = hasElement(permList, Arrays.asList(perm),false);
                }else if(type == 2){ // 2: 只校验角色,多个角色需要同时满足;
                    flag = hasElement(roleList, Arrays.asList(role),true);
                }else if(type == 3){ // 3: 只校验角色,多个角色满足一个即可;
                    flag = hasElement(roleList, Arrays.asList(role),false);
                }else if(type == 4){ // 4: 校验权限和角色,需要权限和角色同时满足;
                    flag = hasElement(permList, Arrays.asList(perm),true);
                    if (flag){ // 满足权限时再校验角色,否则直接返回false
                        flag = hasElement(roleList, Arrays.asList(role),true);
                    }
                }else if(type == 5){ // 5: 校验权限和角色,权限和角色任意满足一个;
                    flag = hasElement(permList, Arrays.asList(perm),true);
                    if (!flag){ // 不满足权限时再校验角色,否则直接返回true
                        flag = hasElement(roleList, Arrays.asList(role),true);
                    }
                }
            }
        }
        return flag;
    }

    /**
     * 在list中判断指定元素是否存在
     * @param list1 拥有的权限列表
     * @param list2 给定的权限列表
     * @param isAnd 是否要全部满足, true 全部满足,false 满足一个即可
     */
    public static boolean hasElement(List<String> list1,List<String> list2,boolean isAnd){
        boolean flag = true;
        if (list1 != null && list1.size() != 0 && list2 != null && list2.size() != 0 && list1.size() >= list2.size()) {
            if (list1.contains(list2)) {
                return flag;
            } else {
                for (String element : list2) {
                    boolean b = hasElement(list1,element);
                    if (isAnd){
                        if (!b){ // 同时满足,当有一个不满足时,直接跳出,返回false
                            flag = false;
                            break;
                        }
                    }else {
                        if (b){ // 满足一个即可,当有一个满足时,直接跳出,返回true
                            flag = true;
                            break;
                        }
                        flag = false;
                    }
                }
            }
        } else {
            return false;
        }
        return flag;
    }

    /**
     * 在list中判断指定元素是否存在
     * @param list 拥有的权限列表
     * @param element 给定的权限
     */
    public static boolean hasElement(List<String> list,String element){
        if (list != null && list.size() != 0) {
            if (list.contains(element)) {
                return true;
            } else {
                Iterator it = list.iterator();
                String patt;
                do {
                    if (!it.hasNext()) {
                        return false;
                    }
                    patt = (String) it.next();
                } while(!hasElement(patt, element));
                return true;
            }
        } else {
            return false;
        }
    }

    /**
     * 判断两个元素是否匹配
     * @param value1 拥有的
     * @param value2 给定的需要匹配的元素
     */
    private static boolean hasElement(String value1,String value2){
        if (value1 == null && value2 == null) {
            return true;
        } else if (value1 != null && value2 != null) {
            return value1.indexOf("*") == -1 ? value1.equals(value2) : Pattern.matches(value1.replaceAll("\\*", ".*"), value2);
        } else {
            return false;
        }
    }
}

测试:

在这里插入图片描述

3、新增/更新角色时,更新redis中角色对应的权限

SysMenuService

/**
 * 更新redis中的角色与权限关联
 */
public void updateRolePerm(String roleId){
    SysQuery query = new SysQuery();
    query.setId(roleId);
    List<SysMenu> menuList = selectPermsByRoleId(query);
    List<String> permList = menuList.stream().map(SysMenu::getPerms).collect(Collectors.toList());
    redisUtil.hset(BaseConstant.rolePermList,roleId,permList);
}

SysRoleController

/**
 * 新增/修改角色
 */
@PostMapping("/insert")
@CheckPermission("system:role:edit")
public ResultUtil insert(@RequestBody SysRole role) {
    // ......省略其他代码
    sysMenuService.updateRolePerm(role.getId()); // 更新缓存中角色与菜单权限关联
    return ResultUtil.success();
}

4、更新菜单权限标识时,更新redis中对应的权限标识

SysMenuController

/**
 * 修改菜单
 */
@PostMapping("/update")
@CheckPermission("system:menu:update")
public ResultUtil update(@RequestBody SysMenu menu){
	SysMenu m = service.getById(menu.getId());// 根据菜单id获取更新前的菜单
    // ......省略其他代码
    if (!m.getPerms().equals(menu.getPerms())){ // 如果更改了权限标识,在redis缓存中也需要更改
        service.updatePerm(m.getPerms(),menu.getPerms());
    }
    return ResultUtil.success();
}

最后

源码

都看到最后了,不给博主一个赞再走嘛(づ ̄ 3 ̄)づ

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/109373.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

优优聚:美团成立机器人研究院!

美团成立机器人研究院 不用出门走路购买生活必须品&#xff0c;也不用等待几天的快递时间&#xff0c;现在的消费者越来越习惯“外卖点一切”、半小时送达的购物方式。 在即时零售市场中&#xff0c;美团&#xff0c;无疑是当下的焦点。 万万没想到的是&#xff0c;“外卖送一…

Java+MySQL基于SSM的二手玩具交换网站

本二手玩具交换网站主要包括系统用户管理模块、商品信息管理模块、所有购买记录、订单信息、登录模块、和退出模块等多个模块。它帮助二手玩具交换实现了信息化、网络化,通过测试,实现了系统设计目标,相比传统的管理模式,本系统合理的利用了二手玩具交换数据资源,有效的减少了二…

转行IT,女生学编程有前途吗?

一直以来&#xff0c;IT行业对技术的高要求让人们把这个行业标签为男生专属&#xff0c;从前只有个别女生顶着强大的压力、身边人的不理解坚守在IT岗位。 近些年随着互联网科技的发展与普及&#xff0c;很多女孩子发现原来IT技术没有自己想象中难&#xff0c;而且还可以毕业拿高…

Java项目:springboot课程自动排课系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 课程自动排课系统&#xff0c;该系统分两种角色&#xff1a;管理员与普通用户&#xff1b; 主要功能包括&#xff1a; 首页&#xff1a;查看分…

【运维有小邓】AD域权限报表

企业需要每天都警惕内部攻击和间谍等重大安全威胁。防范潜在的攻击者对保护组织的网络和数据大有裨益。但是&#xff0c;要实现此目标&#xff0c;还需满足几点。您必须完全了解分配给Windows Active Directory (AD)中用户和组的权限&#xff0c;它们可访问的帐户、资源和数据&…

java学习day64(乐友商城)Elasticsearch

1.Elasticsearch介绍和安装 用户访问我们的首页&#xff0c;一般都会直接搜索来寻找自己想要购买的商品。 而商品的数量非常多&#xff0c;而且分类繁杂。如何能正确的显示出用户想要的商品&#xff0c;并进行合理的过滤&#xff0c;尽快促成交易&#xff0c;是搜索系统要研究…

自己可以学习python吗?

现在还有很多小伙伴们在怀疑自学Python编程语言行不行&#xff0c;我想说Python自学是肯定行的&#xff0c;最重要的是要看自己的努力程度&#xff0c;Python是一个简单的编程语言&#xff0c;任何懂python语法规则的人都可以操作部署&#xff0c;更重要的是它是免费开源的&…

【学习笔记】前端HTML+CSS部分必懂基础内容(面试考察重点)

一、HTML 1. 什么是语义化&#xff1f;为什么要语义化&#xff1f;语义化标签有哪些 语义化&#xff1a;根据内容的结构化&#xff08;内容语义化&#xff09;&#xff0c;选择合适的标签&#xff08;代码语义化&#xff09;便于开发者阅读和写出更优雅的代码的同时让浏览器的…

试卷的安全方案

摘 要 随着互联网的飞速发展,传统的人工试卷保密措施已渐渐形成智能互联网加密保护。巨大的变革大大减少工作量&#xff0c;提升试卷质量&#xff0c;但随之而来的试卷拟定到发放以及回收的安全问题日益凸显。为了保护智能互联网试卷的保密性,认证授权加密已经成为了互联网传输…

MIUI10国际版系统自定义字体设置办法

国际版系统主题商店中没有字体设置。只有主题和壁纸 需要用到第三方主题安装工具mythemer和miui字体打包主题工具mifont maker 首先在网络上下载ttf格式的字体。 打开mifont maker&#xff0c;点击create custom font 点击pick font选择下载的字体 点击create miui font 制…

AlertDialog6种使用方法

AlertDialog 1.AlertDialog的6种创建模式 1.1setMessage 1&#xff09;Java代码 //1.创建构造器AlertDialog.Builder buildernew AlertDialog.Builder(this);//2.设置参数builder.setTitle("弹窗提示").setIcon(R.mipmap.boy).setMessage("选择你的性别&#xf…

刚毕业1年,做Python挣了60W!”网友:吹的不多..

现状揭秘 &#xff1a; Python岗位大厂50K起&#xff1f; 程序员&#xff1a; 心态崩了&#xff01; 屠杀各种榜单&#xff0c;拿下语言排行榜的Python&#xff0c;薪酬真的如同网传开挂了吗&#xff1f; 从上图看&#xff0c;Python薪酬普遍集中在 25-35k &#xff0c;也就是…

构建系列之新一代利器Esbuild(上)

What is Esbuild&#xff1f; Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 开发的一款打包工具&#xff0c;相比传统的打包工具&#xff0c;主打性能优势&#xff0c;在构建速度上可以快10~100 倍。 为什么会这么快&#xff1f; go实现&#xff0c;编译为本地代码…

Linux:阿里云服务器购买数据盘并挂载流程

1.进入ECS实例详情&#xff0c;找到‘创建云盘’按钮 2.进入购买界面并配置 3.确认订单等待自动挂载 4.返回实例&#xff0c;就能看到刚刚够买的数据盘 5.查看已挂载数据盘情况 df -lh6.查看所有数据盘包括未挂在的数据盘 fdisk -l 7.对数据盘进行分区。 fdisk /de…

maven中的pom

maven中的pompom的最低要求配置pom的默认行为packaging有哪些关于dependencytypescope关于 Dependency Management构建maven聚合工程&#xff0c;父子工程maven官方文档 !!!pom的最低要求配置 总共5个 project-根元素modelVersion -设置为4.0.0即可groupId-项目分组的idartif…

Polygon zkEVM发布公开测试网2.0

1. 引言 Polygon zkEVM发布公开测试网2.0&#xff0c;相比于10月份发布的公开测试网1.0版本&#xff0c;做了如下改进&#xff1a; 支持递归证明&#xff08;testnet1.0采用的是one batch of transactions对应one proof&#xff09;&#xff1a;从而支持多个provers并行工作&…

【架构师(第五十三篇)】 性能优化之 HTTP 缓存

ETag ETagHTTP 响应头是资源的特定版本的标识符&#xff0c;这可以让缓存更高效&#xff0c;并节省带宽&#xff0c;因为如果内容没有改变&#xff0c;web 服务器不需要发送完整的响应。 第二次请求的时候会添加一个 If-None-Match 请求头&#xff0c;去判断文件是否发生过变化…

[思维模式-11]:《如何系统思考》-7- 认识篇 - 克服片面、局部思维,转向全面思考 =》 UML

目录 第1章 全面思考概述&#xff08;空间&#xff09; 1.1 什么是全面思考&#xff08;整体思考&#xff09; 1.2 全面思考的含义 1.3 程序的局部性原理 第2章 如何做到全面思考 2.1 本位思考 》 全局思考 2.2 大局观&#xff0c;既是一种格局&#xff0c;也是一种能力…

【SpringMVC】下篇,拦截器(一步到位学会它)

✅作者简介&#xff1a;热爱Java后端开发的一名学习者&#xff0c;大家可以跟我一起讨论各种问题喔。 &#x1f34e;个人主页&#xff1a;Hhzzy99 &#x1f34a;个人信条&#xff1a;坚持就是胜利&#xff01; &#x1f49e;当前专栏&#xff1a;【Spring】 &#x1f96d;本文内…

Java项目:springboot私人牙医管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 私人牙医管理系统。该项目分为前后台&#xff0c;共三种角色&#xff1a;管理员、医生、客户&#xff1b; 前台主要功能包括&#xff1a;首页、…