【AOP系列】後端登錄這樣寫才香

【AOP系列】後端登錄這樣寫才香

在日常開發過程中,基本很多接口都會通過token的方式來檢測當前用戶的登錄狀態。

只有登錄過的用戶才能正常訪問接口。

一方面可以防止接口的惡意調用,

另一方面可以省去前端每次請求接口時 可以省略userId字段。

後端直接從userId即可。

那麼一個好的登錄到底應該怎麼寫呢?

在這裏插入圖片描述

依賴

<!--java-jwt    TokenUtil類依賴-->
<dependency>    
    <groupId>com.auth0</groupId>    
    <artifactId>java-jwt</artifactId>    
    <version>3.4.0</version>
</dependency>
<!--SpringBoot-aop   LoginAspect類依賴-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

工具類

TokenUtil.java

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;

/**
 * @ IDE    :IntelliJ IDEA.
 * @ Author :大火yzs
 * @ Date   :2019/9/27  16:44
 * @ Desc   :簡單的token工具類
 *
 依賴:com.auth0.java.jwt
 <dependency>
 <groupId>com.auth0</groupId>
 <artifactId>java-jwt</artifactId>
 <version>3.4.0</version>
 </dependency>
 */
public class TokenUtil {
    private static final String SECRET = "SECRET";			//加密時用到的key
    private static final Logger logger = LoggerFactory.getLogger(TokenUtil.class);
    /**
     * 時間常量 單位秒
     */
    public static final int ONE_DAY_EXPIRE = 86400;        //1天
    public static final int ONE_WEEK_EXPIRE = 604800;      //7天
    public static final int ONE_MONTH_EXPIRE = 2592000;    //30天
    public static final int THREE_MONTH_EXPIRE = 7776000;  //90天
    /**
     * 根據一個字符串生成token 有效期默認7天
     * @param data         要生成token的附帶信息
     * @return String      返回token
     */
    public static String createToken(Object data) {
        return createToken(data,ONE_WEEK_EXPIRE);
    }
    /**
     * 根據一個字符串生成token
     * @param data         要生成token的附帶信息
     * @param expire       有效期時間 單位秒
     * @return String      返回token
     */
    public static String createToken(Object data, int expire) {

            Date expireDate = new Date(System.currentTimeMillis() + expire*1000);
            JWTCreator.Builder builder = JWT.create()
                    .withExpiresAt(expireDate)
                    .withClaim("data",data.toString())
                    .withClaim("alg", "HS256")
                    .withClaim("typ", "JWT")
                    .withClaim("isExpired","false");
            return builder.sign(Algorithm.HMAC256(SECRET)); 
        	//使用上面的加密算法進行簽名,返回String,就是token
    }
    /**
     * 根據token解碼出原字符串
     * @param token        要解碼的token
     * @return String      返回原字符串信息
     */
    public static String verifyToken(String token) {
        try{
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getClaims().get("data").asString();
        }catch (TokenExpiredException e){
            e.printStackTrace();
            throw new RuntimeException("token失效,請重新授權");
        }catch (JWTDecodeException e){
            e.printStackTrace();
            throw new RuntimeException("token錯誤,請重新授權");
        }
    }

    /**
     * 根據token解碼出原信息,並轉換成Long類型
     * @param token         要解碼的token
     * @return Long         返回Long類型的原信息
     */
    public static Long getId(String token) {
        return Long.valueOf(verifyToken(token));
    }

    /**
     * 校驗token是否失效
     * @param token         要校驗的token
     * @return boolean      返回是否失效 true失效,false有效
     */
    public static boolean isTokenExpired(String token) {
        try{
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return false;
        }catch (Exception e){
            logger.error(Console.where() + e.getMessage());
           return true;
        }
    }

}

登陸時主要通過TokenUtil來根據userId生成token

調用接口時需要校驗token,並且從token中獲取userId

爲了方便接口的管理,我們通過兩個註解來管理接口的登錄檢測

登錄註解開發

@CheckLogin

可以用在類上,也可以用在方法上,

import java.lang.annotation.*;

/**
 * @ IDE    :IntelliJ IDEA.
 * @ Author :大火yzs
 * @ Date   :2019/11/6  9:20
 * @ Desc   : APP接口登錄校驗註解
 * @see IgnoreLogin
 * @see LoginAspect
 * 可用在方法上或者類上
 * 如果用方法上,則該方法必須登錄才能訪問。
 * 如果使用類上,則該類中的所有方法都必須登錄才能訪問。
 * 如果有Ignore註解。則此註解不生效。
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckLogin {

}

/**
 * 案例:
 * @RestController
 * class UserController{
 *     @CheckLogin
 *     @GetMapping("/get/user/sex")
 *     public String getUserSex(Long id){
 *         return userService.findById(id).getSex();
 *     }
 *     @GetMapping("/get/user/age")
 *     public String getUserAge(Long id){
 *         return userService.findById(id).getAge();;
 *     }
 * }
 * 請求/get/user/sex 需要登錄權限
 * 請求/get/user/age 不需要登錄權限
 * @CheckLogin
 * @RestController
 * class UserController{
 * }
 * 該類所有接口都需要登錄權限
 * @CheckLogin
 * @RestController
 * class UserController{
 *     @GetMapping("/get/user/sex")
 *     public String getUserSex(Long id){
 *         return userService.findById(id).getSex();
 *     }
 *     @IgnoreLogin
 *     @GetMapping("/get/user/age")
 *     public String getUserAge(Long id){
 *         return userService.findById(id).getAge();;
 *     }
 * }
 * 請求/get/user/sex 需要登錄權限
 * 請求/get/user/age 不需要登錄權限
 *
 * */

@IgnoreLogin

主要用在方法上

import java.lang.annotation.*;

/**
 * @ IDE    :IntelliJ IDEA.
 * @ Author :大火yzs
 * @ Date   :2019/12/18  15:56
 * @ Desc   :忽略登錄註解
 * @see CheckLogin
 * @see LoginAspect
 * 當@APPLogin註解在類上的時候,可以使用該註解取消需要校驗的接口
 * 只能用在方法上,優先級高於AppLogin註解。
 * 一般在類上使用了AppLogin註解時  才使用該註解來取消不用校驗登錄的方法。
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreLogin {
    
}

註解的實現

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * @ IDE    :IntelliJ IDEA.
 * @ Author :大火yzs
 * @ Date   :2019/11/6  9:30
 * @ Desc   : 攔截App請求接口校驗是否登錄
 * @see CheckLogin
 * @see IgnoreLogin
 */
@Slf4j
@Aspect
@Component
public class LoginAspect {
    /**
     * 獲取請求中的token頭,如果沒有提示非法token
     */
    @Around("execution(* com.【包名】.controller.*.*(..))")//攔截controller包下所有方法
    public Object methodAspect(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        CheckLogin onClass = joinPoint.getTarget().getClass().getAnnotation(CheckLogin.class);     //獲取類上是否有AppLogin註解
        CheckLogin onMethod = method.getAnnotation(CheckLogin.class);                              //獲取方法上是否有AppLogin註解
        IgnoreLogin Ignore = method.getAnnotation(IgnoreLogin.class);                          //獲取方法上是否有IgnoreLogin註解
        Boolean checkLogin = Ignore == null && (onClass != null || onMethod != null);          // 條件 沒有Ignore註解。 類上或者方法上有註解
        if (checkLogin){
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String token = request.getHeader("token");
            if (token==null||token.isEmpty()){
                return Result.getCodeResult(401,"缺少token");
            }
            Long userId = TokenUtil.getId(token);
            request.setAttribute("userId",userId);//將用戶id存在request中
//            log.debug("CheckToken|userId:{}|token:{}",userId,token);
        }
        return joinPoint.proceed();
    }

    public static Long getCurrUserId(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        return (Long) request.getAttribute("userId");
    }

}

註解的使用

在這裏插入圖片描述

token的校驗

在這裏插入圖片描述

Service層獲取UserId

在這裏插入圖片描述

這裏準備了兩個接口,分別登錄兩個賬號後,用兩個不同的token請求這兩個接口

這裏使用 IntelliJ IDEA 插件 HTTP Client請求接口

HTTP Client原文教程

在這裏插入圖片描述

最後結果,不僅使用token對接口進行了檢測,而且通過HttpRequest對象存儲了用戶id,保證每個請求都能方便的拿到用戶id,

在這裏插入圖片描述

全局拿用戶信息

OK成功舒適的拿到用戶id後,我們可以在任何時間,任何Service中直接獲取用戶信息。
在UserService中寫一個getCurrUser方法來獲取當前對象;
在這裏插入圖片描述

好了,全局獲取User信息。而不用考錄前端是否傳值,只要前端請求接口時有token,那麼這個請求的任何時間都能可以通過有注入**UserService並調用get.CurrUser()**即可拿到用戶對象

博文🌱

【AOP系列】愛之初體驗(一)

【AOP系列】靜態代理和動態代理(二)

【AOP系列】JDK動態代理源碼分析(三)

【AOP系列】自己動手實現一個JDK的Proxy類(四)

【AOP系列】一個切面類搞定請求日誌

【AOP系列】後端登錄這樣寫才香

【AOP系列】CGlib代碼跟讀

問題清單⭐

什麼是AOP?

什麼是動態代理?

動態代理有哪些實現方式?

爲啥要使用AOP?

JDK動態代理的原理是什麼?

JDK動態代理爲只能代理實現了接口的方法?

AOP的兩種底層實現性能分析和比較?

應用場景🎉

AOP結合 stopwatch 寫一個 請求時間的統計工具

AOP寫一個登錄攔截的功能

AOP寫一個日誌請求處理器

AOP簡單模擬@Transactional的事務處理

此博文中的源代碼都在這裏 aopdemo

這裏還有其他有意思的小demo GitHub

{
	"author": "大火yzs",
	"title": "【AOP系列】後端登錄這樣寫才香",
	"tag": "AOP,登錄",
	"createTime": "2020-03-06  1:37"
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章