权限控制

This commit is contained in:
2025-06-24 08:50:54 +08:00
parent 76a571921a
commit f0566a5969
9 changed files with 452 additions and 129 deletions

View File

@@ -64,6 +64,31 @@
<version>21.6.0.0</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>

View File

@@ -0,0 +1,44 @@
package com.njcn.gather.event.transientes.controller;
import cn.hutool.json.JSONObject;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.event.transientes.websocket.WebSocketServer;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* @Author: cdf
* @CreateTime: 2025-06-23
* @Description:
*/
@Api(tags = "暂降接收")
@RequestMapping("accept")
@RestController
@RequiredArgsConstructor
public class EventGateController extends BaseController {
private final WebSocketServer webSocketServer;
@OperateInfo
@GetMapping("/eventMsg")
@ApiOperation("接收远程推送的暂态事件")
@ApiImplicitParam(name = "eventMsg", value = "暂态事件json字符", required = true)
public HttpResult<JSONObject> eventMsg(@RequestParam("msg") String msg) {
String methodDescribe = getMethodDescribe("eventMsg");
System.out.println(msg);
JSONObject jsonObject = new JSONObject(msg);
webSocketServer.sendMessageToUser("bbb",jsonObject.toString());
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, jsonObject, methodDescribe);
}
}

View File

@@ -78,4 +78,16 @@ public class LargeScreenCountController extends BaseController {
}
}

View File

@@ -0,0 +1,68 @@
package com.njcn.gather.event.transientes.filter;
import com.njcn.gather.event.transientes.utils.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
public class JwtRequestFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
// 可以在这里处理令牌过期的情况
log.info("JWT Token 校验异常,可能过期了");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token expired");
return; // 终止请求处理
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,63 @@
package com.njcn.gather.event.transientes.security;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.event.transientes.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/cn_authenticate")
public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
);
} catch (BadCredentialsException e) {
e.printStackTrace();
throw new BusinessException("Incorrect username or password");
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return jwt;
}
}
// 认证请求类
class AuthRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.event.transientes.security;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里应该从数据库中获取用户信息,本示例使用硬编码用户
if ("cdf".equals(username)) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodedPassword = passwordEncoder.encode("@#001njcnpqs");
return new User("cdf", encodedPassword,
new ArrayList<>());
}else if("screen".equals(username)){
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodedPassword = passwordEncoder.encode("@#001njcnpqs");
return new User("screen", encodedPassword,
new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}

View File

@@ -0,0 +1,56 @@
package com.njcn.gather.event.transientes.security;
import com.njcn.gather.event.transientes.filter.JwtRequestFilter;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/cn_authenticate").permitAll() // 允许访问认证接口
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 使用无状态会话
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,66 @@
package com.njcn.gather.event.transientes.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 100000L; // 10小时
// 生成JWT令牌
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.compact();
}
// 验证令牌
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 提取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 提取过期时间
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}

View File

@@ -1,6 +1,7 @@
package com.njcn.gather.event.transientes.websocket;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@@ -12,6 +13,9 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Description:
@@ -22,156 +26,105 @@ import java.util.concurrent.ConcurrentHashMap;
*/
@Slf4j
@Component
@ServerEndpoint(value ="/api/pushMessage/{userIdAndLineIdAndDevId}")
@ServerEndpoint(value = "/ws/{userId}")
public class WebSocketServer {
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set用来存放每个客户端对应的WebSocket对象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userIdAndLineIdAndDevId = "";
private static final ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>();
private final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(1);
/**
* 连接建立成
* 功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userIdAndLineIdAndDevId") String userIdAndLineIdAndDevId) {
this.session = session;
this.userIdAndLineIdAndDevId = userIdAndLineIdAndDevId;
if (webSocketMap.containsKey(userIdAndLineIdAndDevId)) {
webSocketMap.remove(userIdAndLineIdAndDevId);
//加入set中
webSocketMap.put(userIdAndLineIdAndDevId, this);
public void onOpen(Session session, @PathParam("userId") String userId) {
if (StrUtil.isNotBlank(userId)) {
sessions.put(userId, session);
sendMessage(session, "连接成功");
System.out.println("用户 " + userId + " 已连接");
// 启动心跳检测
startHeartbeat(session, userId);
} else {
//加入set中
webSocketMap.put(userIdAndLineIdAndDevId, this);
//在线数加1
addOnlineCount();
try {
session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "用户ID不能为空"));
} catch (IOException e) {
e.printStackTrace();
}
}
sendMessage("连接成功");
}
/**
* 连接关闭
* 调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userIdAndLineIdAndDevId)) {
webSocketMap.remove(userIdAndLineIdAndDevId);
//从set中删除
subOnlineCount();
}
log.info("监测点退出:" + userIdAndLineIdAndDevId + ",当前在线监测点数为:" + getOnlineCount());
}
/**
* 收到客户端消
* 息后调用的方法
*
* @param message 客户端发送过来的消息
**/
@OnMessage
public void onMessage(String message, Session session) {
//会每30s发送请求1次
log.info("监测点消息:" + userIdAndLineIdAndDevId + ",报文:" + message);
log.info("监测点连接:" + userIdAndLineIdAndDevId + ",当前在线监测点数为:" + getOnlineCount());
public void onMessage(String message, Session session, @PathParam("userId") String userId) {
if ("ping".equalsIgnoreCase(message)) {
// 处理心跳请求
sendMessage(session, "pong");
} else {
// 处理业务消息
System.out.println("收到用户 " + userId + " 的消息: " + message);
// TODO: 处理业务逻辑
}
}
@OnClose
public void onClose(Session session, CloseReason closeReason, @PathParam("userId") String userId) {
sessions.remove(userId);
System.out.println("用户 " + userId + " 已断开连接,状态码: " + closeReason.getReasonPhrase());
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("监测点错误:" + this.userIdAndLineIdAndDevId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务
* 器主动推送
*/
public void sendMessage(String message) {
public void onError(Session session, Throwable throwable, @PathParam("userId") String userId) {
System.out.println("用户 " + userId + " 发生错误: " + throwable.getMessage());
try {
this.session.getBasicRemote().sendText(message);
session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "发生错误"));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发送自定
* 义消息
**/
public static void sendInfo(String message, String lineId) {
log.info("发送消息到:" + lineId + ",报文:" + message);
if (StringUtils.isNotBlank(lineId)) {
Map<String, String> stringStringMap = WebSocketServer.filterMapByKey(webSocketMap, lineId);
stringStringMap.forEach((k,v)->{
webSocketMap.get(k).sendMessage(message);
});
} else {
log.error("监测点" + lineId + ",不在线!");
}
}
/**
* 获得此时的
* 在线监测点
*
* @return
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* 在线监测点
* 数加1
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
/**
* 在线监测点
* 数减1
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
/**
* 过滤所有键包含指定字符串的条目
* @param map 原始的Map
* @param substring 要检查的子字符串
* @return 过滤的Map
*/
public static Map<String, String> filterMapByKey(ConcurrentHashMap<String, WebSocketServer> map, String substring) {
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, WebSocketServer> entry : map.entrySet()) {
if (entry.getKey().contains(substring)) {
result.put(entry.getKey(), entry.getValue().toString());
public void sendMessageToUser(String userId, String message) {
Session session = sessions.get(userId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
System.out.println("发送消息给用户 " + userId + " 失败: " + e.getMessage());
}
} else {
System.out.println("用户 " + userId + " 不在线或会话已关闭");
}
return result;
}
public void sendMessageToAll(String message) {
sessions.forEach((userId, session) -> {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
System.out.println("发送消息给用户 " + userId + " 失败: " + e.getMessage());
}
}
});
}
private void sendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
System.out.println("发送消息失败: " + e.getMessage());
}
}
private void startHeartbeat(Session session, String userId) {
heartbeatExecutor.scheduleAtFixedRate(() -> {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText("ping");
} catch (IOException e) {
System.out.println("发送心跳给用户 " + userId + " 失败: " + e.getMessage());
try {
session.close(new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "心跳失败"));
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}, 10, 10, TimeUnit.SECONDS); // 每10秒发送一次心跳
}
}