前言
本教程基于 Spring Boot 3.x + Spring Security 6.x 实现,采用 JWT + Redis 的认证方案,结合 RBAC 权限模型,实现了一个完整的权限管理系统。
一、项目依赖配置
关键依赖说明:
<!-- SpringWeb --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- mysql驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- Druid 连接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-3-starter</artifactId><version>1.2.21</version></dependency><!-- MybatisPlus起步依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version></dependency><!-- Knife4j API文档生产工具 --><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId><version>4.4.0</version></dependency><!-- swagger注解支持:Knife4j依赖本依赖 --><dependency><groupId>io.swagger</groupId><artifactId>swagger-annotations</artifactId><version>1.5.22</version></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- SpringSecurity --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- validation参数校验依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.40</version></dependency><!-- Hutool工具类库:图形验证码生成、加解密、简单http请求、类拷贝等 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.25</version></dependency><!-- JWT依赖:用于生成和解析JWT --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- 如果jdk大于1.8,则还需导入以下依赖 --><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><!-- SpringTest --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
二、创建RBAC权限数据表
RBAC 模型
,基于角色的访问控制,这是软件设计中最常用的权限管理模型,通过权限关联角色、角色关联用户的方法来间接地赋予用户权限,从而实现用户与权限的解耦。
💡 RBAC模型说明:
- 用户表(user):存储用户基本信息
- 角色表(role):定义系统角色
- 菜单表(menu):存储系统菜单和权限标识
- 关联表:通过中间表实现多对多关系
1. 员工表(用户表)
-- auto-generated definition
create table emp
(id bigint unsigned auto_increment comment 'ID'primary key,dept_id bigint unsigned null comment '部门ID',username varchar(50) not null comment '用户名',password varchar(255) null comment '密码',name varchar(50) not null comment '姓名',status tinyint unsigned default '0' null comment '状态 0-正常 1-禁用',constraint usernameunique (username)
)comment '员工表' row_format = DYNAMIC;
2.菜单表
-- auto-generated definition
create table menu
(id bigint unsigned auto_increment comment '菜单ID'primary key,parent_id bigint unsigned null comment '父菜单ID(支持多级菜单)',menu_name varchar(50) default 'NULL' not null comment '菜单名称',path varchar(255) null comment '路由地址',component varchar(255) null comment '组件路径',visible tinyint default 0 null comment '菜单状态(0显示 1隐藏)',status tinyint default 0 null comment '菜单状态(0正常 1禁用)',perms varchar(100) null comment '权限标识( 如user:read )',icon varchar(100) null comment '菜单图标',order_num tinyint null comment '显示顺序',type char null comment '菜单类型(''M''-菜单 ''B''-按钮)',create_time datetime null comment '创建时间',create_user bigint null comment '创建者ID',update_time datetime null comment '更新时间',update_user bigint null comment '更新者ID'
)comment '菜单表' row_format = DYNAMIC;
3.角色表
-- auto-generated definition
create table role
(id bigint unsigned auto_increment comment '主键ID'primary key,name varchar(50) null comment '角色名称',role_key varchar(50) null comment '角色权限标识(如ADMIN)',description varchar(255) null comment '角色描述',status tinyint default 0 null comment '角色状态(0正常 1停用)',create_time datetime null comment '创建时间',create_user bigint null comment '创建者ID',update_time datetime null comment '更新时间',update_user bigint null comment '更新者ID'
)comment '角色表' row_format = DYNAMIC;
4.员工角色关联表
-- auto-generated definition
create table emp_role
(emp_id bigint unsigned auto_increment comment '员工id',role_id bigint unsigned default '0' not null comment '角色id',primary key (emp_id, role_id)
)row_format = DYNAMIC;
5.角色菜单关联表
-- auto-generated definition
create table role_menu
(role_id bigint unsigned auto_increment comment '角色ID',menu_id bigint unsigned default '0' not null comment '菜单id',primary key (role_id, menu_id)
)row_format = DYNAMIC;
三、核心配置类
1. 安全配置类
创建 SecurityConfig.java
:
/*** @description SpringSecurity配置类*/
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 开启SpringSecurity的自定义配置(在SpringBoot项目中可以省略)
@EnableMethodSecurity // 开启全局函数权限
public class SecurityConfig {// 自定义的用于认证的过滤器,进行jwt的校验操作private final JwtTokenOncePerRequestFilter jwtTokenFilter;// 认证用户无权限访问资源的处理器private final CustomerAccessDeniedHandler customerAccessDeniedHandler;// 客户端进行认证数据的提交时出现异常,或者是匿名用户访问受限资源的处理器private final AnonymousAuthenticationHandler anonymousAuthentication;// 用户认证校验失败处理器private final LoginFailureHandler loginFailureHandler;/*** 创建BCryptPasswordEncoder注入容器,用于密码加密*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 登录时调用AuthenticationManager.authenticate执行一次校验*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception{// 添加自定义异常处理类http.exceptionHandling(configurer -> {configurer.accessDeniedHandler(customerAccessDeniedHandler) // 配置认证用户无权限访问资源的处理器.authenticationEntryPoint(anonymousAuthentication); // 配置匿名用户未认证的处理器});// 配置关闭csrf机制http.csrf(AbstractHttpConfigurer::disable);// 用户认证校验失败处理器http.formLogin(conf -> conf.failureHandler(loginFailureHandler));// STATELESS(无状态):表示应用程序是无状态的,不创建会话http.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS));// 配置放行路径http.authorizeHttpRequests(auth -> auth.requestMatchers("/swagger-ui/**", // 放行Swagger相关路径"/swagger-ui.html","/swagger-resources/**","/v3/api-docs/**","/webjars/**","/doc.html","/admin/emp/login" // 放行登录接口路径).permitAll().anyRequest().authenticated());// 配置过滤器的执行顺序http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}}
什么是 BCrypt 加密?
BCrypt 是一个基于 Blowfish 密码算法的密码哈希函数,专门为密码加密而设计。它具有以下特点:
- 自适应性:可以通过增加迭代次数来增加计算强度
- 加盐处理:自动生成随机盐值并混入密码
- 防彩虹表:每次加密同一个密码得到的结果都不同
- 单向加密:无法通过加密后的密文反推原始密码
2. JWT工具类
创建 JwtUtil.java
:
/*** JWT令牌工具类*/
public class JwtUtil {/*** 生成jwt* 使用Hs256算法, 私匙使用固定秘钥** @param secretKey jwt秘钥* @param ttlMillis jwt过期时间(毫秒)* @param claims 设置的信息* @return*/public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {// 指定签名的时候使用的签名算法,也就是header那部分SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;// 生成JWT的时间long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setClaims(claims)// 设置签名使用的签名算法和签名使用的秘钥.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 设置过期时间.setExpiration(exp);return builder.compact();}/*** Token解密** @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个* @param token 加密后的token* @return*/public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 设置签名的秘钥.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 设置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}}
在application.yml
中加入JWT相关配置:
jwt:secret-key: your_key # jwt签名加密秘钥ttl: 7200000 # jwt过期时间token-name: Authorization # 前端传递过来的令牌名称
创建 JwtProperties.java
:
/*** 生成jwt令牌相关配置*/
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {private String secretKey; // jwt签名加密秘钥private long ttl; // jwt过期时间private String tokenName; // jwt签名加密秘钥}
四、用户认证实现
1.创建员工实体类
创建 Emp.java
:
/*** 管理员实体类*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Tag(name = "Emp", description = "员工实体")
public class Emp implements Serializable {@Serialprivate static final long serialVersionUID = 4317337818874663187L;@Schema(description = "员工ID")private Long id;@Schema(description = "部门ID")private Long deptId;@Schema(description = "用户名")private String username;@JSONField(serialize = false)@Schema(description = "密码")private String password;@Schema(description = "姓名")private String name;@Schema(description = "状态: 0-正常, 1-禁用")private Integer status;
}
2. 自定义用户详情类
创建 EmpLogin.java
实现UserDetails
接口:用于封装用户的详细信息和权限列表
/*** @description UserDetails的实现类*/
@Data
@NoArgsConstructor
public class EmpLogin implements UserDetails {@Serialprivate static final long serialVersionUID = 7330836274775504268L;public EmpLogin(Emp emp, List<String> list) {this.emp = emp;this.list = list;}// 权限列表private List<String> list;private Emp emp;//自定义一个权限列表的集合,中转操作@JSONField(serialize = false) //在序列化对象时忽略该字段private List<SimpleGrantedAuthority> authorities;// 用于返回权限信息@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if (authorities != null) {return authorities;}authorities = new ArrayList<>();for (String item : list) {if (item != null && !item.trim().isEmpty()) {SimpleGrantedAuthority authority = new SimpleGrantedAuthority(item);authorities.add(authority);}}return authorities;}// 获取密码@Overridepublic String getPassword() {return emp.getPassword();}// 获取用户名@Overridepublic String getUsername() {return emp.getUsername();}// 账号是否未过期@Overridepublic boolean isAccountNonExpired() {return UserDetails.super.isAccountNonExpired();}// 判断账号是否没有锁定@Overridepublic boolean isAccountNonLocked() {return UserDetails.super.isAccountNonLocked();}// 判断账户是否没有超时@Overridepublic boolean isCredentialsNonExpired() {return UserDetails.super.isCredentialsNonExpired();}// 判断账号是否可用@Overridepublic boolean isEnabled() {return UserDetails.super.isEnabled();}
}
说明:
getAuthorities
方法构建用户的权限集合。- 重写
getPassword
和getUsername
方法,用于提供用户的凭证
3. 实现UserDetailsService
创建 UserDetailsServiceImpl.java
实现UserDetailsService
接口:完成自定义用户查询逻辑
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {private final EmpMapper empMapper;private final MenuMapper menuMapper;/*** 根据用户名查询用户信息*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {if (username.isEmpty()){throw new InternalAuthenticationServiceException("");}// 根据用户名查询用户信息QueryWrapper<Emp> wrapper = new QueryWrapper<>();wrapper.eq("username", username);Emp emp = empMapper.selectOne(wrapper);// 判断是否查到用户 如果没查到抛出异常if (ObjectUtil.isNull(emp)){throw new UsernameNotFoundException("");}// 2.赋权操作 查询数据库List<String> list = menuMapper.getMenuByUserId(emp.getId());for (String s : list) {System.out.println(s);}return new EmpLogin(emp, list);}
}
说明:
- 根据用户名查询用户信息,如果用户不存在,抛出异常。
- 从数据库中查询用户的权限列表并封装到
EmpLogin
对象中。
五、JWT认证过滤器
创建 JwtAuthenticationTokenFilter.java
:用于拦截请求并校验 JWT 的有效性
/*** @description 每次请求的 Security 过滤类。执行jwt有效性检查,如果失败,不会设置 SecurityContextHolder 信息,会进入 AuthenticationEntryPoint*/
// 每一个servlet请求,只执行一次
@Component
@Slf4j
public class JwtTokenOncePerRequestFilter extends OncePerRequestFilter {@Autowiredprivate JwtProperties jwtProperties; // JWT相关属性配置类@Autowiredprivate RedisUtil redisUtil; // Redis工具类@Autowiredprivate LoginFailureHandler loginFailureHandler;// 添加白名单路径列表private final String[] whitelist = {"/admin/emp/login","/swagger-ui/**","/swagger-ui.html","/swagger-resources/**","/v3/api-docs/**","/webjars/**","/doc.html"};@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 1. 判断当前请求是否在白名单中String uri = request.getRequestURI();if (isWhitelisted(uri)) {filterChain.doFilter(request, response);return;}try {// 2. 校验tokenthis.validateToken(request);} catch (AuthenticationException e) {loginFailureHandler.onAuthenticationFailure(request, response, e); // 处理登录失败的异常return;}filterChain.doFilter(request, response);}// 判断请求路径是否在白名单中private boolean isWhitelisted(String uri) {for (String pattern : whitelist) {if (pattern.endsWith("/**")) {// 处理通配符路径String basePattern = pattern.substring(0, pattern.length() - 3);if (uri.startsWith(basePattern)) {return true;}} else if (pattern.equals(uri)) {// 精确匹配return true;}}return false;}// 校验tokenprivate void validateToken(HttpServletRequest request) {// 说明:登录了,再次请求其他需要认证的资源String token = request.getHeader("Authorization");if (ObjectUtils.isEmpty(token)) { // header没有tokentoken = request.getParameter("Authorization");}if (ObjectUtils.isEmpty(token)) {throw new CustomerAuthenticationException("token为空");}// redis进行校验if (!redisUtil.hasKey("token_" + token)) {throw new CustomerAuthenticationException("token已过期");}// 校验tokenEmpLogin empLogin;try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token);String loginUserString = claims.get(JwtClaimsConstant.EMP_LOGIN).toString();// 把json字符串转为对象empLogin = JSON.parseObject(loginUserString, EmpLogin.class);log.info("当前员工id:{}", empLogin.getEmp().getId());BaseContext.setCurrentId(empLogin.getEmp().getId());} catch (Exception ex) {throw new CustomerAuthenticationException("token校验失败");}BaseContext.setCurrentId(empLogin.getEmp().getId());// 把校验后的用户信息再次放入到SpringSecurity的上下文中UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(empLogin, null,empLogin.getAuthorities()); // 已认证的 Authentication 对象,包含用户的权限信息SecurityContextHolder.getContext().setAuthentication(authentication);System.out.println(empLogin.getAuthorities());}
}
说明:
- 过滤器在每次请求时执行
JWT
校验。 - 通过
Redis
检查Token
的有效性,解析后将用户信息存入SecurityContextHolder
。
六、自定义处理器
帮助我们在认证失败或者授权失败
的情况下也能和我们接口一样返回相同结构的json
,这样可以让前端能对响应进行统一的处理。
1.自定义验证异常类
创建 CustomerAuthenticationException.java
:用于在认证失败的情况下,抛出自定义的认证异常
/*** @description 自定义认证验证异常*/
public class CustomerAuthenticationException extends AuthenticationException {public CustomerAuthenticationException(String msg) {super(msg);}
}
说明:
通过继承 AuthenticationException
,可以将自定义异常与 Spring Security
的认证机制结合,支持在认证失败时抛出特定的错误消息。
2.编写认证用户无权限访问处理器
创建CustomerAccessDeniedHandler.java
:处理已认证用户尝试访问无权限资源的情况,返回统一格式的 JSON 响应
/*** @description 认证用户无权限访问的处理器*/
@Component
@Slf4j
public class CustomerAccessDeniedHandler implements AccessDeniedHandler{@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {log.error("权限不足,URI:{},异常:{}", request.getRequestURI(), accessDeniedException.getMessage());// 发生这个行为,做响应处理,给一个响应的结果response.setContentType("application/json;charset=utf-8");// 构建输出流对象ServletOutputStream outputStream = response.getOutputStream();// 调用fastjson工具,进行Result对象序列化String error = JSON.toJSONString(Result.error("权限不足,请联系管理员"));outputStream.write(error.getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
说明:
- 使用
AccessDeniedHandler
接口实现自定义逻辑。 - 捕获访问拒绝异常并记录日志,同时通过输出流返回统一的 JSON 响应结构,方便前端统一处理。
3.编写匿名用户访问受限资源的处理器
创建AnonymousAuthenticationHandler.java
:处理未认证用户(匿名用户)访问受限资源的情况,返回特定的错误信息。
/*** @description 客户端进行认证数据的提交时出现异常,或者是匿名用户访问受限资源的处理器*/
@Component
public class AnonymousAuthenticationHandler implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {// 发生这个行为,做响应处理,给一个响应的结果response.setContentType("application/json;charset=utf-8");// 构建输出流对象ServletOutputStream outputStream = response.getOutputStream();// 调用fastjson工具,进行Result对象序列化String error = "";if (authException instanceof BadCredentialsException){// 用户名或密码错误 401error = JSON.toJSONString(Result.error(authException.getMessage()));} else if (authException instanceof InternalAuthenticationServiceException) {error = JSON.toJSONString(Result.error("用户名为空"));} else{error = JSON.toJSONString(Result.error("匿名用户无权限访问"));}outputStream.write(error.getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
说明:
- 通过实现
AuthenticationEntryPoint
接口处理未认证用户的访问异常。 - 根据不同类型的
AuthenticationException
返回不同的错误信息,增强了响应的针对性。
4.编写自定义认证失败的处理器
创建LoginFailureHandler.java
:处理用户登录失败时的异常,返回详细的失败原因。
/*** @description 用户校验认证失败的处理器*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {// 发生这个行为,做响应处理,给一个响应的结果response.setContentType("application/json;charset=utf-8");// 构建输出流对象ServletOutputStream outputStream = response.getOutputStream();String message;if (exception instanceof AccountExpiredException) {message = "用户过期,登录失败";} else if (exception instanceof BadCredentialsException) {message = "用户名或密码错误,请重新输入!";} else if (exception instanceof CredentialsExpiredException) {message = "密码过期,请重新输入!";} else if (exception instanceof DisabledException) {message = "账户被禁用,登录失败!";} else if (exception instanceof LockedException) {message = "账户被锁,登录失败!";} else if (exception instanceof InternalAuthenticationServiceException) {message = "账户不存在,登录失败!";} else if (exception instanceof CustomerAuthenticationException) {message = exception.getMessage();} else {message = "登录失败!";}// 调用fastjson工具,进行Result对象序列化String error = JSON.toJSONString(Result.error(message));outputStream.write(error.getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
说明:
- 覆写
onAuthenticationFailure
方法,根据不同的认证异常类型返回详细的失败原因。 - 提供灵活的错误提示信息,便于用户快速定位登录失败的具体原因。
5.编写全局异常处理器
创建GlobalExceptionHandler.java
:捕获并统一处理项目中的异常,包括业务异常和 SQL 异常。
/*** 全局异常处理器,处理项目中抛出的业务异常*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {/*** 捕获所有异常*/@ExceptionHandler(Exception.class)public Result ex(Exception ex){// 如果是访问拒绝异常,不处理,让SecurityConfig中配置的处理器处理if(ex instanceof AccessDeniedException) {throw (AccessDeniedException)ex;}log.error("全局异常信息:{}", ex.getMessage());return Result.error(StringUtils.hasLength(ex.getMessage()) ? ex.getMessage() : "操作失败");}/*** 捕获业务异常*/@ExceptionHandlerpublic Result exceptionHandler(BaseException ex){log.error("业务异常信息:{}", ex.getResultEnum().message());return Result.error(ex.getResultEnum());}/*** 处理SQL异常*/@ExceptionHandlerpublic Result exceptionHandler(SQLIntegrityConstraintViolationException ex){// 错误信息:Duplicate entry 'zhaosi' for key 'employee.idx_username' -- > 用户ID重复String message = ex.getMessage();if(message.contains("Duplicate entry")){String[] split = message.split(" ");String username = split[2];String msg = username + ResultEnum.USER_NAME_HAS_EXISTED.message();return Result.error(msg);}else {return Result.error(ResultEnum.UNKNOWN_ERROR);}}
}
在全局异常处理器中为何要这样写?
在上一篇文章中,进行详细的讲解 Spring Security 6.3 权限异常处理实战解析
七、登录接口实现
1.实体类准备
创建EmpLoginVO.java
/*** 用户登录响应对象*/
@Data
@Builder
@Tag(name = "EmpLoginVO", description = "员工登录响应")
public class EmpLoginVO implements Serializable {@Serialprivate static final long serialVersionUID = 4393557997355879737L;@Schema(description = "用户ID")private Long id;@Schema(description = "用户名")private String username;@Schema(description = "姓名")private String name;@Schema(description = "令牌")private String token;
}
创建EmpLoginDTO.java
@Data
public class EmpLoginDTO implements Serializable {@Serialprivate static final long serialVersionUID = 8347822700891152077L;@NotBlank(message = "账号不能为空")@Pattern(regexp = "^\\w{5,20}$", message = "用户名的长度必须为5~16位")private String username; // 账号@NotBlank(message = "密码不能为空")@Pattern(regexp = "^\\w{5,16}$", message = "密码的长度必须为5~16位")private String password; // 密码
}
2.ThreadLocal工具类准备
创建BaseContext.java
public class BaseContext {public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id) {threadLocal.set(id);}public static Long getCurrentId() {return threadLocal.get();}public static void removeCurrentId() {threadLocal.remove();}}
3.Controller层
创建EmpController.java
:
@CrossOrigin // 允许跨域
@RestController
@RequestMapping("admin/emp")
@Tag(name = "员工管理接口")
@Slf4j
@RequiredArgsConstructor
public class EmpController {private final EmpService empService;private final RedisUtil redisUtil;/*** 员工登录* @param empLoginDTO 员工登录信息* @return 统一返回结果*/@PostMapping("/login")@Operation(summary = "员工登录")public Result<EmpLoginVO> login(@Validated @RequestBody EmpLoginDTO empLoginDTO) {log.info("员工:{},登录成功", empLoginDTO.getUsername());EmpLoginVO empLoginVO = empService.empLogin(empLoginDTO);return Result.success(empLoginVO);}/*** 员工退出登录* @return 统一返回结果*/@PostMapping("/logout")@Operation(summary = "员工退出登录")public Result logout(HttpServletRequest request, HttpServletResponse response) {log.info("员工ID:{},退出登录", BaseContext.getCurrentId());String token = request.getHeader("Authorization");if (ObjectUtils.isEmpty(token)) { // header没有tokentoken = request.getParameter("Authorization");}Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null) {// 清除上下文new SecurityContextLogoutHandler().logout(request, response, authentication);// 清理redisredisUtil.del("token_" + token);// 清理ThreadLocalBaseContext.removeCurrentId();}return Result.success();}
}
4.Service层
创建EmpService.java
接口:
public interface EmpService extends IService<Emp> {/*** 管理员登录* @param empLoginDTO 管理员登录表单* @return 员工登录VO*/EmpLoginVO empLogin(EmpLoginDTO empLoginDTO);}
创建EmpServiceImpl.java
实现类:
@Service
@Slf4j
@RequiredArgsConstructor
public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {private final EmpMapper empMapper;private final AuthenticationManager authenticationManager;private final JwtProperties jwtProperties;private final RedisUtil redisUtil;/*** 管理员登录*/public EmpLoginVO empLogin(EmpLoginDTO empLoginDTO) {String username = empLoginDTO.getUsername();String password = empLoginDTO.getPassword();// 1. 封装用户登录表单,创建未认证Authentication对象UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);// 2. 进行校验Authentication authenticate = authenticationManager.authenticate(authentication);// 3. 获取用户信息if (Objects.isNull(authenticate)){throw new RuntimeException("用户名或密码错误");}EmpLogin empLogin = (EmpLogin) authenticate.getPrincipal();Emp emp = empLogin.getEmp();if (emp.getStatus() == 1){throw new RuntimeException("账号被禁用");}log.info("员工 {} 登录成功", empLogin.getEmp().getName());// 登录成功,生成jwt令牌Map<String, Object> claims = new HashMap<>();// 使用fastjson的方法,把对象转换成json字符串String loginEmpString = JSON.toJSONString(empLogin);claims.put(JwtClaimsConstant.EMP_LOGIN, loginEmpString);String token = JwtUtil.createJWT(jwtProperties.getSecretKey(),jwtProperties.getTtl(),claims);// 存储redis白名单String tokenKey = "token_" + token;redisUtil.set(tokenKey, token, jwtProperties.getTtl()/1000);BaseContext.setCurrentId(emp.getId());//3、返回实体对象return EmpLoginVO.builder().id(emp.getId()).token(token).username(emp.getUsername()).name(emp.getName()).build();}
}
5.Mapper层
创建EmpMapper.java
接口:
@Mapper
public interface EmpMapper extends BaseMapper<Emp> {}
八、权限控制使用
1. 注解方式
在需要权限控制的接口上添加注解
// 在启动类或者SecurityConfig配置类上添加
@EnableMethodSecurity// 需要 "ems:employee:list" 权限才能访问
@PreAuthorize("hasAuthority('ems:employee:list')")
@GetMapping("/page")
public Result<PageResult> getList(EmpPageDTO empPageDTO) {return Result.success(empService.pageQuery(empPageDTO));
}// 需要多个权限中的任意一个
@PreAuthorize("hasAnyAuthority('ems:employee:add','ems:employee:edit')")
@PostMapping
public Result add(@RequestBody EmpAddDTO empAddDTO) {empService.add(empAddDTO);return Result.success();
}
2. 配置方式
在 SecurityConfig
中配置:
.authorizeRequests().antMatchers("/admin/emp/login").anonymous() // 允许匿名访问.antMatchers("/admin/emp/info").authenticated() // 需要认证.antMatchers("/admin/emp/**").hasRole("ADMIN") // 需要ADMIN角色
总结
本教程介绍了 Spring Security 框架的基础搭建过程,包括认证、授权、异常处理等核心功能的实现。通过这些基础配置,我们已经构建了一个安全、可靠的权限管理框架。在下篇教程中,我们将继续完善角色管理和动态权限控制,敬请期待!