树莓派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脑裂”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。

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

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内需要相同。

close