- 实现Spring Security
- 新增RoleMapper PermissionMapper
This commit is contained in:
yulinling 2025-06-13 23:49:25 +08:00
parent ee0109c876
commit 717395f8b3
20 changed files with 631 additions and 108 deletions

17
pom.xml
View File

@ -71,12 +71,12 @@
<artifactId>jakarta.mail</artifactId> <artifactId>jakarta.mail</artifactId>
<version>2.0.1</version> <version>2.0.1</version>
</dependency> </dependency>
<dependency> <!-- <dependency>-->
<groupId>org.springframework.boot</groupId> <!-- <groupId>org.springframework.boot</groupId>-->
<artifactId>spring-boot-devtools</artifactId> <!-- <artifactId>spring-boot-devtools</artifactId>-->
<scope>runtime</scope> <!-- <scope>runtime</scope>-->
<optional>true</optional> <!-- <optional>true</optional>-->
</dependency> <!-- </dependency>-->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
@ -127,6 +127,11 @@
<artifactId>jjwt-jackson</artifactId> <artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version> <version>0.11.5</version>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -13,11 +13,5 @@ import org.springframework.context.ConfigurableApplicationContext;
public class WorkFlowMain { public class WorkFlowMain {
public static void main(String[] args) { public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(WorkFlowMain.class, args); ConfigurableApplicationContext context = SpringApplication.run(WorkFlowMain.class, args);
int length = context.getBeanDefinitionCount();
log.trace("Number of beans in application context: {}", length);
log.debug("Number of beans in application context: {}", length);
log.info("Number of beans in application context: {}", length);
log.warn("Number of beans in application context: {}", length);
log.error("Number of beans in application context: {}", length);
} }
} }

View File

@ -1,26 +1,61 @@
package asia.yulinling.workflow.config; package asia.yulinling.workflow.config;
import asia.yulinling.workflow.model.vo.user.UserPrincipal;
import asia.yulinling.workflow.security.AuthEntryPoint;
import asia.yulinling.workflow.security.JwtAuthenticationFilter;
import asia.yulinling.workflow.service.CustomerDetailsService;
import asia.yulinling.workflow.utils.JwtUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import java.util.List;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig { public class SecurityConfig {
private final CustomerDetailsService customerDetailService;
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtil jwtUtil, AuthEntryPoint authEntryPoint, RequestMatcher requestMatcher) throws Exception {
http.authorizeHttpRequests(authorize -> authorize http
.requestMatchers("/users", "/users/**").permitAll() .csrf(AbstractHttpConfigurer::disable)
.anyRequest().authenticated()) .exceptionHandling(ex -> ex
.formLogin(formLogin -> formLogin .authenticationEntryPoint(authEntryPoint)
.loginPage("/login") .accessDeniedHandler((request, response, accessDeniedException) -> response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"))
.permitAll()) )
.rememberMe(Customizer.withDefaults()); .authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.requestMatchers("/users", "/users/**").authenticated()
.anyRequest().authenticated()
)
.sessionManagement(AbstractHttpConfigurer::disable)
.addFilterBefore(
new JwtAuthenticationFilter(jwtUtil, authEntryPoint, requestMatcher),
UsernamePasswordAuthenticationFilter.class
);
return http.build(); return http.build();
} }
@ -31,4 +66,32 @@ public class SecurityConfig {
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Bean RequestMatcher requestMatcher() {
return new AntPathRequestMatcher("/login", "POST");
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(customerDetailService);
return authProvider;
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserPrincipal user = new UserPrincipal(
1L,
"admin",
passwordEncoder.encode("123456"),
List.of("ADMIN"),
List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))
);
return new InMemoryUserDetailsManager(user);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
} }

View File

@ -0,0 +1,51 @@
package asia.yulinling.workflow.controller;
import asia.yulinling.workflow.dto.request.LoginRequest;
import asia.yulinling.workflow.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 登录控制层
* </p>
*
* @author YLL
* @since 2025/6/13
*/
@RestController
@RequiredArgsConstructor
@Slf4j
public class LoginController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = authenticationManager.authenticate(authToken);
log.info("{}", authentication.getPrincipal());
String token = jwtUtil.generateToken(authentication, false); // 可根据需要传 true/false
return ResponseEntity.ok(token);
} catch (AuthenticationException ex) {
log.info("{}", ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误");
}
}
}

View File

@ -8,10 +8,12 @@ import asia.yulinling.workflow.model.ApiResponse;
import asia.yulinling.workflow.dto.response.PageResult; import asia.yulinling.workflow.dto.response.PageResult;
import asia.yulinling.workflow.model.vo.user.UserVO; import asia.yulinling.workflow.model.vo.user.UserVO;
import asia.yulinling.workflow.service.UserService; import asia.yulinling.workflow.service.UserService;
import asia.yulinling.workflow.utils.JwtUtil;
import cn.hutool.core.lang.Dict; import cn.hutool.core.lang.Dict;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
@ -29,6 +31,8 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
public class TestController { public class TestController {
private final UserService userService; private final UserService userService;
private final JwtUtil jwtUtil;
private final AuthenticationManager authenticationManager;
/** /**
* 测试方法 GET * 测试方法 GET
@ -72,9 +76,4 @@ public class TestController {
public ApiResponse<PageResult<UserVO>> usersPage(PageParam pageParam) { public ApiResponse<PageResult<UserVO>> usersPage(PageParam pageParam) {
return userService.getUserListByPage(pageParam); return userService.getUserListByPage(pageParam);
} }
@GetMapping("/login")
public ApiResponse<String> login() {
return ApiResponse.ofSuccess("登录成功");
}
} }

View File

@ -0,0 +1,17 @@
package asia.yulinling.workflow.dto.request;
import lombok.Data;
/**
* <p>
* 登录请求类
* </p>
*
* @author YLL
* @since 2025/6/13
*/
@Data
public class LoginRequest {
private String username;
private String password;
}

View File

@ -0,0 +1,19 @@
package asia.yulinling.workflow.mapper;
import asia.yulinling.workflow.model.entity.Permission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;
/**
* <p>
*
* </p>
*
* @author YLL
* @since 2025/6/13
*/
@Mapper
@Component
public interface PermissionMapper extends BaseMapper<Permission> {
}

View File

@ -0,0 +1,19 @@
package asia.yulinling.workflow.mapper;
import asia.yulinling.workflow.model.entity.Role;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;
/**
* <p>
* 角色Mapper
* </p>
*
* @author YLL
* @since 2025/6/13
*/
@Mapper
@Component
public interface RoleMapper extends BaseMapper<Role> {
}

View File

@ -3,8 +3,6 @@ package asia.yulinling.workflow.mapper;
import asia.yulinling.workflow.model.entity.User; import asia.yulinling.workflow.model.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -18,29 +16,4 @@ import org.springframework.stereotype.Component;
@Mapper @Mapper
@Component @Component
public interface UserMapper extends BaseMapper<User> { public interface UserMapper extends BaseMapper<User> {
/**
* 根据id查询用户
*
* @param id 主键id
* @return 当前id的用户不存在则是{@code null}
*/
@Select("SELECT * FROM orm_user WHERE id = #{id}")
User selectUserById(@Param("id")Long id);
/**
* 保存用户
*
* @param user 用户
* @return 成功 - {@code 1} 失败 - {@code 0}
*/
int saveUser(@Param("user") User user);
/**
* 删除用户
*
* @param id 主键id
* @return 成功 - {@code 1} 失败 - {@code 0}
*/
int deleteById(@Param("id")Long id);
} }

View File

@ -1,18 +1,23 @@
package asia.yulinling.workflow.model.vo.user; package asia.yulinling.workflow.model.vo.user;
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.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* <p> * <p>
@ -49,12 +54,6 @@ public class UserPrincipal implements UserDetails {
@JsonIgnore @JsonIgnore
private String password; private String password;
/**
* 加密使用盐
*/
@JsonIgnore
private String salt;
/** /**
* 邮箱 * 邮箱
*/ */
@ -98,25 +97,80 @@ public class UserPrincipal implements UserDetails {
/** /**
* 用户角色列表 * 用户角色列表
*/ */
private List<String > roles; private List<String> roles;
/** /**
* 用户权限列表 * 用户权限列表
*/ */
private Collection<? extends GrantedAuthority> authorities; private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String username, String password, String nickname, String phone, String email, String birthday, Integer sex, Integer status, Date createTime, Date updateTime, List<String> roleNames, List<GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.nickname = nickname;
this.phone = phone;
this.email = email;
this.birthday = birthday;
this.sex = sex;
this.status = status;
this.createTime = createTime;
this.updateTime = updateTime;
this.roles = roleNames;
this.authorities = authorities;
}
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(); return List.of();
} }
public UserPrincipal(Long id, String username, String password,
List<String> roles, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.roles = roles;
this.authorities = authorities;
this.status = 1; // 启用
}
public static UserPrincipal create(User user, List<Role> roles, List<Permission> permissions) {
List<String> roleNames = roles.stream().map(Role::getName).collect(Collectors.toList());
List<GrantedAuthority> authorities = permissions.stream().filter(permission -> StrUtil.isNotBlank(permission.getPermission())).map(permission -> 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);
}
@Override @Override
public String getPassword() { public String getPassword() {
return ""; return this.password;
} }
@Override @Override
public String getUsername() { public String getUsername() {
return ""; return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true; // 视业务逻辑可改
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.status != null && this.status == 1;
} }
} }

View File

@ -0,0 +1,26 @@
package asia.yulinling.workflow.security;
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;
/**
* <p>
* 权限不足处理器
* </p>
*
* @author YLL
* @since 2025/6/13
*/
@Component
public class AuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Missing or invalid token");
}
}

View File

@ -0,0 +1,66 @@
package asia.yulinling.workflow.security;
import asia.yulinling.workflow.utils.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* <p>
* Jwt登录拦截器
* </p>
*
* @author YLL
* @since 2025/6/13
*/
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final RequestMatcher requestMatcher;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
)
throws ServletException, IOException {
log.info("{}", request.getRequestURI());
if (requestMatcher.matches(request)) {
log.info("{}", request.getRequestURI());
filterChain.doFilter(request, response);
return;
}
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
String token = null;
if (header != null) {
token = header.substring(7);
}
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.parseToken(token).getSubject();
String roles = jwtUtil.parseToken(token).get("roles").toString();
Authentication auth = new JwtAuthenticationToken(username, roles);
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
authenticationEntryPoint.commence(request, response, new AuthenticationException("") {
});
return;
}
}
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,37 @@
package asia.yulinling.workflow.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Collections;
/**
* <p>
* JWT认证
* </p>
*
* @author YLL
* @since 2025/6/13
*/
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final String principal;
private final String role;
public JwtAuthenticationToken(String username, String role) {
super(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role)));
this.principal = username;
this.role = role;
setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public Object getCredentials() {
return "";
}
}

View File

@ -0,0 +1,55 @@
package asia.yulinling.workflow.service;
import asia.yulinling.workflow.mapper.PermissionMapper;
import asia.yulinling.workflow.mapper.RoleMapper;
import asia.yulinling.workflow.mapper.UserMapper;
import asia.yulinling.workflow.model.entity.Permission;
import asia.yulinling.workflow.model.entity.Role;
import asia.yulinling.workflow.model.entity.User;
import asia.yulinling.workflow.model.vo.user.UserPrincipal;
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.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* 自定义UserDetail查询
* </p>
*
* @author YLL
* @since 2025/6/13
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomerDetailsService implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = Wrappers.lambdaQuery(User.class)
.eq(User::getUsername, username)
.or()
.eq(User::getEmail, username)
.or()
.eq(User::getPhone, username);
User user = userMapper.selectOne(queryWrapper);
List<Role> roles = roleMapper.selectByIds(Collections.singleton(user.getId()));
List<Long> roleIds = roles.stream().map(Role::getId).collect(Collectors.toList());
List<Permission> permissions = permissionMapper.selectByIds(roleIds);
return UserPrincipal.create(user, roles, permissions);
}
}

View File

@ -7,7 +7,7 @@ import asia.yulinling.workflow.dto.response.PageResult;
import asia.yulinling.workflow.model.entity.User; import asia.yulinling.workflow.model.entity.User;
import asia.yulinling.workflow.model.vo.user.UserVO; import asia.yulinling.workflow.model.vo.user.UserVO;
import asia.yulinling.workflow.service.UserService; import asia.yulinling.workflow.service.UserService;
import asia.yulinling.workflow.utils.PageUtils; import asia.yulinling.workflow.utils.PageUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -74,7 +74,7 @@ public class UserServiceImpl implements UserService {
} }
} }
PageResult<UserVO> pageResult = PageUtils.buildPageResult(users, userVOList); PageResult<UserVO> pageResult = PageUtil.buildPageResult(users, userVOList);
return ApiResponse.ofSuccess(pageResult); return ApiResponse.ofSuccess(pageResult);
} }

View File

@ -6,11 +6,12 @@ import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import io.jsonwebtoken.*; import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key; import java.security.Key;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
@ -25,8 +26,8 @@ import java.util.List;
* @since 2025/6/11 * @since 2025/6/11
*/ */
@EnableConfigurationProperties(JwtConfig.class) @EnableConfigurationProperties(JwtConfig.class)
@Configuration
@RequiredArgsConstructor @RequiredArgsConstructor
@Component
@Slf4j @Slf4j
public class JwtUtil { public class JwtUtil {
private final JwtConfig jwtConfig; private final JwtConfig jwtConfig;
@ -34,30 +35,35 @@ public class JwtUtil {
/** /**
* 创建JWT * 创建JWT
* *
* @param rememberMe 记住我 * @param isRememberMe 记住我
* @param id 用户id * @param userId 用户id
* @param subject 用户名 * @param subject 用户名
* @param roles 用户角色 * @param roles 用户角色
* @param authorities 用户权限 * @param authorities 用户权限
* @return JWT * @return JWT
*/ */
public String createJWT(Boolean rememberMe, Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) { public String generateToken(boolean isRememberMe, Long userId, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) {
// 1. 构建签名密钥
Key key = getSigningKey();
// 2. 当前时间
Date now = new Date(); Date now = new Date();
Key key = Keys.hmacShaKeyFor(jwtConfig.getKey().getBytes()); // 3. 计算过期时间
long ttl = isRememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
Date expiration = new Date(now.getTime() + ttl);
// 4. 构建 JWT
JwtBuilder builder = Jwts.builder() JwtBuilder builder = Jwts.builder()
.setId(id.toString()) .setId(String.valueOf(userId))
.setSubject(subject) .setSubject(subject)
.setIssuedAt(now) .setIssuedAt(now)
.signWith(key) .setExpiration(expiration)
.claim("roles", roles) .claim("roles", roles)
.claim("authorities", authorities); .claim("authorities", authorities)
.signWith(key);
// 设置过期时间
Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
builder.setExpiration(new Date(now.getTime() + ttl));
// 5. 返回生成的 token 字符串
return builder.compact(); return builder.compact();
} }
@ -65,13 +71,13 @@ public class JwtUtil {
* 创建JWT * 创建JWT
* *
* @param authentication 用户认证信息 * @param authentication 用户认证信息
* @param rememberMe 记住我 * @param isRememberMe 记住我
* @return JWT * @return JWT
*/ */
public String createJWT(Authentication authentication, Boolean rememberMe) { public String generateToken(Authentication authentication, Boolean isRememberMe) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return createJWT( return generateToken(
rememberMe, isRememberMe,
userPrincipal.getId(), userPrincipal.getId(),
userPrincipal.getUsername(), userPrincipal.getUsername(),
userPrincipal.getRoles(), userPrincipal.getRoles(),
@ -79,25 +85,58 @@ public class JwtUtil {
); );
} }
public Claims parseJWT(String jwt) { /**
* 解析Token
*
* @param token token信息
* @return 解析信息
*/
public Claims parseToken(String token) {
try { try {
// 1. 构建密钥 Key key = getSigningKey();
Key key = Keys.hmacShaKeyFor(jwtConfig.getKey().getBytes());
// 2. 使用 parserBuilder 构建解析器 return Jwts.parserBuilder()
Jws<Claims> jws = Jwts.parserBuilder()
.setSigningKey(key) .setSigningKey(key)
.build() .build()
.parseClaimsJws(jwt); .parseClaimsJws(token)
.getBody();
Claims claims = jws.getBody();
String id = claims.getId();
String username = claims.getSubject();
return claims;
} catch (ExpiredJwtException e) { } catch (ExpiredJwtException e) {
log.error("ExpiredJwtException", e); log.error("Token 已过期: {}", token, e);
throw new JwtException("ExpiredJwtException"); throw new JwtException("Token 已过期", e);
} catch (JwtException e) {
log.error("Token 无效: {}", token, e);
throw new JwtException("Token 无效", e);
} catch (Exception e) {
log.error("Token 解析异常: {}", token, e);
throw new JwtException("Token 解析失败", e);
} }
} }
/**
* 校验Token
*
* @param token token信息
* @return true-token正确 false-token错误
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
/**
* 获取签名密钥
*
* @return 返回key
*/
private Key getSigningKey() {
String secret = jwtConfig.getKey();
if (secret == null || secret.isEmpty()) {
throw new IllegalStateException("JWT 签名密钥未配置");
}
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
} }

View File

@ -7,13 +7,13 @@ import java.util.List;
/** /**
* <p> * <p>
* 分页工具类 * 分页工具类
* </p> * </p>
* *
* @author YLL * @author YLL
* @since 2025/6/9 * @since 2025/6/9
*/ */
public class PageUtils { public class PageUtil {
public static <T> PageResult<T> buildPageResult(IPage<?> iPage, List<T> list) { public static <T> PageResult<T> buildPageResult(IPage<?> iPage, List<T> list) {
PageResult<T> result = new PageResult<>(); PageResult<T> result = new PageResult<>();
@ -26,7 +26,7 @@ public class PageUtils {
// 可选字段 // 可选字段
int navigatePages = 5; int navigatePages = 5;
result.setNavigatePages(navigatePages); result.setNavigatePages(navigatePages);
result.setNavigatePageNums(calculateNavigatePageNumbers((int) iPage.getCurrent(), (int)iPage.getPages(), navigatePages)); result.setNavigatePageNums(calculateNavigatePageNumbers((int) iPage.getCurrent(), (int) iPage.getPages(), navigatePages));
result.setHasNext(iPage.getCurrent() < iPage.getPages()); result.setHasNext(iPage.getCurrent() < iPage.getPages());
result.setHasPrev(iPage.getCurrent() > 1); result.setHasPrev(iPage.getCurrent() > 1);
@ -36,7 +36,7 @@ public class PageUtils {
result.setFrom((int) ((iPage.getCurrent() - 1) * iPage.getSize() + 1)); result.setFrom((int) ((iPage.getCurrent() - 1) * iPage.getSize() + 1));
result.setTo((int) (Math.min(iPage.getCurrent() * iPage.getSize(), iPage.getTotal()))); result.setTo((int) (Math.min(iPage.getCurrent() * iPage.getSize(), iPage.getTotal())));
return result; return result;
} }
private static int[] calculateNavigatePageNumbers(int currentPage, int totalPage, int navigatePages) { private static int[] calculateNavigatePageNumbers(int currentPage, int totalPage, int navigatePages) {

View File

@ -46,14 +46,13 @@ COMMIT;
BEGIN; BEGIN;
INSERT INTO `wk_user` ( INSERT INTO `wk_user` (
id, username, nickname, password, salt, email, birthday, sex, phone, status, id, username, nickname, password, email, birthday, sex, phone, status,
create_time, update_time, last_login_time create_time, update_time, last_login_time
) VALUES ( ) VALUES (
1072806377661009920, 1072806377661009920,
'admin', 'admin',
'管理员', '管理员',
'ff342e862e7c3285cdc07e56d6b8973b', 'ff342e862e7c3285cdc07e56d6b8973b',
'412365a109674b2dbb1981ed561a4c70',
'admin@xkcoding.com', 'admin@xkcoding.com',
'1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28 '1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28
1, 1,
@ -65,14 +64,13 @@ INSERT INTO `wk_user` (
); );
INSERT INTO `wk_user` ( INSERT INTO `wk_user` (
id, username, nickname, password, salt, email, birthday, sex, phone, status, id, username, nickname, password, email, birthday, sex, phone, status,
create_time, update_time, last_login_time create_time, update_time, last_login_time
) VALUES ( ) VALUES (
1072806378780889088, 1072806378780889088,
'user', 'user',
'普通用户', '普通用户',
'6c6bf02c8d5d3d128f34b1700cb1e32c', '6c6bf02c8d5d3d128f34b1700cb1e32c',
'fcbdd0e8a9404a5585ea4e01d0e4d7a0',
'user@xkcoding.com', 'user@xkcoding.com',
'1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28 '1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28
1, 1,

View File

@ -59,7 +59,6 @@ public class UserMapperTest {
User user = User.builder() User user = User.builder()
.username("yulinling_test") .username("yulinling_test")
.password(SecureUtil.md5("123456" + salt)) .password(SecureUtil.md5("123456" + salt))
.salt(salt)
.email("2712495353@qq.com") .email("2712495353@qq.com")
.phone("17770898274") .phone("17770898274")
.status(1) .status(1)

View File

@ -0,0 +1,109 @@
package asia.yulinling.workflow.utils;
import asia.yulinling.workflow.config.JwtConfig;
import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* <p>
* Jwt工具类测试
* </p>
*
* @author YLL
* @since 2025/6/13
*/
class JwtUtilTest {
private JwtUtil jwtUtil;
@BeforeEach
void setUp() {
// 构造 JwtConfig 模拟配置
JwtConfig config = new JwtConfig();
config.setKey("your-256-bit-secret-key-which-needs-to-be-long-enough"); // 至少256位bit
config.setTtl(3600000L); // 1小时
config.setRemember(604800000L); // 7天
jwtUtil = new JwtUtil(config);
}
@Test
void testGenerateTokenAndParseToken() {
Long userId = 123L;
String username = "test_user";
List<String> roles = List.of("USER", "ADMIN");
List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
String token = jwtUtil.generateToken(false, userId, username, roles, authorities);
assertNotNull(token);
Claims claims = jwtUtil.parseToken(token);
assertEquals(userId.toString(), claims.getId());
assertEquals(username, claims.getSubject());
assertEquals(roles, claims.get("roles", List.class));
}
@Test
void testValidateToken() {
String token = jwtUtil.generateToken(false, 1L, "valid_user", List.of("USER"), List.of());
assertTrue(jwtUtil.validateToken(token));
}
@Test
void testValidateToken2() {
String invalidToken = "this.is.not.valid.jwt";
assertFalse(jwtUtil.validateToken(invalidToken));
}
@Test
void testGenerateTokenFromAuthentication() {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
new MockUserPrincipal(100L, "admin", List.of("ADMIN")),
null,
List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))
);
String token = jwtUtil.generateToken(authentication, false);
assertNotNull(token);
Claims claims = jwtUtil.parseToken(token);
assertEquals("admin", claims.getSubject());
assertEquals("100", claims.getId());
}
// Mock UserPrincipal 用于测试
static class MockUserPrincipal extends asia.yulinling.workflow.model.vo.user.UserPrincipal {
private final Long id;
private final String username;
private final List<String> roles;
public MockUserPrincipal(Long id, String username, List<String> roles) {
super(); // 假设父类有默认构造函数
this.id = id;
this.username = username;
this.roles = roles;
}
@Override
public Long getId() {
return id;
}
@Override
public String getUsername() {
return username;
}
@Override
public List<String> getRoles() {
return roles;
}
}
}