banner
NEWS LETTER

自定义注解+AOP+JWT实现Token拦截验证

Scroll down

定义@Authorized注解

首先定义一个Authorized注解,用于后面对需要鉴权的方法添加该注解,Authorzied注解有Role类型的roles数组变量,表明能够调用被Authorized注解的方法的角色有哪些,其默认值如下

1
2
3
4
5
6
7
8
9
10
11
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Authorized{
Role[] roles() default{
Role.ADMIN,
Role.SALE_STAFF,
Role.INVENTORY_MANAGER,
Role.FINANCIAL_STAFF,
Role.HR, Role.GM
}
}
  • 元注解:注解的注解,如@Retention@Target

@Retention注解

  • 生命周期:用来定义该注解在哪个级别可用,可选的有源代码中(SOURCE)类文件中(CLASS)或者运行时(RUNTIME)
  • @Retention注解源码可以看到拥有一个RetentionPolicy类型的变量value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package java.lang.annotation;

/**
* Indicates how long annotations with the annotated interface are to
* be retained. If no Retention annotation is present on
* an annotation interface declaration, the retention policy defaults to
* {@code RetentionPolicy.CLASS}.
*
* <p>A Retention meta-annotation has effect only if the
* meta-annotated interface is used directly for annotation. It has no
* effect if the meta-annotated interface is used as a member interface in
* another annotation interface.
*
* @author Joshua Bloch
* @since 1.5
* @jls 9.6.4.2 @Retention
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
  • RetentionPolicy源码如下,可以看到它是个枚举类型,有**源代码中(SOURCE)类文件中(CLASS)或者运行时(RUNTIME)**,对应的生命周期长度为RUNTIME>CLASS>SOURCE
    1. 一般如果需要在运行时去动态获取注解信息,用生命周期最长的 RUNTIME 标注
    2. 如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解
    3. 如果只是做一些检查性的操作,就用SOURCE标注,比如源码中的 @Override、@SuppressWarnings、@Native、@Generated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package java.lang.annotation;

/**
* Annotation retention policy. The constants of this enumerated class
* describe the various policies for retaining annotations. They are used
* in conjunction with the {@link Retention} meta-annotation interface to
* specify how long annotations are to be retained.
*
* @author Joshua Bloch
* @since 1.5
*/
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,

/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,

/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}

@Target

  • 注解目标:用来定义你的注解将应用于什么地方(例如是一个方法或者一个域)
  • @Target注解源码可以看到拥有一个ElementType类型的数组变量value
1
2
3
4
5
6
7
8
9
10
11
12
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation interface
* can be applied to.
* @return an array of the kinds of elements an annotation interface
* can be applied to
*/
ElementType[] value();
}
  • ElementType源码如下,可以看到它是个枚举类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public enum ElementType {
/** Class, interface (including annotation interface), enum, or record
* declaration */
TYPE,

/** Field declaration (includes enum constants) */
FIELD,

/** Method declaration */
METHOD,

/** Formal parameter declaration */
PARAMETER,

/** Constructor declaration */
CONSTRUCTOR,

/** Local variable declaration */
LOCAL_VARIABLE,

/** Annotation interface declaration (Formerly known as an annotation type.) */
ANNOTATION_TYPE,

/** Package declaration */
PACKAGE,

/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,

/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE,

/**
* Module declaration.
*
* @since 9
*/
MODULE,

/**
* Record component
*
* @jls 8.10.3 Record Members
* @jls 9.7.4 Where Annotations May Appear
*
* @since 16
*/
RECORD_COMPONENT;
}

实现JwtConfig类

JwtCOnfig类用来签发Jwt和解析Jwt,它的源码如下:

  • UserService.login登录和数据库校验成功后调用createJwt(),并把这个JWT返回给前端,以后前端每次请求都需要带上这个JWT
  • 切面获取前端发来的http请求,调用parseJwt解析JWT,验证是否拥有对应方法的权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
@Data
public class JwtConfig {

private static String secret;

private static long expire;

private Key key;

@Value("${jwt.secret}") //value在application.yml中配置
public void setJwtSecret(String jwtSecret) { //通过set让static方法读取配置文件中的值
JwtConfig.secret = jwtSecret;
}

@Value("${jwt.expire}") //value在application.yml中配置
public void setExpire(long expire) { //通过set让static方法读取配置文件中的值
JwtConfig.expire = expire;
}

//签发Jwt
public String createJWT(UserPO userPO) {
Date date = new Date();
Date expireDate = new Date(date.getTime() + expire);

String jwt = JWT.create()
//可以将基本信息放到claims中
.withClaim("name", userPO.getName())
//name
.withClaim("role", userPO.getRole().name())
//超时设置,设置过期的日期
.withExpiresAt(expireDate)
//签发时间
.withIssuedAt(new Date())
//SECRET加密
.sign(Algorithm.HMAC256(secret));
return jwt;
}

//解析Jwt
public Map<String, Claim> parseJwt(String token) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaims();
} catch (TokenExpiredException e) {
throw new MyServiceException("A0230", "用户登陆已过期");
} catch (JWTVerificationException | IllegalArgumentException e) { //解析错误 或者 token写错
throw new MyServiceException("A0200", "登陆失败");
}


}

实现增强类

新建一个AuthAspect切面,用于增强需要鉴权的方法,其定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
@Configuration
@Order(1)
public class AuthAspect {
private final UserService userService;
private final JwtConfig jwtConfig;

@Autowired
public AuthAspect(UserService userService, JwtConfig jwtConfig){
this.userService = userService;
this.jwtConfig = jwtConfig;
}

@Before(value = "execution(public * com.nju.edu.erp.web.controller.*.*(..)) && @annotation(authorized)")
public void authCheck(JoinPoint joinPoint, Authorized authorized);
}
  • AuthAspect类有三个注解

    • @Aspect:把当前类标识为一个切面供容器读取
    • @Configuration:表明它是一个配置类
    • @Order(1):用于有多个增强类对同一个方法进行增强的时候表明其优先级,越小优先级越高
  • AuthAspect类除去构造方法外只有一个authChech()方法,它有一个@Before注解

    • @Before:标识一个前置增强方法,相当于BeforeAdvice的功能
    • 这个注解的值是检查所有com.nju.edu.erp.web.controller包下所有类public并且被Authorized注解所标注的方法
  • authCheck()方法代码如下,主要操作包括:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Before(value = "execution(public * com.nju.edu.erp.web.controller.*.*(..)) && @annotation(authorized)")
    public void authCheck(JoinPoint joinPoint, Authorized authorized) {
    try {
    //1.获取当前http请求
    HttpServletRequest httpServletRequest = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    //2.获得Authorized标头,没有则抛出异常
    String token = Optional.ofNullable(httpServletRequest.getHeader("Authorization")).
    orElseThrow(() -> new MyServiceException("A0002", "用户未获得第三方登录授权"));
    //3.auth方法调用jwtConfig.parseJwt解析出UserVO信息
    UserVO user = userService.auth(token);

    //4.判断切面方法是否包含当前token对应的角色
    if (!Arrays.stream(authorized.roles()).collect(Collectors.toList()).contains(user.getRole())) {
    //5.如果不在就抛出访问未授权的异常
    throw new MyServiceException("A0003", "访问未授权");
    }
    }catch (MyServiceException e) {
    throw new MyServiceException("A0004", "认证失败");
    }
    }

需要鉴权的接口添加@Authorized注解

controller层对前端来的路由添加Authorized注解进行鉴权,如:

1
2
3
4
5
6
@GetMapping("/create")
@Authorized(roles = {Role.ADMIN, Role.GM, Role.INVENTORY_MANAGER})
public Response createCategory(@RequestParam(value = "parentId") int parentId,
@RequestParam(value = "name") String name) {
return Response.buildSuccess(categoryService.createCategory(parentId, name));
}

添加登录方法

UserService.login登录功能,后端从数据库根据用户名和邮箱进行验证,验证成功调用jwtConfig.createJWT生成JWT,并把这个JWT返回给前端,以后前端每次请求都需要带上这个JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class UserServiceImpl implements UserService {

private final UserDao userDao;

private final JwtConfig jwtConfig;

@Autowired
public UserServiceImpl(UserDao userDao, JwtConfig jwtConfig);

@Override
public Map<String, String> login(UserVO userVO) {
UserPO userPO = userDao.findByNameAndPassword(userVO.getName(), userVO.getPassword());
if (null == userPO) {
throw new MyServiceException("A0000", "用户名或密码错误");
}
Map<String, String> authToken = new HashMap<>();
String token = jwtConfig.createJWT(userPO);
authToken.put("token", token);
return authToken;
}

@Override
public int updatePassword(UserVO userVO,String newPassword);

@Override
public UserVO auth(String token);
}

自定义注解+AOP+JWT验证流程总结

  1. 调用UserService.login登录功能,后端从数据库根据用户名和邮箱进行验证,验证成功调用jwtConfig.createJWT生成JWT,并把这个JWT返回给前端,以后前端每次请求都需要带上这个JWT
  2. controller层对前端来的路由添加@Authorized注解进行鉴权
  3. AuthAspect类对Authorized注解进行增强,@Order注解用于有多个增强类对同一个方法进行增强的时候表明其优先级,越小优先级越高
  4. 添加Before方法,用于拦截controller包中所有public并且有@Authorized注解的方法
  5. 拦截http请求并获得Authorized标头,调用jwtConfig.parseJwt解析出UserVO信息,判断该UserVO的角色是否在Authorized注解的roles集合中,如果不在就抛出访问未授权的异常

JoinPoint常用方法

  • Object[] getArgs:获取目标方法的参数数组
  • Signature getSignature:获取目标方法的签名
  • Object getTarget:获取被织入增强处理的目标对象
  • Object getThis:获取AOP框架为目标对象生成的代理对象

参考文献

自定义注解+AOP+JWT实现Token拦截验证

@Retention的作用和@Target注解的说明以及使用方法