分类: 未分类

  • 自制用于HTTPS的SSL证书,包括CA根证书以及在Spring Boot和Nginx中使用的证书

    主体角色又三个:客户端、服务端、以及CA机构。

    如下图所示:

    CA机构根证书

    生成CA机构私钥

    openssl genrsa -out ca.key 2048

    生成CA证书(注意:生成过程中需要输入一些CA机构的信息)

    openssl req -x509 -new -key ca.key -out ca.crt

    生成Server端证书

    生成Server端的key私钥

    openssl genrsa -out server.key 2048

    生成Server端的csr证书请求文件(注意:生成过程中需要你输入一些服务端信息)

    openssl req -new -key server.key -out server.csr
    
    #注意到这一步时的意思
    Please enter the following 'extra' attributes
    to be sent with your certificate request
    A challenge password []:  ##加密CA证书的密码,也要记住,可不输入
    An optional company name []:  ##可以不输入

    使用CA证书生成Server端的crt证书

    openssl x509 -req -sha256 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out server.crt

    转换成用于SpringBoot的P12证书

    使用Server端的key和crt转换p12(注意:-name 后面的参数对应的是证书的alias别名)

    openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 -name "server"

    把ca证书放到keystore中(非必要)

    keytool -importcert -keystore server.p12 -file ca.crt

    将p12证书转换成jks证书(例如jboss用到的)

    keytool -importkeystore -srckeystore server.p12 -srcstoretype PKCS12 -deststoretype JKS -destkeystore server.jks

    将crt证书转换成cer证书

    双击打开*.crt证书文件:

    在证书导出向导窗口点击【下一步】,选择Base-64编码的X.509(.CER),点击【下一步】:

    然后按提示操作,即可完成转换。

  • 树莓派4B使开放热点共享物联网4G模块的网络

    第一步:hostapd

    安装hostapd

    sudo apt install hostapd
    sudo systemctl stop hostapd

    编辑“/etc/hostapd/hostapd.conf”文件(若无,则创建):

    interface=wlan0
    driver=nl80211
    ssid=raspberry_hotspot
    hw_mode=g
    channel=7
    wmm_enabled=0
    macaddr_acl=0
    auth_algs=1
    ignore_broadcast_ssid=0
    wpa=2
    wpa_passphrase=12345678
    wpa_key_mgmt=WPA-PSK
    wpa_pairwise=TKIP
    rsn_pairwise=CCMP

    启动服务:

    sudo systemctl unmask hostapd
    sudo systemctl enable hostapd
    sudo systemctl start hostapd

    过一会,就能在其他移动设备中搜索到该热点了,只是此时能连接该热点,但是还不能上网,因为相关的dns设置没有配好。

    如果服务启动失败,比如报错 "systemctl status hostapd.service" and "journalctl -xe" for details 等,可以按如下步骤逐一排查: 1. 重启树莓派,再尝试启动 hostapd 2. 命令行执行 sudo /usr/sbin/hostapd /etc/hostapd/hostapd.conf,直接运行 hostapd,观察输出日志,一般都能发现问题。比如常见的: 1. hostapd.conf 配置错误导致启动失败。检查配置文件,修改后,新启动 hostapd 即可。 2. wlan0 端口未开启导致启动失败。执行 sudo ifconfig wlan0 up 开启端口后,重新启动 hostapd 即可。

    第二步:配置dhcp

    编辑“/etc/dhcpcd.conf”,在文件末尾添加如下代码:

    interface wlan0
        static ip_address=192.168.4.1/24
        nohook wpa_supplicant

    意思是将树莓派的地址设置为192.168.4.1:

    接着重启dhcpcd服务:

    sudo systemctl restart dhcpcd

    然后执行“ifconfig”命令,便可以看到wlan0的地址为192.168.4.1:

    第三步:dnsmasq

    dnsmasq 是一个小型的用于配置 DNS 和 DHCP 的工具,适用于小型网络,它提供了 DNS 和 DHCP 功能。

    首先安装dnsmasq服务:

    sudo apt install dnsmasq
    sudo systemctl stop dnsmasq

    编辑“/etc/dnsmasq.conf”文件(若无此文件则新建即可),内容如下:

    interface=wlan0
    dhcp-range=192.168.4.20,192.168.4.50,255.255.255.0,24h
    server=8.8.8.8
    server=8.8.4.4

    dhcp-range 配置项的意思是,dhcp 服务会给客户端分配 192.168.4.20 到 192.168.4.50 的 IP 空间,24 小时租期。

    然后重启 dnsmasq 服务:

    sudo systemctl reload dnsmasq

    第四步:配置iptables转发

    开启 Linux 内核的 ip 转发功能,编辑“/etc/sysctl.conf”系统配置文件,去掉 net.ipv4.ip_forward=1 这个配置项的注释:

    添加路由转发规则(注意:eth0为网线口,需要视情况更换成访问网络的端口,如“usb0”):

    sudo iptables -t nat -A  POSTROUTING -o eth0 -j MASQUERADE
    sudo iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT
    sudo iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT

    设置开机时自动导入防火墙规则(将刚刚设置的这些规则应用于我们的每一次启动):

    sudo sh -c "iptables-save > /etc/iptables.ipv4.nat"

    编辑“/etc/rc.local”,把“iptables-restore < /etc/iptables.ipv4.nat”加到最后一行“exit 0”的前面:

    第五步:重启树莓派

    sudo reboot

    其他

    若dnsmasq经常会出现问题,原因可能是wlan0还没热启动完成,而dnsmasq就先启动了,所以出现了启动失败的现象,可以尝试以下操作(未验证):

    // 打开管理启动
    sudo vim /etc/rc.local
    // 添加如下行,在 exit 0 之前
    sudo bash /etc/dnsmasq_delayinit.sh
    // 然后编辑dnsmasq_delayinit.sh
    sudo vim /etc/dnsmasq_delayinit.sh
    #!/bin/sh
    sleep 10
    sudo service dnsmasq restart
    // 再设置成可执行
    sudo chmod +x /etc/dnsmasq_delayinit.sh
  • SpringBoot3.1.2+Security6.1.2+Jwt+Redis实现简RBAC权限控制

    用JDK8用了差不多十年了,也该更新一下了,毕竟SpringBoot3系列已经不再支持JDK8了,所以本次的整合是基于JDK17的。

    SpringBoot3.1.2整合Security时,Security的默认版本为6.1.2。

    废话不多说,直接上代码。

    (response.write封装我不贴了,这种基础代码自己解决就好)

    依赖:pom.xml:

            <!--spring security 启动器-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>3.1.2</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
                <version>2.3.1</version>
            </dependency>
            <!--jwt-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.0</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>3.0.0</version>
            </dependency>
            <!-- mysql -->
            <dependency>
                <groupId>com.mysql</groupId>
                <artifactId>mysql-connector-j</artifactId>
                <scope>runtime</scope>
            </dependency>
            <!--redis-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>

    配置文件application.yml:

    server:
      port: 8080
    
    spring:
      # 数据源配置
      datasource:
        url: jdbc:mysql://localhost:3306/hkbea-gbt?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    
    # mybatis配置
    mybatis:
      # 配置SQL映射文件路径
      mapper-locations: classpath:/mapper/*.xml
      # 实体类别名包扫描(请更换成实际的)
      type-aliases-package: **.**.**.**
      # 驼峰命名
      configuration:
        map-underscore-to-camel-case: true

    token异常处理类:AuthenticationEntryPointImpl.java

    import com.fasterxml.jackson.databind.ObjectMapper;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    
    /**
     * 认证失败
     */
    @Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println("无效登录信息");
        }
    }
    permission异常处理类:AccessDeniedHandlerImpl.java
    import com.fasterxml.jackson.databind.ObjectMapper;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    
    @Component
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println("你无权进行本操作");
        }
    }

    核心配置类:SecurityConfig.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
    import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    /**
     * SpringSecurity 核心配置类
     * prePostEnabled = true 开启注解权限认证功能
     */
    @Configuration
    @EnableWebSecurity
    @EnableMethodSecurity
    public class SecurityConfig {
    
        @Autowired
        private AuthenticationConfiguration authenticationConfiguration;
    
        //认证过滤器
        @Autowired
        private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
        // 认证失败处理器
        @Autowired
        private AuthenticationEntryPointImpl authenticationEntryPoint;
    
        // 授权失败处理器
        @Autowired
        private AccessDeniedHandlerImpl accessDeniedHandler;
    
        /**
         * 认证配置
         * anonymous():匿名访问:未登录可以访问,已登录不能访问
         * permitAll():有没有认证都能访问:登录或未登录都能访问
         * denyAll(): 拒绝
         * authenticated():认证之后才能访问
         * hasAuthority():包含权限
         */
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
            http
                // 允许跨域(也可以不允许,一般前后端分离项目要关闭此功能)
                .cors()
                //关闭csrf:为了防止通过伪造用户请求来访问受信用站点的非法请求访问
                .and()
                    .csrf().disable();
            http
                .anonymous().disable()  //禁用anonymous用户(影响Authentication.getPrincipal()的返回):disable=>匿名用户的authentication为null
                // 禁用session (前后端分离项目,不通过Session获取SecurityContext)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                //登录配置  //此处全部注释,在本案例中采用注解进行权限拦截
                //.and()
                    // 添加token过滤器
                    //.apply(permitAllSecurityConfig)
                    //.and()
                    // 配置路径是否需要认证
                    //.authorizeHttpRequests()
                        //配置放行资源(注意: 放行资源必须放在所有认证请求之前!)
                        //.requestMatchers("/api/v1/auth/login").permitAll()
                        //.requestMatchers("/api/v1/auth/register").permitAll()
                        //.requestMatchers("/api/v1/on1on/**").permitAll()
                        //.requestMatchers("/api/v1/admin/**").hasAuthority("admin")  // 对于admin接口 需要权限admin
                        // 除上面外的所有请求全部需要鉴权认证
                        //.anyRequest().authenticated()   //代表所有请求,必须认证之后才能访问
                        //.anyRequest().permitAll()
                .and()
                    .authenticationManager(authenticationManager(authenticationConfiguration))
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                    //此处为添加jwt过滤
                    .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            ;
    
            // 配置异常处理器
            http.exceptionHandling()
                    // 认证失败
                    .authenticationEntryPoint(authenticationEntryPoint)
                    // 授权失败
                    .accessDeniedHandler(accessDeniedHandler);
    
    
            http.headers().frameOptions().disable();
    
            return http.build();
        }
    
        /**
         * 注入AuthenticationManager 进行用户认证(基于用户名和密码或使用用户名和密码进行身份验证)
         * @param authenticationConfiguration
         * @return
         * @throws Exception
         */
        @Bean
        public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
            return authenticationConfiguration.getAuthenticationManager();
        }
    
        /**
         * 提供密码机密处理机制:
         * 将BCryptPasswordEncoder对象注入到spring容器中,更换掉原来的 PasswordEncoder加密方式
         * 原PasswordEncoder密码格式为:{id}password。它会根据id去判断密码的加密方式。
         * 如果没替换原来的加密方式,数据库中想用明文密码做测试,将密码字段改为{noop}123456这样的格式
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    }

    RBAC中的Account.java:

    import lombok.Data;
    
    import java.io.Serializable;
    
    @Data
    public class Account implements Serializable {
    
        private String id;
        private String username;
        private String password;
        private String nickname;
    
    }
    

    RBAC中的AccountDao:AccountDao.java

    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    
    @Mapper
    public interface AccountDao {
        /**
         * 获取用户信息
         */
        @Select("select * from account where username = #{username}")
        Account getUserInfo(String username);
    }

    RBAC中的AccountService接口:AccountService.java

    public interface AccountService {
    
        /**
         * 根据用户名获取用户信息
         */
        Account getUserInfo(String username);
    }

    RBAC中的AccountService接口实现类:AccountServiceImpl.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class AccountServiceImpl implements AccountService {
    
        @Autowired
        private AccountDao accountDao;
    
        /**
         * 根据用户名获取用户信息
         */
        @Override
        public Account getUserInfo(String username) {
            return accountDao.getUserInfo(username);
        }
    }

    RBAC中的权限查询类:AuthDao.java

    import org.apache.ibatis.annotations.Mapper;
    
    import java.util.List;
    import java.util.Map;
    
    @Mapper
    public interface AuthDao {
    
        List<Map<String,Object>> getRolesAndPermissionsByAccountId(String id);
    
    }
    

    RBAC中的权限查询Mapper:AuthMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.hkbea.gbt.dao.AuthDao">
    
        <!--通过手机号查找用户权限信息-->
        <select id="getRolesAndPermissionsByAccountId" resultType="java.util.HashMap" parameterType="String">
            SELECT role.name AS role, permission.name AS permission
            FROM role,
                 permission,
                 account_role,
                 role_permission
            WHERE account_role.account_id=#{id}
              AND account_role.role_id=role.id
              AND role_permission.role_id=role.id
              AND role_permission.permission_id=permission.id
        </select>
    
    </mapper>
    SpringSecurity核心的用户类:LoginUser.java
    import com.alibaba.fastjson2.annotation.JSONField;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.extern.log4j.Log4j2;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    
    /**
     * 重写UserDetails返回的用户信息
     * SpringSecurity返回的用户信息实体类
     */
    @Log4j2
    @Data
    @NoArgsConstructor
    public class LoginUser implements UserDetails {
    
        private Account account;    //用户信息
    
        private List<Map<String,Object>> rolesAndPermissions; //角色信息
    
        private List<String> roles; //角色信息
    
        private List<String> permissions;   //权限信息
    
        public LoginUser(Account account, List<Map<String,Object>> rolesAndPermissions) {
            this.account = account;
            this.roles = new ArrayList<>();
            this.permissions = new ArrayList<>();
            for (Map<String,Object> map : rolesAndPermissions){
                log.info(map);
                log.info("role : {}",map.get("role"));
                this.roles.add((String)map.get("role"));
                log.info("permission : {}",map.get("permission"));
                this.permissions.add((String)map.get("permission"));
            }
        }
    
        @JSONField(serialize = false) //fastjson注解,表示此属性不会被序列化,因为SimpleGrantedAuthority这个类型不能在redis中序列化
        private List<SimpleGrantedAuthority> authorities;
    
        /**
         * 获取权限信息
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            // 权限为空的时候才往遍历,不为空直接返回
            if (authorities != null) {
                return authorities;
            }
            //把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
            authorities = new ArrayList<>();
            for (String permission : permissions) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
                authorities.add(authority);
            }
            return authorities;
        }
    
        /**
         * 获取密码
         */
        @Override
        public String getPassword() {
            return account.getPassword();
        }
    
        /**
         * 获取用户名
         */
        @Override
        public String getUsername() {
            return account.getUsername();
        }
    
        /**
         * 判断是否过期(账户是否未过期,过期无法验证)
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        /**
         * 是否锁定(指定用户是否解锁,锁定的用户无法进行身份验证)
         */
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        /**
         * 是否没有超时(指示是否已过期的用户的凭据(密码),过期的凭据防止认证)
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        /**
         * 是否可用(用户是否被启用或禁用。禁用的用户无法进行身份验证。)
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    
    }

    Redis使用FastJson序列化的相关类:FastJsonRedisSerializer.java

    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.parser.ParserConfig;
    import com.alibaba.fastjson.serializer.SerializerFeature;
    import com.fasterxml.jackson.databind.JavaType;
    import com.fasterxml.jackson.databind.type.TypeFactory;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.SerializationException;
    
    import java.nio.charset.Charset;
    
    /**
     * Redis使用FastJson序列化
     */
    public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    
        public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    
        private Class<T> clazz;
    
        static {
            ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        }
    
        public FastJsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }
    
        @Override
        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }
    
        @Override
        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length <= 0) {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);
    
            return JSON.parseObject(str, clazz);
        }
    
    
        protected JavaType getJavaType(Class<?> clazz) {
            return TypeFactory.defaultInstance().constructType(clazz);
        }
    }

    Redis配置类:RedisConfig.java

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * redis配置类
     * 避免存入redis中的key看上去乱码的现象
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        @SuppressWarnings(value = {"unchecked", "rawtypes"})
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(connectionFactory);
    
            //使用FastJsonRedisSerializer来序列化和反序列化redis的value值
            FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
    
            // 使用StringRedisSerializer来序列化和反序列化redis的key值
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(serializer);
    
            // Hash的key也采用StringRedisSerializer的序列化方式
            template.setHashKeySerializer(new StringRedisSerializer());
            template.setHashValueSerializer(serializer);
    
            template.afterPropertiesSet();
            return template;
        }
    }

    Redis工具类:RedisCache.java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.BoundSetOperations;
    import org.springframework.data.redis.core.HashOperations;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.stereotype.Component;
    
    import java.util.*;
    import java.util.concurrent.TimeUnit;
    
    /**
     * redis工具类
     */
    @Component
    public class RedisCache {
        @Autowired
        public RedisTemplate redisTemplate;
    
        /**
         * 缓存基本的对象,Integer、String、实体类等
         *
         * @param key   缓存的键值
         * @param value 缓存的值
         */
        public <T> void setCacheObject(final String key, final T value) {
            redisTemplate.opsForValue().set(key, value);
        }
    
        /**
         * 缓存基本的对象,Integer、String、实体类等
         *
         * @param key      缓存的键值
         * @param value    缓存的值
         * @param timeout  时间
         * @param timeUnit 时间颗粒度
         */
        public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
            redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
        }
    
        /**
         * 设置有效时间
         *
         * @param key     Redis键
         * @param timeout 超时时间
         * @return true=设置成功;false=设置失败
         */
        public boolean expire(final String key, final long timeout) {
            return expire(key, timeout, TimeUnit.SECONDS);
        }
    
        /**
         * 设置有效时间
         *
         * @param key     Redis键
         * @param timeout 超时时间
         * @param unit    时间单位
         * @return true=设置成功;false=设置失败
         */
        public boolean expire(final String key, final long timeout, final TimeUnit unit) {
            return redisTemplate.expire(key, timeout, unit);
        }
    
        /**
         * 获得缓存的基本对象。
         *
         * @param key 缓存键值
         * @return 缓存键值对应的数据
         */
        public <T> T getCacheObject(final String key) {
            ValueOperations<String, T> operation = redisTemplate.opsForValue();
            return operation.get(key);
        }
    
        /**
         * 删除单个对象
         *
         * @param key
         */
        public boolean deleteObject(final String key) {
            return redisTemplate.delete(key);
        }
    
        /**
         * 删除集合对象
         *
         * @param collection 多个对象
         * @return
         */
        public long deleteObject(final Collection collection) {
            return redisTemplate.delete(collection);
        }
    
        /**
         * 缓存List数据
         *
         * @param key      缓存的键值
         * @param dataList 待缓存的List数据
         * @return 缓存的对象
         */
        public <T> long setCacheList(final String key, final List<T> dataList) {
            Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
            return count == null ? 0 : count;
        }
    
        /**
         * 获得缓存的list对象
         *
         * @param key 缓存的键值
         * @return 缓存键值对应的数据
         */
        public <T> List<T> getCacheList(final String key) {
            return redisTemplate.opsForList().range(key, 0, -1);
        }
    
        /**
         * 缓存Set
         *
         * @param key     缓存键值
         * @param dataSet 缓存的数据
         * @return 缓存数据的对象
         */
        public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
            BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
            Iterator<T> it = dataSet.iterator();
            while (it.hasNext()) {
                setOperation.add(it.next());
            }
            return setOperation;
        }
    
        /**
         * 获得缓存的set
         *
         * @param key
         * @return
         */
        public <T> Set<T> getCacheSet(final String key) {
            return redisTemplate.opsForSet().members(key);
        }
    
        /**
         * 缓存Map
         *
         * @param key
         * @param dataMap
         */
        public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
            if (dataMap != null) {
                redisTemplate.opsForHash().putAll(key, dataMap);
            }
        }
    
        /**
         * 获得缓存的Map
         *
         * @param key
         * @return
         */
        public <T> Map<String, T> getCacheMap(final String key) {
            return redisTemplate.opsForHash().entries(key);
        }
    
        /**
         * 往Hash中存入数据
         *
         * @param key   Redis键
         * @param hKey  Hash键
         * @param value 值
         */
        public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
            redisTemplate.opsForHash().put(key, hKey, value);
        }
    
        /**
         * 获取Hash中的数据
         *
         * @param key  Redis键
         * @param hKey Hash键
         * @return Hash中的对象
         */
        public <T> T getCacheMapValue(final String key, final String hKey) {
            HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
            return opsForHash.get(key, hKey);
        }
    
        /**
         * 删除Hash中的数据
         *
         * @param key
         * @param hkey
         */
        public void delCacheMapValue(final String key, final String hkey) {
            HashOperations hashOperations = redisTemplate.opsForHash();
            hashOperations.delete(key, hkey);
        }
    
        /**
         * 获取多个Hash中的数据
         *
         * @param key   Redis键
         * @param hKeys Hash键集合
         * @return Hash对象集合
         */
        public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
            return redisTemplate.opsForHash().multiGet(key, hKeys);
        }
    
        /**
         * 获得缓存的基本对象列表
         *
         * @param pattern 字符串前缀
         * @return 对象列表
         */
        public Collection<String> keys(final String pattern) {
            return redisTemplate.keys(pattern);
        }
    }

    JWT工具类:JwtTokenUtil.java

    import io.jsonwebtoken.*;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    @Configuration
    @Component
    @Slf4j
    public class JwtTokenUtil {
    
        /**
         * token的头key
         */
        public static final String AUTH_HEADER_KEY = "Authorization";
        /**
         * token前缀
         */
        public static final String TOKEN_PREFIX = "Bearer ";
    
        /**
         * token 过期时间 30分钟
         */
        public static final long EXPIRATION = 1000 * 60 * 30;
    
        /**
         * 加密的key(自己生成的任意值)
         */
        public static final String APP_SECRET_KEY = "secret";
    
        /**
         * 生成token
         * @return token字符串
         */
        public static String createToken(Account account) {
    
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", account.getId());
            claims.put("username", account.getUsername());
            claims.put("nickname", account.getNickname());
    
            String token = Jwts
                    .builder()
                    .setId(account.getId())
                    .setClaims(claims)
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                    .signWith(SignatureAlgorithm.HS256, APP_SECRET_KEY)
                    .compact();
            return TOKEN_PREFIX +token;
        }
    
        /**
         * 解析jwt
         * @return 解析后的JWT Claims
         */
        public static Claims parseJwt(String token) {
            try {
                return Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
            } catch (Exception e) {
                log.error("token exception : ", e);
            }
            return null;
        }
    
        /**
         * 获取当前登录用户的ID
         *
         * @param token
         * @return
         */
        public static String getAccountId(String token) {
            Claims claims = parseJwt(token);
            return (String)claims.get("id");
        }
    
        /**
         * 获取当前登录用户用户名
         *
         * @param token
         * @return
         */
        public static String getUsername(String token) {
            Claims claims = parseJwt(token);
            return (String)claims.get("username");
        }
    
        /**
         * 检查token是否过期
         *
         * @param  token token
         * @return boolean
         */
        public static boolean isExpiration(String token) {
            Claims claims = parseJwt(token);
            return claims.getExpiration().before(new Date());
        }
    
    
        public boolean validateToken(String token) {
            try {
                Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token);
                return true;
            } catch (SignatureException e) {
                log.error("Invalid JWT signature: {}", e.getMessage());
            } catch (MalformedJwtException e) {
                log.error("Invalid JWT token: {}", e.getMessage());
            } catch (ExpiredJwtException e) {
                log.error("JWT token is expired: {}", e.getMessage());
            } catch (UnsupportedJwtException e) {
                log.error("JWT token is unsupported: {}", e.getMessage());
            } catch (IllegalArgumentException e) {
                log.error("JWT claims string is empty: {}", e.getMessage());
            }
            return false;
        }
    
    }
    
    

    JWT权限拦截过滤器:JwtAuthenticationTokenFilter.java

    import com.alibaba.fastjson.JSONObject;
    import io.jsonwebtoken.Claims;
    import jakarta.annotation.Resource;
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import lombok.extern.log4j.Log4j2;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import java.util.Objects;
    
    /**
     * token认证过滤器
     * 作用:解析请求头中的token。并验证合法性
     * 继承 OncePerRequestFilter 保证请求经过过滤器一次
     */
    @Log4j2
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        @Resource
        private RedisCache redisCache;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain){
    
            String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
    
            if (StringUtils.hasText(token)) {
                Claims claims = JwtTokenUtil.parseJwt(token);
                if(claims!=null){
                    String accountId = (String)claims.get("id");
                    String redisKey = "login:" + accountId;
                    // 先转成JSON对象
                    JSONObject jsonObject = redisCache.getCacheObject(redisKey);
                    // JSON对象转换成Java对象
                    LoginUser loginUser = jsonObject.toJavaObject(LoginUser.class);
    
                    // redis中用户不存在
                    if (Objects.isNull(loginUser)) {
                        throw new RuntimeException("redis中用户不存在!");
                    }
    
                    // 将Authentication对象(用户信息、已认证状态、权限信息)存入 SecurityContextHolder
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
    
            log.info("authentication :{}",SecurityContextHolder.getContext().getAuthentication());
    
            //放行
            try {
                filterChain.doFilter(request, response);    //放行,因为后面的会抛出相应的异常
            }catch (Exception e){
                e.printStackTrace();
            }
    
        }
    }

    鉴权Controller类:AuthApiControllerV1.java

    import com.alibaba.fastjson2.JSONObject;
    import com.hkbea.gbt.controller.api.v1.util.ApiControllerExceptionV1;
    import com.hkbea.gbt.controller.api.v1.util.ResultVo;
    import com.hkbea.gbt.model.Account;
    import com.hkbea.gbt.on1on.On1onApiUtil;
    import com.hkbea.gbt.security.core.LoginUser;
    import com.hkbea.gbt.security.service.LoginService;
    import com.hkbea.gbt.service.AccountService;
    import jakarta.annotation.security.PermitAll;
    import lombok.extern.log4j.Log4j2;
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.UUID;
    
    @Log4j2
    @RestController
    @RequestMapping("/api/v1/auth")
    public class AuthApiControllerV1 {
    
        @Autowired
        private LoginService loginService;
    
        @Autowired
        private AccountService accountService;
    
        @PostMapping("login")
        @PermitAll
        public ResultVo login(@RequestBody JSONObject body) throws ApiControllerExceptionV1 {
    
            String username = body.getString("username");
            String password = body.getString("password");
    
            if(username==null||(username=username.trim()).equals("")){
                throw new ApiControllerExceptionV1("请输入正确的email账号");
            }
            if(password==null||(password=password.trim()).equals("")){
                throw new ApiControllerExceptionV1("请输入password");
            }
    
            String jwt = loginService.login(username,password);
            if(StringUtils.isEmpty(jwt)){
                throw new ApiControllerExceptionV1("账号或密码错误");
            }
            return new ResultVo(ResultVo.SUCCESS,"登录成功",jwt);
        }
    
        @GetMapping("info")
        @PreAuthorize("authenticated")
        public ResultVo info(Authentication authentication) throws ApiControllerExceptionV1 {
    
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    
            Account account = loginUser.getAccount();
    
            JSONObject out = new JSONObject();
            out.put("id",account.getId());
            out.put("username",account.getUsername());
            out.put("on1on_id",account.getOn1onId());
    
            return new ResultVo(out);
        }
    
        /**
         * 退出登录
         */
        @RequestMapping(value="logout", method = {RequestMethod.DELETE})
        @PreAuthorize("authenticated")
        public ResultVo logout() throws ApiControllerExceptionV1{
            if(loginService.logout()){
                return new ResultVo(ResultVo.SUCCESS,"注销成功");
            }
            throw new ApiControllerExceptionV1("退出登录异常");
        }
    }
    
    

    测试Controller:TestApiControllerV1.java

    import com.alibaba.fastjson2.JSONObject;
    import jakarta.annotation.security.PermitAll;
    import lombok.extern.log4j.Log4j2;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.web.bind.annotation.*;
    
    @Log4j2
    @RestController
    @RequestMapping("/api/v1/test")
    public class TestApiControllerV1 {
    
        @GetMapping("")
        @PermitAll
        public ResultVo test(){
    
            log.info("test");
    
            JSONObject out = new JSONObject();
    
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if(authentication!=null){
                LoginUser loginUser = (LoginUser) authentication.getPrincipal();
                System.out.println("username :"+loginUser.getAccount().getUsername());
                System.out.println("凭证 :"+authentication.getCredentials());
                System.out.println("权限 :"+authentication.getAuthorities());
    
                out.put("username",loginUser.getAccount().getUsername());
                out.put("credentials",authentication.getCredentials());
                out.put("authorities",authentication.getAuthorities());
            }
    
            return new ResultVo(out);
        }
    
    }

    MySQL测试数据脚本:

    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for account
    -- ----------------------------
    DROP TABLE IF EXISTS `account`;
    CREATE TABLE `account`  (
      `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of account
    -- ----------------------------
    INSERT INTO `account` VALUES ('5a07d8d95b9a46179d10a67f3765d363', 'admin', '$2a$10$MKzkFJJLuGMWO1eL9YuS9uinjq6H4Kalf9MzDxIFPQ4.fV9IKHNCC', '管理员');
    INSERT INTO `account` VALUES ('85a223c1a519463e9e8de3a096818e6a', 'user', '123456', '普通用户');
    
    -- ----------------------------
    -- Table structure for account_role
    -- ----------------------------
    DROP TABLE IF EXISTS `account_role`;
    CREATE TABLE `account_role`  (
      `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `account_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `role_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of account_role
    -- ----------------------------
    INSERT INTO `account_role` VALUES ('eb014d68525849c29a5998e7133c1db9', '5a07d8d95b9a46179d10a67f3765d363', '28fb6f155b3b49e38a7241893b8d03f2');
    
    -- ----------------------------
    -- Table structure for permission
    -- ----------------------------
    DROP TABLE IF EXISTS `permission`;
    CREATE TABLE `permission`  (
      `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of permission
    -- ----------------------------
    INSERT INTO `permission` VALUES ('80ad0fd1296f48a8bdd98ec700947c63', 'admin_console');
    
    -- ----------------------------
    -- Table structure for role
    -- ----------------------------
    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role`  (
      `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of role
    -- ----------------------------
    INSERT INTO `role` VALUES ('28fb6f155b3b49e38a7241893b8d03f2', '管理员');
    
    -- ----------------------------
    -- Table structure for role_permission
    -- ----------------------------
    DROP TABLE IF EXISTS `role_permission`;
    CREATE TABLE `role_permission`  (
      `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `role_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `permission_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of role_permission
    -- ----------------------------
    INSERT INTO `role_permission` VALUES ('28e1307e38124275b9e2d428adee3e0b', '28fb6f155b3b49e38a7241893b8d03f2', '80ad0fd1296f48a8bdd98ec700947c63');
    
    SET FOREIGN_KEY_CHECKS = 1;
    

    测试结果:

  • 在SpringBoot中简单使用EhCache

    很不巧,项目所在的环境中不允许使用Redis/Memcached等需要额外安装软件环境,因此只能使用EhCache。

    基本介绍

    EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。

    使用方法

    第一步:引入依赖

            <!-- ehcache 缓存依赖-->
            <dependency>
                <groupId>net.sf.ehcache</groupId>
                <artifactId>ehcache</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
            </dependency>

    第二步:创建配置

    在application中加入以下配置:

    spring:
      cache:
        type: ehcache
        ehcache:
          config: classpath:/ehcache.xml

    创建配置文件ehcache.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
             updateCheck="false">
        <defaultCache maxElementsInMemory="1000"
                      overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="3600"
                      timeToLiveSeconds="3600" memoryStoreEvictionPolicy="LRU"/>
        <!--
           name:缓存名称。
           maxElementsInMemory:缓存最大个数。
           eternal:对象是否永久有效,一但设置了,timeout将不起作用。
           timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
           timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
           overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
           diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
           maxElementsOnDisk:硬盘最大缓存个数。
           diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
           diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
           memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
           clearOnFlush:内存数量最大时是否清除。
           -->
        <cache name="5s" maxElementsInMemory="100"
               overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="5" timeToLiveSeconds="5"
               memoryStoreEvictionPolicy="LRU"/>
    </ehcache>

    第三步:编写配置/工具类

    创建工具类文件EhCacheUtil.java:

    package com.ruoyi.common.core.ehcache;
    
    import com.ruoyi.common.constant.CacheConstants;
    import net.sf.ehcache.Cache;
    import net.sf.ehcache.Element;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.ehcache.EhCacheCacheManager;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.ArrayList;
    import java.util.Collection;
    
    @Configuration
    public class EhCacheUtil {
    
        private static CacheManager manager;
    
        @Autowired
        public void setManager(CacheManager manager) {
            EhCacheUtil.manager = manager;
        }
    
        /**
         * 获取指定name的cache对象
         * @param cacheName
         * @return
         */
        private static Cache getCache(String cacheName) {
            net.sf.ehcache.CacheManager cacheManager = ((EhCacheCacheManager) manager).getCacheManager();
            if (!cacheManager.cacheExists(cacheName))
                cacheManager.addCache(cacheName);
            return cacheManager.getCache(cacheName);
        }
    
        /**
         * 获取指定cache缓存的指定key对象
         * @param cacheName
         * @param key
         * @return
         */
        public static <T> T get(String cacheName, Object key) {
            Cache cache = getCache(cacheName);
            Element e = cache.get(key);
            if(e!=null){
                return (T) e.getObjectValue();
            }
            return null;
        }
    
        /**
         *
         * @param cacheName
         * @param key
         * @param value
         * @param ttl   缓存自创建日期起至失效时的间隔时间(秒)
         * @param tti   缓存创建以后,最后一次访问缓存的日期至失效之时的时间间隔(秒)
         */
        public static void put(String cacheName, Object key, Object value, Integer ttl, Integer tti) {
            Cache cache = getCache(cacheName);
            Element e = new Element(key, value);
            //不设置则使用xml配置
            if (ttl != null)
                e.setTimeToLive(ttl);
            if (tti != null)
                e.setTimeToIdle(tti);
            cache.put(e);
        }
    
        /**
         * 获得缓存的基本对象列表
         *
         * @param pattern 字符串前缀
         * @return 对象列表
         */
        public static Collection<String> keys(String cacheName, String pattern)
        {
            Cache cache = getCache(cacheName);
            Collection<String> allKeys = cache.getKeys();
            Collection<String> returnkeys = new ArrayList<>();
            for(String key : allKeys){
                if(key.matches(pattern)){
                    returnkeys.add(key);
                }
            }
            return returnkeys;
        }
    
        /**
         * @Description:    删除缓存记录
         * @param cacheName 缓存名字
         * @param key       缓存key
         * @return boolean  是否成功删除
         */
        public static boolean remove(String cacheName, String key) {
            Cache cache = getCache(cacheName);
            if (null == cache) {
                return false;
            }
            return cache.remove(key);
        }
    
        /**
         * @Description: 删除全部缓存记录
         * @param cacheName 缓存名字
         */
        public static void removeAll(String cacheName) {
            Cache cache = getCache(cacheName);
            if (null != cache) {
                //logOnRemoveAllIfPinnedCache();
                cache.removeAll();
            }
        }
    
    }

    第四步:使用缓存

    创建缓存:

    EhCacheUtil.put(Constants.EHCACHE_NAME, userKey, loginUser, expireTime*60,null);

    获取缓存:

    EhCacheUtil.get(Constants.EHCACHE_NAME,userKey);

    删除缓存:

    EhCacheUtil.remove(Constants.EHCACHE_NAME,userKey);
  • 关于SpringBoot连接MSSQL的一些问题

    情况1:没有开启TCP/IP连接

    com.microsoft.sqlserver.jdbc.SQLServerException: 通过端口 1433 连接到主机 localhost 的 TCP/IP 连接失败。错误:“Socket operation on nonsocket: configureBlocking。请验证连接属性。确保 SQL Server 的实例正在主机上运行,且在此端口接受 TCP/IP 连接,还要确保防火墙没有阻止到此端口的 TCP 连接。”。
    	at com.microsoft.sqlserver.jdbc.SQLServerException.makeFromDriverError(SQLServerException.java:237)
    	at com.microsoft.sqlserver.jdbc.SQLServerException.ConvertConnectExceptionToSQLServerException(SQLServerException.java:288)
    	at com.microsoft.sqlserver.jdbc.SocketFinder.findSocket(IOBuffer.java:2720)
    	at com.microsoft.sqlserver.jdbc.TDSChannel.open(IOBuffer.java:761)
    .....

    解决方法:

    搜索SQL SERVER 配置管理器,启用如下图所示的地方即可:

    注意:修改后要重启SQL SERVER服务才能生效。

    情况二:安全连接不通过

    com.microsoft.sqlserver.jdbc.SQLServerException: 驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接。错误:“sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target”。 ClientConnectionId:cae7aed5-1127-4c7e-a992-e9f2a379fd94
    	at com.microsoft.sqlserver.jdbc.SQLServerConnection.terminate(SQLServerConnection.java:3680)
    	at com.microsoft.sqlserver.jdbc.TDSChannel.enableSSL(IOBuffer.java:2113)
    	at com.microsoft.sqlserver.jdbc.SQLServerConnection.connectHelper(SQLServerConnection.java:3204)
    	at com.microsoft.sqlserver.jdbc.SQLServerConnection.login(SQLServerConnection.java:2833)
    	at com.microsoft.sqlserver.jdbc.SQLServerConnection.connectInternal(SQLServerConnection.java:2671)
    	at com.microsoft.sqlserver.jdbc.SQLServerConnection.connect(SQLServerConnection.java:1640)
    ......

    解决方法:

    原来的连接url是这样写的:

    url: jdbc:sqlserver://localhost:1433;DatabaseName=csr-vwork

    需要改成以下两种之一:

                    url: jdbc:sqlserver://localhost:1433;DatabaseName=csr-vwork;encrypt=false
                    url: jdbc:sqlserver://localhost:1433;DatabaseName=csr-vwork;encrypt=true;trustServerCertificate=true

    这个是因为sqlever在jdbc连接的时候需要一定的安全验证,我们跳过或者强制可信即可。

  • 在k8s-1.23.17集群中使用ingress-nginx暴露服务

    本人转载并修改于原文《ingress-nginx详解和部署方案》,出处:https://blog.csdn.net/weixin_44729138/article/details/105978555

    在此先感谢原作者的奉献。

    1、ingress介绍

    K8s集群对外暴露服务的方式目前只有三种:
    Loadblancer;Nodeport;ingress
    前两种熟悉起来比较快,而且使用起来也比较方便,在此就不进行介绍了。

    下面详细讲解下ingress这个服务,ingress由两部分组成:

    ingress controller:将新加入的Ingress转化成Nginx的配置文件并使之生效
    ingress服务:将Nginx的配置抽象成一个Ingress对象,每添加一个新的服务只需写一个新的Ingress的yaml文件即可
    其中ingress controller目前主要有两种:基于nginx服务的ingress controller和基于traefik的ingress controller。
    而其中traefik的ingress controller,目前支持http和https协议。由于对nginx比较熟悉,而且需要使用TCP负载,所以在此我们选择的是基于nginx服务的ingress controller。
    但是基于nginx服务的ingress controller根据不同的开发公司,又分为k8s社区的ingres-nginx和nginx公司的nginx-ingress。
    在此根据github上的活跃度和关注人数,我们选择的是k8s社区的ingres-nginx。

    k8s社区提供的ingress,github地址如下:

    https://github.com/kubernetes/ingress-nginx

    nginx社区提供的ingress,github地址如下:

    https://github.com/nginxinc/kubernetes-ingress

    2、ingress的工作原理

    ingress具体的工作原理如下:
    step1:ingress contronler通过与k8s的api进行交互,动态的去感知k8s集群中ingress服务规则的变化,然后读取它,并按照定义的ingress规则,转发到k8s集群中对应的service。
    step2:而这个ingress规则写明了哪个域名对应k8s集群中的哪个service,然后再根据ingress-controller中的nginx配置模板,生成一段对应的nginx配置。
    step3:然后再把该配置动态的写到ingress-controller的pod里,该ingress-controller的pod里面运行着一个nginx服务,控制器会把生成的nginx配置写入到nginx的配置文件中,然后reload一下,使其配置生效,以此来达到域名分配置及动态更新的效果。

    3、ingress可以解决的问题

    1)动态配置服务
    如果按照传统方式, 当新增加一个服务时, 我们可能需要在流量入口加一个反向代理指向我们新的k8s服务. 而如果用了Ingress, 只需要配置好这个服务, 当服务启动时, 会自动注册到Ingress的中, 不需要而外的操作。

    2)减少不必要的端口暴露
    配置过k8s的都清楚, 第一步是要关闭防火墙的, 主要原因是k8s的很多服务会以NodePort方式映射出去, 这样就相当于给宿主机打了很多孔, 既不安全也不优雅. 而Ingress可以避免这个问题, 除了Ingress自身服务可能需要映射出去, 其他服务都不要用NodePort方式。

    4、部署ingress(deployment的方式)

    查看当前版api版本:

    kubectl explain Ingress

    注:查看ingress和自己本地的k8s版本是否对应上,在GitHub上有表格参考。

    下载配置文件:

    mkdir -p /root/ingress && cd /root/ingress
    wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.3/deploy/static/provider/baremetal/deploy.yaml

    查看源:

    cat deploy.yaml | grep image:

    替换镜像:

    sed  -i 's#k8s.gcr.io/ingress-nginx/controller:v1.1.3@sha256:31f47c1e202b39fadecf822a9b76370bd4baed199a005b3e7d4d1455f4fd3fe2#registry.cn-hangzhou.aliyuncs.com/imges/controller:v1.1.3#' deploy.yaml
    sed  -i 's#k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660#registry.cn-hangzhou.aliyuncs.com/chenby/kube-webhook-certgen:v1.1.1#' deploy.yaml

    注意:有些版本的ingress-nginx的配置配置文件的type默认为LoadBalaner,需要暴露的服务其后端svc访问类型需要改成NodePort:

    另外,ingress-controller的官方yaml默认注释了hostNetwork 工作方式,以防止端口的在宿主机的冲突,没有绑定到宿主机 80 端口,因此还要在名为ingress-nginx-controller的Deployment配置的spec下增加“hostNetwork: true”的属性:

    启动POD:

    kubectl apply -f deploy.yaml

    查看pod运行情况:

    kubectl get pod -n ingress-nginx

    启动时间有点长,最终的运行结果应该是这样的:

    说明:status为completed的两个pod为job类型资源,completed表示job已经成功执行无需管它。

    查看svc运行情况:

    kubectl get svc -n ingress-nginx

    查看controller所在的工作节点:

    kubectl get pods -n ingress-nginx -o wide
    进入对应的节点环境中,查看是否80端口已经开启:
    [root@k8s-node03 ~]# netstat -lnp|grep 80

    至此,ingress-nginx部署成功。

    5、启用默认后端

    mkdir -p /root/ingress ; cd /root/ingress
    cat > backend.yaml <<EOF
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: default-http-backend
      labels:
        app.kubernetes.io/name: default-http-backend
      namespace: kube-system
    spec:
      replicas: 1
      selector:
        matchLabels:
          app.kubernetes.io/name: default-http-backend
      template:
        metadata:
          labels:
            app.kubernetes.io/name: default-http-backend
        spec:
          terminationGracePeriodSeconds: 60
          containers:
          - name: default-http-backend
            image: registry.cn-hangzhou.aliyuncs.com/imges/defaultbackend-amd64:1.5 
            livenessProbe:
              httpGet:
                path: /healthz
                port: 8080
                scheme: HTTP
              initialDelaySeconds: 30
              timeoutSeconds: 5
            ports:
            - containerPort: 8080
            resources:
              limits:
                cpu: 10m
                memory: 20Mi
              requests:
                cpu: 10m
                memory: 20Mi
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: default-http-backend
      namespace: kube-system
      labels:
        app.kubernetes.io/name: default-http-backend
    spec:
      ports:
      - port: 80
        targetPort: 8080
      selector:
        app.kubernetes.io/name: default-http-backend
    EOF

    部署:

    kubectl apply -f backend.yaml

    查看默认后端运行情况:

    kubectl get pods -n kube-system

    访问ingress-nginx-controller节点所在的node的80端口,看是否正确返回backend的404提示页面:

    curl 192.168.239.23

    6、测试

    创建svc和pod

    我们使用默认的nginx镜像创建svc和pod,用于测试:

    vim ingress-dev.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-dm
      namespace: default
    spec:
      replicas: 2
      selector:
        matchLabels:
          name: nginx
      template:
        metadata:
          labels:
            name: nginx
        spec:
          containers:
          - name: nginx
            image: nginx:1.16
            imagePullPolicy: IfNotPresent
            ports:
            - containerPort: 80
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: nginx-svc
      namespace: default
    spec:
      ports:
      - port: 80
        targetPort: 80
        protocol: TCP
      selector:
        name: nginx
    
    kubectl apply -f ingress-dev.yaml
    kubectl get pod -n default -o wide

    查看对应的ClusterIP:

    kubectl get pods,svc -o wide
    测试访问该IP地址:
    curl 192.168.163.56

    配置ingress

    vim ingress.yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: nginx-test
      namespace: default
      annotations:
        kubernetes.io/ingress.class: "nginx"
    spec:
      rules:
      - host: test.sway.com.cn
        http:
          paths:
          - path: /
            pathType: Prefix    # 前缀匹配
            backend:
              service:
                name: nginx-svc
                port:
                  number: 80
    kubectl get ingress -n default

    测试服务的可访问情况:

    curl test.sway.com.cn

    至此,所有配置完成。

  • DRBD报错”0: Failure: (127) Device minor not allocated“

    查看drbd服务:
    systemctl status drbd
    如图所示,服务没有启动,那么我们启动即可:
    systemctl start drbd

    温馨提示:是不是忘了设置开机启动了?

  • DRBD脑裂”Split-Brain detected but unresolved, dropping connection!“

    主节点宕机后,备节点写入了数据,当主节点再次启动时出现脑裂提示:

    主节点状态:

    状态为StandAlone或者WFonnection

    备节点状态:

    状态为StandAlone或者WFonnection

    解决方法:

    情况一:以primary的数据为准:

    在从节点上操作:

    #设为从盘
    drbdadm secondary r0
    #丢弃修改
    drbdadm -- --discard-my-data connect r0

    查看执行结果:

    cat /proc/drbd
    恢复正常。

    情况二:以secondary的数据为准:

    在主节点上操作:

    #设为从盘
    drbdadm secondary r0
    #丢弃修改
    drbdadm -- --discard-my-data connect r0

    在从节点上手动连接资源:

    drbdadm connect r0

    查看执行结果:

    cat /proc/drbd

    其他

    若主备不正确,使用以下指令即可

    drbdadm primary r0

    如果“drbdadm secondary r0”时出现以下提示:

    则可能数据已被挂在,请先umount。

    如果卸载失败,可以重启服务器后再尝试。

  • DRBD提示”UpToDate/Diskless“无盘状态

    在“无盘”节点上重启drbd服务即可。若不可行,则可以尝试执行以下命令:

    drbdadm detach erp
    drbdadm connect erp
    drbdadm attach erp
    systemctl restart drbd.service
  • keepalived报“Can’t open PID file /var/run/keepalived.pid (yet?) after start: No such file or directory”

    情况一:权限不足

    解决方法,在keepalived.conf中增加:

       script_user root
       enable_script_security

    情况二:同一局域网存在相同的路由ID

    如果同一局域网内存在多组不同的keepalived,请确保keepalived.conf中的virtual_router_id与其他组的不一样,否出会存在交叉调用。

    当然也有可能时这样的报错:

    注意:同一组keepalived内需要相同。