feat:
- 实现Spring Security - 新增RoleMapper PermissionMapper
This commit is contained in:
parent
ee0109c876
commit
717395f8b3
17
pom.xml
17
pom.xml
@ -71,12 +71,12 @@
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||
<!-- <artifactId>spring-boot-devtools</artifactId>-->
|
||||
<!-- <scope>runtime</scope>-->
|
||||
<!-- <optional>true</optional>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@ -127,6 +127,11 @@
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -13,11 +13,5 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
public class WorkFlowMain {
|
||||
public static void main(String[] 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);
|
||||
}
|
||||
}
|
||||
@ -1,26 +1,61 @@
|
||||
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.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.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.password.PasswordEncoder;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
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
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SecurityConfig {
|
||||
|
||||
private final CustomerDetailsService customerDetailService;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/users", "/users/**").permitAll()
|
||||
.anyRequest().authenticated())
|
||||
.formLogin(formLogin -> formLogin
|
||||
.loginPage("/login")
|
||||
.permitAll())
|
||||
.rememberMe(Customizer.withDefaults());
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtil jwtUtil, AuthEntryPoint authEntryPoint, RequestMatcher requestMatcher) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.exceptionHandling(ex -> ex
|
||||
.authenticationEntryPoint(authEntryPoint)
|
||||
.accessDeniedHandler((request, response, accessDeniedException) -> response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"))
|
||||
)
|
||||
.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();
|
||||
}
|
||||
|
||||
@ -31,4 +66,32 @@ public class SecurityConfig {
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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("用户名或密码错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,12 @@ import asia.yulinling.workflow.model.ApiResponse;
|
||||
import asia.yulinling.workflow.dto.response.PageResult;
|
||||
import asia.yulinling.workflow.model.vo.user.UserVO;
|
||||
import asia.yulinling.workflow.service.UserService;
|
||||
import asia.yulinling.workflow.utils.JwtUtil;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
@ -29,6 +31,8 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class TestController {
|
||||
private final UserService userService;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
/**
|
||||
* 测试方法 GET
|
||||
@ -72,9 +76,4 @@ public class TestController {
|
||||
public ApiResponse<PageResult<UserVO>> usersPage(PageParam pageParam) {
|
||||
return userService.getUserListByPage(pageParam);
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public ApiResponse<String> login() {
|
||||
return ApiResponse.ofSuccess("登录成功");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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> {
|
||||
}
|
||||
19
src/main/java/asia/yulinling/workflow/mapper/RoleMapper.java
Normal file
19
src/main/java/asia/yulinling/workflow/mapper/RoleMapper.java
Normal 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> {
|
||||
}
|
||||
@ -3,8 +3,6 @@ package asia.yulinling.workflow.mapper;
|
||||
import asia.yulinling.workflow.model.entity.User;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@ -18,29 +16,4 @@ import org.springframework.stereotype.Component;
|
||||
@Mapper
|
||||
@Component
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
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.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@ -49,12 +54,6 @@ public class UserPrincipal implements UserDetails {
|
||||
@JsonIgnore
|
||||
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;
|
||||
|
||||
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
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
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
|
||||
public String getPassword() {
|
||||
return "";
|
||||
return this.password;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 "";
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ 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.service.UserService;
|
||||
import asia.yulinling.workflow.utils.PageUtils;
|
||||
import asia.yulinling.workflow.utils.PageUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -6,11 +6,12 @@ import io.jsonwebtoken.security.Keys;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import io.jsonwebtoken.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
@ -25,8 +26,8 @@ import java.util.List;
|
||||
* @since 2025/6/11
|
||||
*/
|
||||
@EnableConfigurationProperties(JwtConfig.class)
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public class JwtUtil {
|
||||
private final JwtConfig jwtConfig;
|
||||
@ -34,30 +35,35 @@ public class JwtUtil {
|
||||
/**
|
||||
* 创建JWT
|
||||
*
|
||||
* @param rememberMe 记住我
|
||||
* @param id 用户id
|
||||
* @param isRememberMe 记住我
|
||||
* @param userId 用户id
|
||||
* @param subject 用户名
|
||||
* @param roles 用户角色
|
||||
* @param authorities 用户权限
|
||||
* @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();
|
||||
|
||||
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()
|
||||
.setId(id.toString())
|
||||
.setId(String.valueOf(userId))
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(now)
|
||||
.signWith(key)
|
||||
.setExpiration(expiration)
|
||||
.claim("roles", roles)
|
||||
.claim("authorities", authorities);
|
||||
|
||||
// 设置过期时间
|
||||
Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
|
||||
builder.setExpiration(new Date(now.getTime() + ttl));
|
||||
.claim("authorities", authorities)
|
||||
.signWith(key);
|
||||
|
||||
// 5. 返回生成的 token 字符串
|
||||
return builder.compact();
|
||||
}
|
||||
|
||||
@ -65,13 +71,13 @@ public class JwtUtil {
|
||||
* 创建JWT
|
||||
*
|
||||
* @param authentication 用户认证信息
|
||||
* @param rememberMe 记住我
|
||||
* @param isRememberMe 记住我
|
||||
* @return JWT
|
||||
*/
|
||||
public String createJWT(Authentication authentication, Boolean rememberMe) {
|
||||
public String generateToken(Authentication authentication, Boolean isRememberMe) {
|
||||
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
||||
return createJWT(
|
||||
rememberMe,
|
||||
return generateToken(
|
||||
isRememberMe,
|
||||
userPrincipal.getId(),
|
||||
userPrincipal.getUsername(),
|
||||
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 {
|
||||
// 1. 构建密钥
|
||||
Key key = Keys.hmacShaKeyFor(jwtConfig.getKey().getBytes());
|
||||
Key key = getSigningKey();
|
||||
|
||||
// 2. 使用 parserBuilder 构建解析器
|
||||
Jws<Claims> jws = Jwts.parserBuilder()
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(key)
|
||||
.build()
|
||||
.parseClaimsJws(jwt);
|
||||
|
||||
Claims claims = jws.getBody();
|
||||
String id = claims.getId();
|
||||
String username = claims.getSubject();
|
||||
|
||||
return claims;
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.error("ExpiredJwtException", e);
|
||||
throw new JwtException("ExpiredJwtException");
|
||||
log.error("Token 已过期: {}", token, e);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import java.util.List;
|
||||
* @author YLL
|
||||
* @since 2025/6/9
|
||||
*/
|
||||
public class PageUtils {
|
||||
public class PageUtil {
|
||||
public static <T> PageResult<T> buildPageResult(IPage<?> iPage, List<T> list) {
|
||||
PageResult<T> result = new PageResult<>();
|
||||
|
||||
@ -26,7 +26,7 @@ public class PageUtils {
|
||||
// 可选字段
|
||||
int navigatePages = 5;
|
||||
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.setHasPrev(iPage.getCurrent() > 1);
|
||||
@ -46,14 +46,13 @@ COMMIT;
|
||||
BEGIN;
|
||||
|
||||
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
|
||||
) VALUES (
|
||||
1072806377661009920,
|
||||
'admin',
|
||||
'管理员',
|
||||
'ff342e862e7c3285cdc07e56d6b8973b',
|
||||
'412365a109674b2dbb1981ed561a4c70',
|
||||
'admin@xkcoding.com',
|
||||
'1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28
|
||||
1,
|
||||
@ -65,14 +64,13 @@ 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
|
||||
) VALUES (
|
||||
1072806378780889088,
|
||||
'user',
|
||||
'普通用户',
|
||||
'6c6bf02c8d5d3d128f34b1700cb1e32c',
|
||||
'fcbdd0e8a9404a5585ea4e01d0e4d7a0',
|
||||
'user@xkcoding.com',
|
||||
'1994-11-28 00:00:00', -- birthday: 785433600000 → 1994-11-28
|
||||
1,
|
||||
|
||||
@ -59,7 +59,6 @@ public class UserMapperTest {
|
||||
User user = User.builder()
|
||||
.username("yulinling_test")
|
||||
.password(SecureUtil.md5("123456" + salt))
|
||||
.salt(salt)
|
||||
.email("2712495353@qq.com")
|
||||
.phone("17770898274")
|
||||
.status(1)
|
||||
|
||||
109
src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java
Normal file
109
src/test/java/asia/yulinling/workflow/utils/JwtUtilTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user