【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請求接口
最後結果,不僅使用token對接口進行了檢測,而且通過HttpRequest對象存儲了用戶id,保證每個請求都能方便的拿到用戶id,
全局拿用戶信息
OK成功舒適的拿到用戶id後,我們可以在任何時間,任何Service中直接獲取用戶信息。
在UserService中寫一個getCurrUser方法來獲取當前對象;
好了,全局獲取User信息。而不用考錄前端是否傳值,只要前端請求接口時有token,那麼這個請求的任何時間都能可以通過有注入**UserService並調用get.CurrUser()**即可拿到用戶對象
博文🌱
問題清單⭐
什麼是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"
}