Spring Boot 权限控制三件套:JWT 登录校验 + 拦截器 + AOP 角色注解实战

文章目录

  • 接口校验,权限拦截
  • 统一异常处理和信息返回
    • [1. 创建统一信息返回类](#1. 创建统一信息返回类)
    • [2. 创建全局统一异常处理类](#2. 创建全局统一异常处理类)
    • [3. 创建一个枚举类型](#3. 创建一个枚举类型)
    • [4. 创建自定义的异常类](#4. 创建自定义的异常类)
  • 拦截器+JWT实现登录校验
    • [1. 添加依赖](#1. 添加依赖)
    • [2. JWT工具包](#2. JWT工具包)
    • [3. Threadlocal保存用户信息](#3. Threadlocal保存用户信息)
    • [4. 拦截器校验登录](#4. 拦截器校验登录)
    • [5. 注册拦截器](#5. 注册拦截器)
    • [6. 自定义注解+AOP角色校验](#6. 自定义注解+AOP角色校验)
    • [7. Controller层示例](#7. Controller层示例)

接口校验,权限拦截

通过自定义注解,基于面向切面编程来实现

  • 加依赖
xml 复制代码
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>

1. 自定义异常

java 复制代码
// com.yourpackage.exception.AccessDeniedException.java package com.yourpackage.exception; 
public class AccessDeniedException extends RuntimeException { 
	public AccessDeniedException(String message) { 
		super(message); 
	}
}
  • 继承RuntimeException 是为了让他必须是非受检异常,不需要再方法上显示throws

2. 自定义注解

java 复制代码
//
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface hasRole {
	String[] value(); //允许的用户类型数组
}
元注解 作用
@Target 指定注解可用的位置(如方法、类、字段等)
@Retention 指定注解保留策略(源码/编译器/运行时)
@Documented 是否包含在JavaDoc中
@Inherited 子类是否继承父类的注解

3. AOP面向切面类

java 复制代码
@Aspect  
@Component  
public class RoleCheckAspect {  
  
    @Around("@annotation(hasRole)")  
    public Object checkPermission(ProceedingJoinPoint joinPoint, HasRole hasRole) throws Throwable {  
        // 1. 从 session 获取当前用户  
        WhitelistSetting currentUser = SessionUtils.getCurrentUserInfo();  
        if (currentUser == null) {  
            throw new RuntimeException("用户未登录,请先登录");  
        }  
  
        // 2. 获取用户的角色ID(假设 WhitelistSetting 有 getRoleId() 方法)  
        String userRoleId = currentUser.getRoleId();  
        if (userRoleId == null || userRoleId.trim().isEmpty()) {  
            throw new RuntimeException("用户角色信息缺失");  
        }  
  
        // 3. 获取注解中允许的角色列表  
        String[] allowedRoles = hasRole.value();  
        if (allowedRoles == null || allowedRoles.length == 0) {  
            throw new RuntimeException("HasRole 注解必须指定至少一个角色");  
        }  
  
        // 4. 校验用户角色是否在允许列表中  
        boolean hasAccess = Arrays.asList(allowedRoles).contains(userRoleId);  
        if (!hasAccess) {  
            throw new RuntimeException("权限不足:需要角色 [" + String.join(", ", allowedRoles) + "],当前角色为 [" + userRoleId + "]");  
        }  
  
        // 5. 放行  
        return joinPoint.proceed();  
    }  
}

4. Controller层使用

java 复制代码
@RestController  
@RequestMapping("/api")  
public class DemoController {  
  
    @GetMapping("/admin/data")  
    @HasRole({"ADMIN", "SUPER_ADMIN"})  
    public String adminData() {  
        return "管理员专属数据";  
    }  
  
    @GetMapping("/user/profile")  
    @HasRole({"USER", "ADMIN"})  
    public String userProfile() {  
        return "用户或管理员可访问";  
    }  
}

统一异常处理和信息返回

1. 创建统一信息返回类

java 复制代码
public class Resp<T> {

    //服务端返回的错误码
    private int code = 200;
    //服务端返回的错误信息
    private String msg = "success";
    //我们服务端返回的数据
    private T data;

    private Resp(int code,String msg,T data){
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static <T> Resp success(T data){
        Resp resp = new Resp(200, "success", data);
        return resp;
    }

    public static <T> Resp success(String msg,T data){
        Resp resp = new Resp(200,msg, data);
        return resp;
    }

    public static <T> Resp error(AppExceptionCodeMsg appExceptionCodeMsg){
        Resp resp = new Resp(appExceptionCodeMsg.getCode(), appExceptionCodeMsg.getMsg(), null);
        return resp;
    }
    public static <T> Resp error(int code,String msg){
        Resp resp = new Resp(code,msg, null);
        return resp;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    public T getData() {
        return data;
    }

}

2. 创建全局统一异常处理类

java 复制代码
@ControllerAdvice
public class GlobalExceptionHandler {


    @ExceptionHandler(value = {Exception.class})
    @ResponseBody
    public <T> Resp<T> exceptionHandler(Exception e){
        //这里先判断拦截到的Exception是不是我们自定义的异常类型
        if(e instanceof AppException){
            AppException appException = (AppException)e;
            return Resp.error(appException.getCode(),appException.getMsg());
        }

        //如果拦截的异常不是我们自定义的异常(例如:数据库主键冲突)
        return Resp.error(500,"服务器端异常");
    }
}

3. 创建一个枚举类型

java 复制代码
//这个枚举类中定义的都是跟业务有关的异常
public enum AppExceptionCodeMsg {

    INVALID_CODE(10000,"验证码无效"),
    USERNAME_NOT_EXISTS(10001,"用户名不存在"),
    USER_CREDIT_NOT_ENOUTH(10002,"用户积分不足");
    ;

    private int code ;
    private String msg ;

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }


    AppExceptionCodeMsg(int code, String msg){
        this.code = code;
        this.msg = msg;
    }

}

4. 创建自定义的异常类

java 复制代码
public class AppException extends RuntimeException{

    private int code = 500;
    private String msg = "服务器异常";


    public AppException(AppExceptionCodeMsg appExceptionCodeMsg){
        super();
        this.code = appExceptionCodeMsg.getCode();
        this.msg = appExceptionCodeMsg.getMsg();

    }

    public AppException(int code,String msg){
        super();
        this.code = code;
        this.msg = msg;

    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

}

拦截器+JWT实现登录校验

1. 添加依赖

xml 复制代码
<dependencies>
    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>

    <!-- Spring AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2. JWT工具包

java 复制代码
package com.demo.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtils {

    private static final long EXPIRE = 2 * 60 * 60 * 1000;

    private static final SecretKey SECRET_KEY =
            Keys.hmacShaKeyFor("abcdefg1234567890abcdefg1234567890".getBytes());

    public static String generateToken(Long userId, String role) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", role);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(String.valueOf(userId))
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .signWith(SECRET_KEY)
                .compact();
    }

    public static Claims parseToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

3. Threadlocal保存用户信息

java 复制代码
public class UserContext {
    private static final ThreadLocal<Long> userIdHolder = new ThreadLocal<>();
    private static final ThreadLocal<String> roleHolder = new ThreadLocal<>();

    public static void setUserId(Long id) { userIdHolder.set(id); }
    public static Long getUserId() { return userIdHolder.get(); }
    public static void setRole(String role) { roleHolder.set(role); }
    public static String getRole() { return roleHolder.get(); }
    public static void clear() { userIdHolder.remove(); roleHolder.remove(); }
}

4. 拦截器校验登录

java 复制代码
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        if (uri.equals("/login")) return true; // 放行登录

        String token = request.getHeader("Authorization");
        if (token == null) returnJson(response, 401, "未登录"); 
        else {
            try {
                token = token.replace("Bearer ", "");
                var claims = JwtUtils.parseToken(token);
                UserContext.setUserId(Long.valueOf(claims.getSubject()));
                UserContext.setRole((String) claims.get("role"));
                return true;
            } catch (Exception e) {
                returnJson(response, 401, "Token 无效或过期");
                return false;
            }
        }
        return false;
    }

    private void returnJson(HttpServletResponse response, int code, String msg) throws Exception {
        response.setContentType("application/json;charset=UTF-8");
        ObjectMapper mapper = new ObjectMapper();
        response.getWriter().write(mapper.writeValueAsString(Result.fail(code, msg)));
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        UserContext.clear();
    }
}

5. 注册拦截器

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor).addPathPatterns("/**");
    }
}

6. 自定义注解+AOP角色校验

java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String[] value();
}
java 复制代码
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class RoleAspect {
    @Around("@annotation(RequireRole)")
    public Object checkRole(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        RequireRole annotation = signature.getMethod().getAnnotation(RequireRole.class);

        String userRole = UserContext.getRole();
        for (String role : annotation.value()) {
            if (role.equals(userRole)) return joinPoint.proceed();
        }
        return Result.fail(403, "权限不足");
    }
}

7. Controller层示例

java 复制代码
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
public class UserController {

    @PostMapping("/login")
    public Result<Map<String,Object>> login(@RequestParam String username, @RequestParam String password) {
        // 模拟验证
        Long userId = 1L;
        String role = switch (username) {
            case "student" -> "student";
            case "counselor" -> "counselor";
            case "teacher" -> "teacher";
            default -> "student";
        };
        String token = JwtUtils.createToken(userId, role);

        Map<String,Object> data = Map.of("token", token, "role", role);
        return Result.success(data);
    }

    @RequireRole({"student"})
    @GetMapping("/list")
    public Result<String> list() {
        return Result.success("学生可以访问列表");
    }

    @RequireRole({"counselor","teacher"})
    @PostMapping("/update")
    public Result<String> update() {
        return Result.success("辅导员/老师可以更新");
    }
}
相关推荐
光算科技5 小时前
商品颜色/尺码选项太多|谷歌爬虫不收录怎么办
java·javascript·爬虫
派大鑫wink5 小时前
分享一些在Spring Boot中进行参数配置的最佳实践
java·spring boot·后端
想学习java初学者5 小时前
SpringBoot整合MQTT多租户(优化版)
java·spring boot·后端
代码栈上的思考5 小时前
MyBatis XML的方式来实现
xml·java·mybatis
阿拉斯攀登5 小时前
Spring Boot 深度解析:核心原理与自动配置全解
java·spring boot
AM越.5 小时前
Java设计模式超详解--观察者设计模式
java·开发语言·设计模式
专注VB编程开发20年5 小时前
c#语法和java相差多少
java·开发语言·microsoft·c#
有一个好名字5 小时前
设计模式-单例模式
java·单例模式·设计模式
叶子丶苏5 小时前
第十七节_PySide6基本窗口控件深度补充_窗口绘图类(QPicture类) 下篇
python·pyqt