feat: 优化Security

This commit is contained in:
yulinling 2025-06-23 22:23:26 +08:00
parent 018c644d68
commit 5d44035a2f
13 changed files with 196 additions and 64 deletions

View File

@ -1,5 +1,6 @@
package asia.yulinling.workflow.config;
import asia.yulinling.workflow.scope.RequestScopeData;
import asia.yulinling.workflow.security.*;
import asia.yulinling.workflow.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
@ -34,6 +35,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
@Slf4j
public class SecurityConfig {
private final RequestScopeData scopeData;
private final JwtUserDetailsService jwtUserDetailsService;
private final JwtRbacAuthenticationService jwtRbacAuthenticationService;
@ -68,7 +70,7 @@ public class SecurityConfig {
)
// JWT过滤器
.addFilterBefore(
new JwtAuthenticationFilter(jwtUtil, jwtUserDetailsService),
new JwtAuthenticationFilter(jwtUtil, scopeData, jwtUserDetailsService),
UsernamePasswordAuthenticationFilter.class
);

View File

@ -0,0 +1,39 @@
package asia.yulinling.workflow.dto.request;
import lombok.Data;
/**
* <p>
* 更新用户信息类
* </p>
*
* @author YLL
* @since 2025/6/23
*/
@Data
public class UpdateUserRequest {
/**
* 主键id
*/
private Long id;
/**
* 昵称
*/
private String nickname;
/**
* 生日
*/
private String birthday;
/**
* 性别,-1,-2
*/
private Integer sex;
/**
* 手机号
*/
private String phone;
}

View File

@ -4,13 +4,11 @@ import asia.yulinling.workflow.model.entity.Permission;
import asia.yulinling.workflow.model.entity.Role;
import asia.yulinling.workflow.model.entity.User;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@ -37,7 +35,6 @@ public class UserPrincipal implements UserDetails {
/**
* 主键id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
@ -53,7 +50,6 @@ public class UserPrincipal implements UserDetails {
/**
* 加密后的密码
*/
@JsonIgnore
private String password;
/**
@ -156,8 +152,6 @@ public class UserPrincipal implements UserDetails {
List<Role> roles,
List<Permission> permissions) {
log.info("roleNames{}\n{}authorities", roles, permissions);
List<String> roleNames = roles
.stream()
.map(Role::getName)
@ -171,20 +165,12 @@ public class UserPrincipal implements UserDetails {
new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getUsername(),
user.getPassword(),
user.getNickname(),
user.getPhone(),
user.getEmail(),
user.getBirthday(),
user.getSex(),
user.getStatus(),
user.getCreateTime(),
user.getUpdateTime(),
roleNames,
authorities);
UserPrincipal userPrincipal = new UserPrincipal();
BeanUtils.copyProperties(user, userPrincipal);
userPrincipal.setRoles(roleNames);
userPrincipal.setAuthorities(authorities);
return userPrincipal;
}

View File

@ -0,0 +1,22 @@
package asia.yulinling.workflow.scope;
import lombok.Data;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
/**
* <p>
* 请求生命周期的全局数据
* </p>
*
* @author YLL
* @since 2025/6/23
*/
@Component
@RequestScope
@Data
public class RequestScopeData {
private String token;
private Long userId;
private boolean isLogin;
}

View File

@ -1,8 +1,8 @@
package asia.yulinling.workflow.security;
import asia.yulinling.workflow.exception.SecurityException;
import asia.yulinling.workflow.constant.Status;
import asia.yulinling.workflow.model.vo.user.UserPrincipal;
import asia.yulinling.workflow.scope.RequestScopeData;
import asia.yulinling.workflow.utils.JwtUtil;
import asia.yulinling.workflow.utils.ResponseUtil;
import jakarta.servlet.FilterChain;
@ -32,6 +32,7 @@ import java.io.IOException;
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final RequestScopeData scopeData;
private final JwtUserDetailsService jwtUserDetailsService;
@Override
@ -45,7 +46,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
try {
// 2. 解析token获取username
String username = jwtUtil.parseToken(token).getSubject();
String username = jwtUtil.getUsernameByToken(token);
Long userId = jwtUtil.getUserIdByToken(token);
scopeData.setUserId(userId);
scopeData.setToken(token);
scopeData.setLogin(true);
// 3. 根据username验证password
UserPrincipal userPrincipal = jwtUserDetailsService.loadUserByUsername(username);
@ -57,11 +63,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} catch (SecurityException e) {
log.error("Security异常:{}", e.getMessage());
ResponseUtil.renderJson(response, e);
}
} else {
scopeData.setLogin(false);
}
filterChain.doFilter(request, response);

View File

@ -12,7 +12,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
@ -57,8 +56,8 @@ public class JwtRbacAuthenticationService {
List<Permission> pagePerms = permissions.stream()
.filter(permission -> Objects.equals(permission.getType(), 1))
.filter(permission -> !StringUtils.isEmpty(permission.getUrl()))
.filter(permission -> !StringUtils.isEmpty(permission.getMethod()))
.filter(permission -> !permission.getUrl().isEmpty())
.filter(permission -> !permission.getMethod().isEmpty())
.toList();
for (Permission permission : pagePerms) {

View File

@ -2,7 +2,6 @@ package asia.yulinling.workflow.security;
import asia.yulinling.workflow.mapper.PermissionMapper;
import asia.yulinling.workflow.mapper.RoleMapper;
import asia.yulinling.workflow.mapper.RoleUserMapper;
import asia.yulinling.workflow.mapper.UserMapper;
import asia.yulinling.workflow.model.entity.Permission;
import asia.yulinling.workflow.model.entity.Role;
@ -12,6 +11,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@ -33,7 +33,6 @@ import java.util.stream.Collectors;
public class JwtUserDetailsService implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final RoleUserMapper roleUserMapper;
private final PermissionMapper permissionMapper;
@Override
@ -53,21 +52,20 @@ public class JwtUserDetailsService implements UserDetailsService {
log.error("未找到用户信息{}", username);
throw new UsernameNotFoundException("未找到用户信息:" + username);
}
log.info("username:{}", username);
// 3. 查找用户对应的角色
List<Role> roles = roleMapper.selectRoleByUserId(user.getId());
if (roles.isEmpty()) {
log.error("未找到角色信息{}", roles);
throw new UsernameNotFoundException("未找到角色信息" + roles);
throw new AccessDeniedException("未找到角色信息" + roles);
}
List<Permission> permissions = permissionMapper.selectPermissionsByRoleId(roles.stream().map(Role::getId).collect(Collectors.toList()));
if (permissions.isEmpty()) {
log.error("未找到权限信息{}", permissions);
throw new UsernameNotFoundException("未找到权限信息" + permissions);
throw new AccessDeniedException("未找到权限信息" + permissions);
}
// 4. 返回User

View File

@ -1,8 +1,9 @@
package asia.yulinling.workflow.service;
import asia.yulinling.workflow.dto.request.PageParam;
import asia.yulinling.workflow.model.ApiResponse;
import asia.yulinling.workflow.dto.request.UpdateUserRequest;
import asia.yulinling.workflow.dto.response.PageResult;
import asia.yulinling.workflow.model.ApiResponse;
import asia.yulinling.workflow.model.vo.user.UserVO;
import org.springframework.transaction.annotation.Transactional;
@ -24,4 +25,12 @@ public interface UserService {
* @return 用户列表
*/
ApiResponse<PageResult<UserVO>> getUserListByPage(PageParam pageParam);
/**
* 更新用户信息
*
* @param request UpdateUserRequest
* @return 更新状态
*/
ApiResponse<?> updateUserInfo(UpdateUserRequest request);
}

View File

@ -1,18 +1,23 @@
package asia.yulinling.workflow.service.impl;
import asia.yulinling.workflow.dto.request.PageParam;
import asia.yulinling.workflow.dto.request.UpdateUserRequest;
import asia.yulinling.workflow.dto.response.PageResult;
import asia.yulinling.workflow.mapper.UserMapper;
import asia.yulinling.workflow.model.ApiResponse;
import asia.yulinling.workflow.dto.response.PageResult;
import asia.yulinling.workflow.model.entity.User;
import asia.yulinling.workflow.model.vo.user.UserVO;
import asia.yulinling.workflow.scope.RequestScopeData;
import asia.yulinling.workflow.service.UserService;
import asia.yulinling.workflow.utils.PageUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
@ -32,6 +37,7 @@ public class UserServiceImpl implements UserService {
/** 注入用户Mapper */
private final UserMapper userMapper;
private final RequestScopeData scopeData;
/**
* 获取用户列表分页
@ -65,4 +71,41 @@ public class UserServiceImpl implements UserService {
throw new RuntimeException(e);
}
}
/**
* 更新用户信息
*
* @param request UpdateUserRequest
* @return 更新状态
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ApiResponse<?> updateUserInfo(UpdateUserRequest request) {
Long userId = scopeData.getUserId();
Long requestUserId = request.getId();
if (!requestUserId.equals(userId)) {
log.error("user id not match, scopeData: {}, requestUserId: {}", requestUserId, userId);
throw new RuntimeException("用户Id不匹配");
}
User user = new User();
BeanUtils.copyProperties(request, user);
LambdaUpdateWrapper<User> userLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
userLambdaUpdateWrapper.eq(User::getId, userId);
userLambdaUpdateWrapper.set(StringUtils.hasText(request.getNickname()), User::getNickname, request.getNickname());
userLambdaUpdateWrapper.set(StringUtils.hasText(request.getBirthday()), User::getBirthday, request.getBirthday());
userLambdaUpdateWrapper.set(request.getSex() != null, User::getSex, request.getSex());
userLambdaUpdateWrapper.set(StringUtils.hasText(request.getPhone()), User::getPhone, request.getPhone());
try {
userMapper.update(userLambdaUpdateWrapper);
return ApiResponse.ofSuccess("更新成功");
} catch (Exception e) {
log.error("update user error:{}", e.getMessage());
return ApiResponse.ofSuccess("更新失败");
}
}
}

View File

@ -3,7 +3,9 @@ package asia.yulinling.workflow.utils;
import asia.yulinling.workflow.constant.Const;
import asia.yulinling.workflow.constant.Status;
import asia.yulinling.workflow.exception.SecurityException;
import asia.yulinling.workflow.model.vo.user.UserPrincipal;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
@ -13,7 +15,6 @@ import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.security.Key;
@ -59,6 +60,27 @@ public class JwtUtil {
* @return JWT
*/
public String generateToken(Authentication authentication, boolean isRememberMe) {
// 1. 解析principal
Object principal = authentication.getPrincipal();
Long userId = null;
String username = null;
if (principal instanceof UserPrincipal) {
userId = ((UserPrincipal) principal).getId();
username = ((UserPrincipal) principal).getUsername();
}
return generateToken(userId, username, isRememberMe);
}
/**
* 创建JWT
*
* @param userId 用户Id
* @param username 用户名
* @param isRememberMe 记住我
* @return JWT
*/
public String generateToken(Long userId, String username, boolean isRememberMe) {
// 1. 当前时间
Date now = new Date();
@ -66,18 +88,20 @@ public class JwtUtil {
long ttl = isRememberMe ? this.remember : this.ttl;
Date expiration = new Date(now.getTime() + ttl);
// 3. 构建 JWT
String username = authentication.getName();
// 3. 构建JwtBuilder
JwtBuilder builder = Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(this.key());
// 4. 生成token
String token = builder.compact();
// 5. 将token存入redis
stringRedisTemplate.opsForValue().set(Const.REDIS_JWT_KEY_PREFIX + username, token, ttl, TimeUnit.SECONDS);
// 6. 返回生成的token
return token;
}
@ -122,6 +146,16 @@ public class JwtUtil {
}
}
public Long getUserIdByToken(String token) {
Claims claims = parseToken(token);
return (Long) claims.get("userId");
}
public String getUsernameByToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 校验Token
*

View File

@ -1,24 +1,22 @@
BEGIN;
INSERT INTO `wk_permission`
VALUES (1072806379288399872, '测试页面', '/test', 1, 'page:test', 'GET', 1, 0);
VALUES (1072806379288399872, '测试页面', '/test', 1, 'page:test', 'GET', 1, 0, NULL, NULL);
INSERT INTO `wk_permission`
VALUES (1072806379313565696, '测试页面-查询', '/**/test', 2, 'btn:test:query', 'GET', 1, 1072806379288399872);
VALUES (1072806379313565696, '测试页面-查询', '/**/test', 2, 'btn:test:query', 'GET', 1, 1072806379288399872, NULL,
NULL);
INSERT INTO `wk_permission`
VALUES (1072806379330342912, '测试页面-添加', '/**/test', 2, 'btn:test:insert', 'POST', 2, 1072806379288399872);
VALUES (1072806379330342912, '测试页面-添加', '/**/test', 2, 'btn:test:insert', 'POST', 2, 1072806379288399872, NULL,
NULL);
INSERT INTO `wk_permission`
VALUES (1072806379342925824, '监控在线用户页面', '/monitor', 1, 'page:monitor:online', NULL, 2, 0);
VALUES (1072806379342925824, '监控在线用户页面', '/monitor', 1, 'page:monitor:online', NULL, 2, 0, NULL, NULL);
INSERT INTO `wk_permission`
VALUES (1072806379363897344, '在线用户页面-查询', '/**/api/monitor/online/user', 2, 'btn:monitor:online:query', 'GET',
1,
1072806379342925824);
1, 1072806379342925824, NULL, NULL);
INSERT INTO `wk_permission`
VALUES (1072806379384868864, '在线用户页面-踢出', '/**/api/monitor/online/user/kickout', 2,
'btn:monitor:online:kickout',
'DELETE', 2, 1072806379342925824);
'btn:monitor:online:kickout', 'DELETE', 2, 1072806379342925824, NULL, NULL);
INSERT INTO `wk_permission`
VALUES (1072806379384868865, '用户列表', '/users', 1,
'page:test',
'GET', 1, 0);
VALUES (1072806379384868865, '用户列表', '/users', 1, 'page:test', 'GET', 1, 0, NULL, NULL);
COMMIT;
BEGIN;

View File

@ -21,11 +21,6 @@ public class UserServiceTest extends WorkFlowMainTests {
@Autowired
private UserService userService;
@Test
public void getUserList() {
ApiResponse<PageResult<UserVO>> users = userService.getUserList();
Assertions.assertEquals(200, users.getCode().intValue());
}
@Test
public void getUserListByPage() {