在OpenWRT中使用USB网卡桥接Wlan及Lan网络时偶现“br-lan: reveived packet on eth0 with own address as source address”的错误提示

使用以下命令查看网络端口的相关信息:

ip addr

然后返回如下信息:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq master br-lan state DOWN group default qlen 1000
    link/ether e4:5f:01:16:9b:b3 brd ff:ff:ff:ff:ff:ff
3: gre0@NONE: <NOARP> mtu 1476 qdisc noop state DOWN group default qlen 1000
    link/gre 0.0.0.0 brd 0.0.0.0
4: gretap0@NONE: <BROADCAST,MULTICAST> mtu 1462 qdisc noop state DOWN group default qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
5: erspan0@NONE: <BROADCAST,MULTICAST> mtu 1450 qdisc noop state DOWN group default qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
7: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master br-lan state UP group default qlen 1000
    link/ether e4:5f:01:16:9b:b4 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::e65f:1ff:fe16:9bb4/64 scope link
       valid_lft forever preferred_lft forever
17: br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether e4:5f:01:16:9b:b3 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global br-lan
       valid_lft forever preferred_lft forever
    inet6 fe80::e65f:1ff:fe16:9bb5/64 scope link
       valid_lft forever preferred_lft forever
54: usb0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master br-SIM7600G state UNKNOWN group default qlen 1000
    link/ether b6:09:ba:f2:6a:1f brd ff:ff:ff:ff:ff:ff
55: br-SIM7600G: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether b6:09:ba:f2:6a:1f brd ff:ff:ff:ff:ff:ff
    inet 192.168.225.54/24 brd 192.168.225.255 scope global br-SIM7600G
       valid_lft forever preferred_lft forever
    inet6 fe80::b409:baff:fef2:6a1f/64 scope link
       valid_lft forever preferred_lft forever

由上述信息可以看到,ip信息中的br-lan和eth0的mac地址一样的,分析原因可能是由于安装好OpenWRT并配置完USB网卡的桥接网络《br-lan》时,br-lan的mac地址默认可能会出现和其他网络端口的mac地址一样的情况。

因此,我们只需要更改其br-lan的mac地址即可解决这个问题。

登录OpenWRT的管理界面,找到如下位置:

如上图所示,我讲mac地址最后的B3改成了B5,于是重启设备生效后,该问题解决。

补充

经过之前的操作后,这个问题已经有一段时间消失没有出现了。但最近又再出现了,使用“ip addr”命令查看mac地址也没有存在一样的情况。这个时候可以使用以下方法尝试打开br-lan的STP功能:

#查看STP的开启情况
brctl show
#为br-lan开启STP
brctl stp br-lan on

然后再次查看STP的开启情况,此时应该已经开启成功,如下图所示:

在Windows下使用JBoss的standalone模式运行打成war包的SrpingBoot时使用外部配置文件

先在SpringBoot项目中新增一个MyApplicationContext.java文件,内容如下:

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.InputStreamResource;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        String activeProfile = applicationContext.getEnvironment().getProperty("spring.profiles.active");
        if("uat".equals(activeProfile) || "prod".equals(activeProfile)){
            YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
            try {
                String jbossHome = (String) applicationContext.getEnvironment().getSystemEnvironment().get("JBOSS_HOME");
                PropertySource<?> propertySource = loader.load("externalConfiguration", new InputStreamResource(Files.newInputStream(Paths.get(jbossHome+"\\standalone\\configuration\\application-druid.yml")))).get(0);
                applicationContext.getEnvironment().getPropertySources().addLast(propertySource);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

然后再resources的META-INF目录中创建一个spring.factories文件,内容需要指向到刚刚创建的MyApplicationContextInitializer类文件:

在系统中新建JBOSS_HOME环境并指向JBOSS的根目录:

然后在jboss的standalone\configuration目录中放置对应的配置文件:

然后启用jboss服务,此时springboot的war应用会优先到这个configuration目录中寻找对应的application配置文件,找不到时便会使用war包中的配置文件。

记一次使用VMWare 16.1无法安装Win10的问题

创建了一个虚拟机,ISO为Win10的官方镜像,启动后显示下面的画面后就自动关闭了:

然后尝试使用物理U盘启动,结果也会显示如下弹窗:

创建了一个虚拟机,ISO为Win10的官方镜像,启动后显示下面的画面后就自动关闭了:

然后尝试使用物理U盘启动,结果也会显示如下弹窗:

搜索资料后,可能是由于未开启CPU的虚拟化功能引起的,请确保虚拟机的设置中CPU已如下图所示:

然后启动虚拟机,但又有如下提示:

此时有三种原因,请顺序进行:

原因一:电脑是否支持Inter的VT-x技术并且在BIOS中已经开启?

解决方法:请自行搜索网络资料,这个问题比较基础,开启成功的话,Windows任务管理器的性能板块可以找到以下提示。

原因二:Windows的Hyper-V功能与VMWare的功能冲突。

解决方法:请确保Windows的功能开关如下图所示:

并确保相关的Service服务已经禁用,如下图所示:

原因三:虚拟机访问物理资源时一定是需要通过VMM去建立一个虚拟的Ring0权限,内核隔离开启后, 默认会启动hybrid-v, 这个东西和虚拟机是冲突的。

解决方法:打开“Windows安全中心>设备安全性>内核隔离”,确保是关闭的,如下图所示:

OK,至此问题解决,顺利进入安装程序:

树莓派zero w在烧录好镜像后配置wifi

方法一:

用读卡器读取刚刚烧录好的SD卡,然后再SD卡的根目录创建一个文件:wpa_supplicant.conf

country=CN
update_config=1
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev

network={
	ssid="RaspberryPi-OpenWrt"
	psk="woshishabi"
	key_mgmt=WPA-PSK
}
  • ssid:WiFi的名称
  • psk:WiFi的密码
  • key_mgmt:加密方式,以实际情况填写,家用目前通常为WPA/WPA2,我这里填写WPA-PSK即可。

注意保存的时候要用unix的文件格式,不然不生效。

方法二:

直接在pi zero中修改文件“/etc/wpa_supplicant/wpa_supplicant.conf”并在末尾添加如下代码:

network{
	ssid="z"                 #WIFI名
	psk="helloworld123"         #WIFI密码
	scan_ssid=1
}

其他

记得重启,然后可以使用以下命令来查看是否wifi已经生效:

ifconfig wlan0

如果仍然连接不上,可以使用以下指令查看pi所能scan得到的所有WIFI:

sudo iwlist wlan0 scan | grep ESSID

注意:zero的wifi是2.4G的,如果你的路由器wifi是5G的,则会出现scan不到的情况。

JBOSS EAP 7.4 修改在standalone下默认的HTTPS证书

jboss在standalone模式下的配置文件是在/standalone/configuration/standalone.xml,所以接下来的操作都在这个文件中进行。

确认JBOSS的HTTPS启用情况

打开配置文件,确保以下配置中的https的配置是存在的,且是你想要的:

    <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
        <socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
        <socket-binding name="http" port="${jboss.http.port:8080}"/>
        <socket-binding name="https" port="${jboss.https.port:8443}"/>
        <socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
        <socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
        <socket-binding name="txn-recovery-environment" port="4712"/>
        <socket-binding name="txn-status-manager" port="4713"/>
        <outbound-socket-binding name="mail-smtp">
            <remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:25}"/>
        </outbound-socket-binding>
    </socket-binding-group>

配置你的SSL证书

找到security-realms对应的配置:

        <security-realms>
            <security-realm name="ManagementRealm">
                <authentication>
                    <local default-user="$local" skip-group-loading="true"/>
                    <properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
                </authentication>
                <authorization map-groups-to-roles="false">
                    <properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
                </authorization>
            </security-realm>
            <security-realm name="ApplicationRealm">
                <server-identities>
                    <ssl>
                        <keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password" alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
                    </ssl>
                </server-identities>
                <authentication>
                    <local default-user="$local" allowed-users="*" skip-group-loading="true"/>
                    <properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
                </authentication>
                <authorization>
                    <properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
                </authorization>
            </security-realm>
        </security-realms>

在security-realms内增加你的security-realm,如:MySecurityRealm(其实以下代码是拷贝ApplicationRealm后修改其keystore的配置以及增加了truststore的配置,具体见高亮行)

            <security-realm name="MySecurityRealm">
                <server-identities>
                    <ssl>
                        <keystore path="D:\xxx\yourKeyStore.jks" keystore-password="123456" alias="yourKeyAlias" key-password="123456"/>
                    </ssl>
                </server-identities>
                <authentication>
                    <local default-user="$local" allowed-users="*" skip-group-loading="true"/>
                    <properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
                    <truststore path="D:\xxx\yourKeyStore.jks" keystore-password="123456"/>
                </authentication>
                <authorization>
                    <properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
                </authorization>
            </security-realm>

修改你需要配置的subsystem对应的security-realm

大家一般都是根目录部署,所以一般是搜索“default-host”关键字来找到对应的subsystem。

        <subsystem xmlns="urn:jboss:domain:undertow:12.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
            <buffer-cache name="default"/>
            <server name="default-server">
                <http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
                <https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
                <host name="default-host" alias="localhost">
                    <location name="/" handler="welcome-content"/>
                    <http-invoker security-realm="ApplicationRealm"/>
                </host>
            </server>
            <servlet-container name="default">
                <jsp-config/>
                <websockets/>
            </servlet-container>
            <handlers>
                <file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
            </handlers>
        </subsystem>

从上述代码片段中可以找到对应的“security-realm”的名称是“ApplicationRealm”,将他修改成“MySecurityRealm”。

重启JBOSS

重启以让配置生效。

自制用于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连接的时候需要一定的安全验证,我们跳过或者强制可信即可。

close