Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.


Recently, in my work, there was a scene where JWT was used to issue and verify tokens. I just used JWT and did not use SpringSecurity and Shiro for integration. I didn’t expect that a simple function was changed four or five times a week, which can be said to have experienced constant optimization. Let’s take a look at the process of modification.

Version 1.0

The original version did this by passing the HttpServletRequest parameter to the Controller layer interface:

@RestController
@RequestMapping("api")
public class TestController {
    @Autowired
    MyService myService;

    @GetMapping("test")
    public String test(HttpServletRequest request){
        String result = myService.test(request);
        returnresult; }}Copy the code

For simplicity, we omit the interface layer of the Service and call the method of the Service layer directly:

@Service
public class MyService {
    public String test(HttpServletRequest request){
        String token = request.getHeader("token");
        System.out.println(token);
        // Verify token and handle business logic
        return "success"; }}Copy the code

The token in the header is retrieved via HttpServletRequest in the method and validated before the business logic is processed. At first glance, there is no problem, but when you write too much, you feel that it is very troublesome to write such an unnecessary parameter for each interface. Can you handle it?

Version 2.0

At this time, IT reminds me of what I said when I learned Spring before. It would be good to deal with a lot of repetitive work unrelated to business in the section. However, in retrospect, not every method in a Service needs to validate the token. Instead, write an annotation and use a section to handle the annotated method.

Defining an annotation, called NeedToken, also requires no attributes and is straightforward:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedToken {
}
Copy the code

The cutting is simple: Get the current HttpServletRequest request from the RequestContextHolder provided by SpringMvc, and then retrieve the token in the header.

The first question arises: How do I pass the token to the method called in the Service after obtaining it in the slice?

Recall that you can get method parameters in the section and then change the values of the parameters on the fly. Modify the parameters of the Service method to remove the annoying HttpServletRequest and add a Strings parameter to receive the token.

@Service
public class MyService {
    @NeedToken
    public String test(String token){
        System.out.println(token);
        // Verify token and handle business logic
        return "success"; }}Copy the code

The section is implemented as follows:

@Aspect
@Component
public class TokenAspect {
    @Pointcut("@annotation(com.cn.hydra.aspectdemo.annotation.NeedToken)")
    public void tokenPointCut(a) {}@Around("tokenPointCut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");

        Object[] args = point.getArgs();
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] paramName = methodSignature.getParameterNames();
        List<String> paramNameList = Arrays.asList(paramName);
        if (paramNameList.contains("token")) {int pos = paramNameList.indexOf("token");
            args[pos]=token;
        }

        Object object = point.proceed(args);
        returnobject; }}Copy the code

The following things are done in the section:

  • Define the pointcut@NeedTokenAnnotation methods are woven into logic
  • throughRequestContextHolderTo obtainHttpServletRequestTo obtain the token in the header
  • throughMethodSignatureObtain the parameter list of the method and change the token value in the parameter list
  • The original method is called with the new argument list, passing the token to the method

We don’t need to pass HttpServletRequest to the Controller method, but because we are calling the Service method and the method has a parameter token, we can pass null:

@RestController
@RequestMapping("api")
public class TestController {
    @Autowired
    MyService myService;
    
    @GetMapping("test")
    public String test(a){
        String result = myService.test(null);
        returnresult; }}Copy the code

Write here, although said can solve the problem, but to write a null parameter, let a person is very uncomfortable, as obsessive-compulsive must find a way to eliminate it.

Version 3.0

Is there a way to pass tokens to methods without passing parameters? We can get the object that the method belongs to through the section, and then we can inject the value of the property directly through reflection. Modify Service again to declare a global variable that reflects the use of injected tokens.

@Service
public class MyService{
    private String TOKEN;
    
    @NeedToken
    public String test(a) {
        System.out.println(TOKEN);
        // Verify token and handle business logic
        returnTOKEN; }}Copy the code

Modify section implementation method:

@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
    try {
        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");

        Field tokenField = point.getTarget().getClass().getDeclaredField("TOKEN");
        tokenField.setAccessible(true);
        tokenField.set(point.getTarget(),token);

        Object object = point.proceed();
        return object;
    } catch (Throwable e) {
        e.printStackTrace();
        throwe; }}Copy the code

Note that instead of modifying the parameters passed in by the method, we do this by getting the class’s Field and then injecting the actual value into the Field corresponding to the current object’s token.

I feel good about myself for a while, but when I write several classes, I realize that every Service class has to declare a global variable of type String.

Version 4.0

So the idea of being lazy and skimming is really a driving force for social progress. After a lot of thinking, write a parent class, declare the token in the parent class, and then each Service will inherit it as a subclass, so it won’t be forgotten, and the code will be much cleaner.

Define a parent class first. The reason for using a parent class instead of an interface is that variables declared in the interface are final by default and therefore cannot be changed.

public class BaseService {
    public String TOKEN = null;
}
Copy the code

BaseService (); BaseService ();

@Service
public class MyService extends BaseService {
    @NeedToken
    public String test(a) {
        System.out.println(TOKEN);
        // Verify token and handle business logic
        returnTOKEN; }}Copy the code

Call an interface test and throw an exception:

java.lang.NoSuchFieldException: TOKEN
  at java.lang.Class.getDeclaredField(Class.java:2070)
  at com.cn.hydra.aspectdemo.aspect.TokenAspect.doAround(TokenAspect.java:35)
  ...
Copy the code

< span style = “box-sizing: inherit! Important; color: RGB (74, 74, 74); font-size: 14px! Important;”

@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
    try {
        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");

        //Field tokenField = point.getTarget().getClass().getDeclaredField("TOKEN");Class<? > baseClazz = point.getTarget().getClass().getSuperclass(); Field tokenField = baseClazz.getDeclaredField("TOKEN");
        tokenField.setAccessible(true);
        tokenField.set(point.getTarget(),token);

        Object object = point.proceed();
        return object;
    } catch (Throwable e) {
        e.printStackTrace();
        throwe; }}Copy the code

Instead, get the parent class from the current object, then get the variables in the parent class, and inject the token value through reflection.

I tested the token acquisition several times without any problems. Spring’s beans are singletons by default, and the value of a global variable can be changed by any thread. Then there are situations where one thread might get another thread’s modified token. There are some methods that can be modified, but if I change the scope of the Bean to prototype or request for a token, it will not be worth the loss.

Add a method to the Service. The method has a parameter name indicating the user’s identity, and then compare it with the token:

@Service
public class MyService extends BaseService {
    @NeedToken
    public boolean checkToken(String name) {
        System.out.println(name+""+TOKEN  +""+ name.equals(TOKEN));
        returnname.equals(TOKEN); }}Copy the code

Use CyclicBarrier to test 200 concurrent requests. Note that postman is not used for this test, since postman runner will execute requests serially. So if you are not familiar with JMeter or other tools, you should use CyclicBarrier. (If you are not familiar with CyclicBarrier, check out this article on CyclicBarrier.)

The test class is implemented as follows:

public class HttpSendTest {
    public static void main(String[] args) {
        CyclicBarrier barrier=new CyclicBarrier(200);
        Thread[] threads=new Thread[100];
        for (int i = 0; i <100 ; i++) {
            threads[i]=new Thread(()->{
                try{ barrier.awit(); sendGet("http://127.0.0.1:8088/api/test2"."name=hydra"."hydra");
                    //barrier.await();
                } catch(Exception e) { e.printStackTrace(); }}); threads[i].start(); } Thread[] threads2=new Thread[100];
        for (int i = 0; i <100 ; i++) {
            threads2[i]=new Thread(()->{
                try {
                    sendGet("http://127.0.0.1:8088/api/test2"."name=trunks"."trunks"); / move on barrier. Await (); }catch(Exception e) { e.printStackTrace(); }}); threads2[i].start(); }}public static String sendGet(String url, String param, String token) {
        StringBuilder result = new StringBuilder();
        BufferedReader in = null;
        try {
            String urlNameString = url + "?" + param;
            URL realUrl = new URL(urlNameString);
            URLConnection connection = realUrl.openConnection();
            connection.setRequestProperty("accept"."* / *");
            connection.setRequestProperty("connection"."Keep-Alive");
            connection.setRequestProperty("token", token);
            connection.setRequestProperty("user-agent"."Mozilla / 4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
            connection.connect();
            in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
            String line;
            while((line = in.readLine()) ! =null) {
                result.append(line);
            }
            System.out.println(result);
        } catch (ConnectException e) {
            e.printStackTrace();
        } catch (SocketTimeoutException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(in ! =null) { in.close(); }}catch(Exception ex) { ex.printStackTrace(); }}returnresult.toString(); }}Copy the code

The name attribute passed to the parameter in the test is the same as the token carried in the request. Running the test case, it is found that some mismatches do occur in the case of high concurrency, indicating that the obtained token is not its own:

Version 5.0

After working so hard to change several versions of the code, there is such a big problem. How can this be done? The point is to ensure that each thread has its own unique copy of the token, which is exactly what ThreadLocal is.

Redefining the parent class to use ThreadLocal to store tokens:

spublic class BaseService2 {
    public static ThreadLocal<String> TOKEN= 
            ThreadLocal.withInitial(() -> null);
}
Copy the code

Modify the Service:

@Service
public class MyService2 extends BaseService2 {
    @NeedToken
    public boolean testToken(String name) {
        String token=TOKEN.get();
        boolean check = name.equals(token);
        System.out.println(name+""+token +""+check);
        returncheck; }}Copy the code

Modify section:

@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
    try {

        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token"); Class<? > baseClazz = point.getTarget().getClass().getSuperclass(); Field tokenField = baseClazz.getDeclaredField("TOKEN");
        ThreadLocal<String> local = (ThreadLocal<String>) tokenField.get(point.getTarget());
        local.set(token);

        tokenField.setAccessible(true);
        tokenField.set(point.getTarget(),local);

        Object object = point.proceed();
        return object;
    } catch (Throwable e) {
        e.printStackTrace();
        throwe; }}Copy the code

Get a ThreadLocal object by reflection, assign a value to ThreadLocal by set, and write it back to the object by reflection. Concurrent testing again showed no anomalies, even with 600 concurrent requests. Optimization process to the end here, of course, there may be what did not think of the place, welcome to give me a message to discuss.

The last

If you think it is helpful, you can like it and forward it. Thank you very much

Public number agriculture ginseng, add a friend, do a thumbs-up friend ah