项目登录功能实现及权限校验

本文采用JWT+注解+Cache(缓存)完成需求这里有条件的也可以吧缓存换成redis,对于大量用户登录redis更快而且可以记录更多token,缓存限制比较大

一、拦截器和Cache处理

1、AuthInterceptor拦截器(实现对所有请求的拦截和处理)

/**
 * @Description: 请求拦截器
 * @Author: zhang
 * @Date: 2023/10/24 16:51
 **/
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {

    public static final String TOKEN_KEY = "token";

    /**
     * 需要校验的请求接口
     */
    public static final String[] SUPER_HANDLE_PREFIX = new String[] {
            "/admin",
            "/admin/stores",
            "/management"
    };

    private static AntPathMatcher matcher = new AntPathMatcher();

    @Autowired
    private JwtUserService jwtUserService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        if (!method.isAnnotationPresent(NotLoginToken.class)) {
            String token = getToken(request);

            String uri = request.getRequestURI();//获取请求的url
            //判断是否为需要校验电脑接口
            for (int i = 0; i < SUPER_HANDLE_PREFIX.length; i++) {
                if (uri.startsWith(SUPER_HANDLE_PREFIX[i])) {
                    if (StringUtils.isBlank(token)) throw new ApplicationException(ErrorCodes.EMPTY_TOKEN, "token不能为空");
                    //用户登录token校验
                    ActionResult<LoginDto> result = jwtUserService.verifyToken(token);
                    if (ErrorCodes.OK != result.getCode()) {
                        throw new ApplicationException(result.getCode(), result.getMessage());
                    }

                    LoginDto user = result.getData();
                    //校验用户权限
                    if (method.isAnnotationPresent(Permission.class)) {
                        //如果标记访问权限则验证权限
                        Permission permission = method.getAnnotation(Permission.class);
                        String permissionTag = permission.value();    //获取到标识
                        Integer userType = user.getUserType();
                        //如果不匹配(这里可以做查询数据库的操作,查询对应权限,我这里为演示方便定义了一个枚举进行简单的逻辑判断)
                        if (!AdminTypeEnum.valueOf(userType).getPermissionTag().equals(permissionTag)){
                            throw new ApplicationException(ErrorCodes.DENIED_UNAUTHORIZED, "该账号无此权限");
                        }
                    }
                }
            }
        }
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }

    /**
     * 获取到token
     * @param request HttpServletRequest
     * @return token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_KEY);
        if (token == null) {
            token = request.getParameter(TOKEN_KEY);
        }
        if (token == null) {
            Cookie cookie = getTokenFromCookie(request);
            if (cookie != null) {
                token = cookie.getValue();
            }
        }
        return token;
    }

    /**
     * 拿到Cookie中的数据
     * @param request HttpServletRequest
     * @return cookie
     */
    private Cookie getTokenFromCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (AuthInterceptor.TOKEN_KEY.equals(cookie.getName())) {
                    return cookie;
                }
            }
        }
        return null;
    }

    private void writeResponse(HttpServletResponse response, String msg) {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(ActionResult.error(msg));
        } catch (IOException e) {
            log.error("response error", e);
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

}

2、TokenCache缓存处理的核心类(这里有条件的可以用redis)

/**
 * @Description: 缓存处理的核心
 * @Author: zhang
 * @Date: 2023/10/24 15:20
 **/
@Getter
@Setter
@Component
public class TokenCache {

    /**
     * 最大缓存数量
     */
    private static final int MAXIMUM_SIZE = 1000;
    /**
     * 缓存失效时间/分钟 TimeUnit.MINUTES
     */
    private static final int EXPIRE_DURATION = 30;

    private Cache<String, String> tokenCache;

    public TokenCache() {
        this.tokenCache = CacheBuilder.newBuilder()
                .maximumSize(MAXIMUM_SIZE)  //最大缓存数量
                .expireAfterWrite(EXPIRE_DURATION, TimeUnit.MINUTES)  //缓存失效时间/分钟
                .build();
    }

    /**
     * 获取缓存中对应的值,不存在则生成新的值并返回,实现了Callable接口
     * @param key 键
     * @return token
     * @throws ExecutionException
     */
    public String getTokenOrGenerateToken(String key) throws ExecutionException {
        return tokenCache.get(key, new Callable<String>() {
            @Override
            public String call() throws Exception {
                return RandomStringUtils.randomAlphanumeric(32);
            }
        });
    }

    /**
     * 设置token
     * @param key key
     * @param token token
     */
    public void setToken(String key, String token){
        // 设置缓存值
        tokenCache.put(key, token);
    }

    /**
     * 获取缓存中的token
     * @param key key
     * @return token
     */
    public String getToken(String key){
        // 获取缓存中的token
        return tokenCache.getIfPresent(key);
    }

    /**
     * 删除缓存中的token
     * @param key key
     */
    public void deleteToken(String key){
        // 删除缓存值
        tokenCache.invalidate(key);
    }
}

3、JWT的解析和处理(这里也可以使用其他技术)

1)JwtUserService JWT服务层
/**
 * @Description: json-web-token解析类
 * @Author: zhang
 * @Date: 2023/10/24 16:51
 **/
public interface JwtUserService {

    /**
     * 创建Jwt
     * @param user User
     * @return String
     */
    String generateToken(LoginDto user);

    /**
     * 验证并解析Jwt
     * @param token String
     * @return ActionResult<User>
     */
    ActionResult<LoginDto> verifyToken(String token);

    /**
     * 刷新Jwt
     * @param token String
     * @return String
     */
    String refreshToken(String token);

    /**
     * 刷新Jwt
     * @param user User
     * @return String
     */
    String refreshToken(LoginDto user);


    /**
     * 清除过期的token
     * @param token
     */
    void clearToken(String token);

    /**
     * 生成对应key
     * @param user LoginDto对象
     * @return key
     */
    String getCacheKey(LoginDto user);

    /**
     * 验证并解析Jwt
     * @param token String
     * @return ActionResult<User>
     */
    LoginDto analysisToken(String token);
}
2)JwtUserServiceImplJWT逻辑处理层
@Slf4j
@Service(value = "jwtUserService")
@RequiredArgsConstructor(onConstructor = @__({@Autowired, @Lazy}))
public class JwtUserServiceImpl implements JwtUserService {

    /**
     * 过期时间(7天)
     */
    private static final int EXPIRE_TIME = 7;
    /**
     * 签名密钥
     */
    private static final String SECRET = "ElkI99999I1NDUxNTUiLCJ1c2VySW";
    /**
     * 签名算法
     */
    private static final Algorithm ALGORITHM = Algorithm.HMAC256(SECRET);

    private final TokenCache tokenCache;

    @Override
    public String generateToken(LoginDto user) {
        try{
            Date futureDate = DateUtils.addDay(DateTime.now(), getLoginRememberExp());
            //生成签名的时间
            String token = JWT.build(user).withIssuedAt(new Date()).withExpiresAt(futureDate).sign(ALGORITHM);
            String key = getCacheKey(user);
            if (StringUtils.isNotBlank(tokenCache.getToken(key))){  //校验是否存在这个token
                tokenCache.deleteToken(key);
            }
            tokenCache.setToken(key, token);
            return token;
        }catch (JWTCreationException | IllegalAccessException e){
            e.printStackTrace();
            throw new ApplicationException(ErrorCodes.ILLEGAL_OPERATE,e.getMessage());
        }
    }

    @Override
    public ActionResult<LoginDto> verifyToken(String token) {
        try{
            LoginDto user = JWT.decode(token, ALGORITHM, LoginDto.class);
            String key = getCacheKey(user);
            String userToken = tokenCache.getToken(key);
            if(userToken == null){
                return ActionResult.error(ErrorCodes.EXPIRED_TOKEN, "登录超时,请重新登录");
            }
            //生产进行验证token过期
//            if(SpringContextHolder.getCurrentEnv() == Env.prod) {
                if (!token.equals(userToken)) {
                    return ActionResult.error(ErrorCodes.EXPIRED_TOKEN, "登录超时,请重新登录");
                }
//            }
            return ActionResult.ok(user);
        } catch (IllegalAccessException | InstantiationException e) {
            return ActionResult.error(ErrorCodes.INVALID_ARGUMENTS, "Token格式不正确");
        } catch (TokenExpiredException e) {
            return ActionResult.error(ErrorCodes.EXPIRED_TOKEN, "登录超时,请重新登录");
        }catch (SignatureVerificationException e){
            return ActionResult.error(ErrorCodes.INVALID_SIGN, "Token签名无效");
        }catch (JWTDecodeException e){
            return ActionResult.error(ErrorCodes.ILLEGAL_OPERATE, "非法Token,请重新登录");
        }
    }

    @Override
    public String refreshToken(String token) {
        ActionResult<LoginDto> result = verifyToken(token);
        if(ErrorCodes.OK == result.getCode()){
            return generateToken(result.getData());
        }
        return null;
    }

    @Override
    public String refreshToken(LoginDto user) {
        return generateToken(user);
    }

    private int getLoginRememberExp(){
        return  EXPIRE_TIME;
    }

    @Override
    public void clearToken(String token){
        try{
            LoginDto user = JWT.decode(token, ALGORITHM, LoginDto.class);
            String key = getCacheKey(user);
            tokenCache.deleteToken(key);
        } catch (Exception e) {
            throw new ApplicationException(ErrorCodes.EXPIRED_TOKEN,"登录超时");
        }

    }

    /**
     * 这里自定义key拼接就可以了
     * @param user LoginDto对象
     * @return
     */
    @Override
    public String getCacheKey(LoginDto user){
        String key = null;
        if(user.getUserType().intValue() == AdminTypeEnum.ADMINISTRATOR.getCode().intValue()) {
            key = user.getUserType() +"_"+ user.getUserId() +"_"+ user.getStoreNo();
        }else if(user.getUserType().intValue() == AdminTypeEnum.ADMIN_SUPPER.getCode().intValue()){
            key = user.getUserType() +"_"+ user.getUserId() +"_"+ user.getStoreNo();
        }else if(user.getUserType().intValue() == AdminTypeEnum.ADMIN.getCode().intValue()){
            key = user.getUserType() +"_"+ user.getUserId() +"_"+ user.getStoreNo();
        }
        return key;
    }

    @Override
    public LoginDto analysisToken(String token) {
        LoginDto user = null;
        if (StringUtils.isEmpty(token)){
            return user;
        }
        try {
            user = JWT.decode(token, ALGORITHM, LoginDto.class);
        } catch (Exception e) {
            log.error("JwtUserServiceImpl.analysisToken token fail exception:{}", e.getMessage(), e);
        }
        return user;
    }
}

以上就是一整个对于token校验的逻辑

二、依赖项及各种工具类

1、导入JWT依赖项(这里使用Maven)

    <dependencies>
        <!-- Jwt依赖 -->
        <dependency>
            <groupId>com.github.xandb</groupId>
            <artifactId>xandb-jwt</artifactId>
            <version>2.0.1-RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>

        <!-- org.apache.commons -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>

        <!-- SLF4J 日志门面 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
            <version>2.5.4</version>
        </dependency>
        <!-- Logback 日志实现 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.5</version>
        </dependency>

        <!-- Hutool. -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.10</version>
        </dependency>

        <!-- Validator. -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>7.0.2.Final</version>
        </dependency>

    </dependencies>

2、这里是我使用的各种工具类代码

1)@NotLoginToken注解,表示接口不用校验
/**
 * @Description: 标记 @NotLoginToken 的接口将不会做登录token校验也不会做权限校验,直接放行
 * @Author: zhang
 * @Date: 2023/8/16 16:08
 **/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotLoginToken {

//    boolean required() default true;
}
2)@Permission注解,表示对接口进行权限校验
/**
 * @Description: 权限校验注解,加在控制器的接口上,用于对指定的接口权限做校验
 * @Author: zhang
 * @Date: 2023/10/25 14:33
 **/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {

    /**
     * 权限的具体标识标识
     * 用于细致化具体接口的权限
     * @return 标识(String)
     */
    String value();
}
3)LoginDto JWT解析对象
@Data
@Accessors(chain = true)
@NoArgsConstructor
public class LoginDto implements Serializable {

    private static final long serialVersionUID = 7231832734440331591L;

    @ApiModelProperty(value = "用户id")
    private Integer userId;

    @ApiModelProperty(value = "用户名")
    private String userName;

    @ApiModelProperty(value = "用户类型, 0-Administrator(唯一),1-超管,2-门店管理员")
    private Integer userType;

    @ApiModelProperty(value = "门店编号")
    private String storeNo;
}
4)AdminTypeEnum 用户(类型)枚举
/**
 * @Description: 用户类型
 * @Author: zhang
 * @Date: 2023/10/25 10:00
 **/
@Getter
public enum AdminTypeEnum {

    ADMINISTRATOR(0, "Administrator", "Administrator"),
    ADMIN_SUPPER(1, "admin.supper", "超管"),
    ADMIN(2, "store.admin", "门店管理员"),
    ;

    private final Integer code;
    /**
     * 权限标识,为展示在这里设置,实际情况建议配置在数据库
     */
    private final String permissionTag;
    private final String name;

    AdminTypeEnum(Integer code, String permissionTag, String name){
        this.code = code;
        this.permissionTag = permissionTag;
        this.name = name;
    }

    public static AdminTypeEnum valueOf(Integer code) {
        for (AdminTypeEnum c : AdminTypeEnum.values()) {
            if (c.getCode().equals(code)) {
                return c;
            }
        }
        return null;
    }

}

三、代码示例

1、对于无token用户使用@NotLoginToken注解

    @ApiOperation("测试")
    @PostMapping("/test")
    @ApiOperationSupport(author = "author")
    @NotLoginToken
    public ActionResult<?> test(@RequestBody @Valid RequestForm form) {
        //业务逻辑...
        return ActionResult.ok();
    }

这个接口使用了@NotLoginToken注解将不需要进行登录验证

2、使用@Permission注解进行权限校验

    @ApiOperation("测试")
    @PostMapping("/test")
    @ApiOperationSupport(author = "author")
    @NotLoginToken
    @Permission("test.permission")
    public ActionResult<?> test(@RequestBody @Valid RequestForm form) {
        //业务逻辑...
        return ActionResult.ok();
    }

这个注解会对传入的值进行校验,如果权限标识不匹配,将返回异常信息

以上就是对于登录鉴权的整个过程了,欢迎交流学习~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值